Skip to content

Commit 120ecb9

Browse files
authored
New: [AEA-6055] - Exclude future updates from prescription list and ensure only most recent post-dated (predicted) update returned (#2619)
## Summary - ✨ New Feature ### Details Exclude future updates from prescription list and ensure only most recent post-dated (predicted) update returned
1 parent bf789e9 commit 120ecb9

5 files changed

Lines changed: 192 additions & 22 deletions

File tree

.devcontainer/devcontainer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"streetsidesoftware.code-spell-checker",
3737
"timonwong.shellcheck",
3838
"mkhl.direnv",
39-
"github.vscode-github-actions"
39+
"github.vscode-github-actions",
40+
"sonarsource.sonarlint-vscode"
4041
],
4142
"settings": {
4243
"python.defaultInterpreterPath": "/workspaces/eps-prescription-status-update-api/.venv/bin/python",

packages/gsul/src/dynamoDBclient.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,11 @@ export async function getItemsUpdatesForPrescription(
3939
return items.map((singleUpdate) => ({
4040
itemId: String(singleUpdate.LineItemID),
4141
latestStatus: String(singleUpdate.Status),
42-
isTerminalState: String(singleUpdate.TerminalStatus) === "completed",
43-
lastUpdateDateTime: String(singleUpdate.LastModified)
42+
isTerminalState: String(singleUpdate.TerminalStatus).toLowerCase() === "completed",
43+
lastUpdateDateTime: String(singleUpdate.LastModified),
44+
...(singleUpdate.PostDatedLastModifiedSetAt && {
45+
postDatedLastModifiedSetAt: String(singleUpdate.PostDatedLastModifiedSetAt)
46+
})
4447
}))
4548
}
4649

packages/gsul/src/getStatusUpdates.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const lambdaHandler = async (event: requestType): Promise<responseType> => {
2727
// this is an async map so it returns an array of promises
2828
const itemResults = event.prescriptions.map(async (prescription) => {
2929
const queryResult = await getItemsUpdatesForPrescription(prescription.prescriptionID, prescription.odsCode, logger)
30-
return buildResult(prescription, queryResult)
30+
return filterOutFutureReduceToLatestUpdates(prescription, queryResult)
3131
})
3232

3333
// wait for all the promises to complete
@@ -40,21 +40,56 @@ const lambdaHandler = async (event: requestType): Promise<responseType> => {
4040
return response
4141
}
4242

43-
export const buildResult = (
43+
export const filterOutFutureReduceToLatestUpdates = (
4444
inputPrescription: inputPrescriptionType,
45-
items: Array<itemType>
45+
items: Array<itemType>,
46+
currentTime: number = Date.now() // injectable for testing
4647
): outputPrescriptionType => {
47-
// get unique item ids with the latest update based on lastUpdateDateTime
48-
const uniqueItems: Array<itemType> = Object.values(
49-
items.reduce(function (r, e) {
50-
if (!r[e.itemId] || Date.parse(e.lastUpdateDateTime) > Date.parse(r[e.itemId].lastUpdateDateTime)) r[e.itemId] = e
51-
return r
52-
}, {})
53-
)
48+
49+
// filter out items with future lastUpdateDateTime
50+
const validTimeUpdates = items.filter(item => {
51+
const updateTime = Date.parse(item.lastUpdateDateTime)
52+
return updateTime <= currentTime
53+
})
54+
55+
// group by itemId and separate post-dated from regular updates
56+
const itemGroups: Record<string, {regular: itemType | null, postDated: itemType | null}> = {}
57+
58+
validTimeUpdates.forEach(item => {
59+
if (!itemGroups[item.itemId]) {
60+
itemGroups[item.itemId] = {regular: null, postDated: null}
61+
}
62+
const group = itemGroups[item.itemId]
63+
64+
if (item.postDatedLastModifiedSetAt && !group.postDated) { // this is a post-dated update
65+
group.postDated = item
66+
} else if (item.postDatedLastModifiedSetAt && group.postDated) { // also a post-dated update
67+
const existingTime = Date.parse(group.postDated.postDatedLastModifiedSetAt)
68+
const newTime = Date.parse(item.postDatedLastModifiedSetAt)
69+
if (newTime > existingTime) {
70+
group.postDated = item
71+
}
72+
} else if (!group.regular) { // this is a regular update
73+
group.regular = item
74+
} else if (group.regular) { // also a regular update
75+
const existingTime = Date.parse(group.regular.lastUpdateDateTime)
76+
const newTime = Date.parse(item.lastUpdateDateTime)
77+
if (newTime > existingTime) {
78+
group.regular = item
79+
}
80+
}
81+
})
82+
83+
// flatten both regular and post-dated updates into single array
84+
const uniqueItems: Array<itemType> = []
85+
Object.values(itemGroups).forEach(group => {
86+
if (group.regular) uniqueItems.push(group.regular)
87+
if (group.postDated) uniqueItems.push(group.postDated)
88+
})
5489

5590
const result: outputPrescriptionType = {
5691
prescriptionID: inputPrescription.prescriptionID,
57-
onboarded: items.length > 0,
92+
onboarded: items.length > 0, // consider onboarded even if all updates were post-dated
5893
items: uniqueItems
5994
}
6095
return result

packages/gsul/src/schema/response.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ const itemSchema = {
1515
},
1616
lastUpdateDateTime: {
1717
type: "string"
18+
},
19+
postDatedLastModifiedSetAt: {
20+
type: "string"
1821
}
1922
}
2023
} as const

packages/gsul/tests/testBuildResult.test.ts

Lines changed: 136 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {buildResult} from "../src/getStatusUpdates"
1+
import {filterOutFutureReduceToLatestUpdates} from "../src/getStatusUpdates"
22
import {inputPrescriptionType} from "../src/schema/request"
33
import {outputPrescriptionType, itemType} from "../src/schema/response"
44

@@ -8,6 +8,8 @@ type scenariosType = {
88
queryResults: Array<itemType>
99
expectedResult: outputPrescriptionType
1010
}
11+
const now = new Date()
12+
const futureDateTime = new Date(now.valueOf() + (24 * 60 * 60 * 1000)).toISOString()
1113
const scenarios: Array<scenariosType> = [
1214
{
1315
scenarioDescription: "should return correct data when a matched prescription found",
@@ -83,7 +85,7 @@ const scenarios: Array<scenariosType> = [
8385
}
8486
},
8587
{
86-
scenarioDescription: "should return correct data for multiple items",
88+
scenarioDescription: "should return latest item for multiple updates for each of multiple statuses",
8789
inputPrescriptions: {
8890
prescriptionID: "abc",
8991
odsCode: "123"
@@ -97,9 +99,9 @@ const scenarios: Array<scenariosType> = [
9799
},
98100
{
99101
itemId: "item_1",
100-
latestStatus: "latest_item_1_status",
102+
latestStatus: "item_1_status",
101103
isTerminalState: true,
102-
lastUpdateDateTime: "1972-01-01T00:00:00Z"
104+
lastUpdateDateTime: "1972-01-01T00:00:00Z" // newer update for item_1
103105
},
104106
{
105107
itemId: "item_2",
@@ -109,9 +111,9 @@ const scenarios: Array<scenariosType> = [
109111
},
110112
{
111113
itemId: "item_2",
112-
latestStatus: "early_item_2_status",
114+
latestStatus: "item_2_status",
113115
isTerminalState: true,
114-
lastUpdateDateTime: "1970-01-01T00:00:00Z"
116+
lastUpdateDateTime: "1970-01-01T00:00:00Z" // older update for item_2
115117
}
116118
],
117119
expectedResult: {
@@ -120,7 +122,7 @@ const scenarios: Array<scenariosType> = [
120122
items: [
121123
{
122124
itemId: "item_1",
123-
latestStatus: "latest_item_1_status",
125+
latestStatus: "item_1_status",
124126
isTerminalState: true,
125127
lastUpdateDateTime: "1972-01-01T00:00:00Z"
126128
},
@@ -132,11 +134,137 @@ const scenarios: Array<scenariosType> = [
132134
}
133135
]
134136
}
137+
},
138+
{
139+
scenarioDescription: "should exclude item when post-dated update hasn't matured",
140+
inputPrescriptions: {
141+
prescriptionID: "abc",
142+
odsCode: "123"
143+
},
144+
queryResults: [
145+
{
146+
itemId: "item_1",
147+
latestStatus: "Ready to collect",
148+
isTerminalState: false,
149+
lastUpdateDateTime: "2030-01-01T00:00:00Z", // Future, no fallback
150+
postDatedLastModifiedSetAt:"1972-01-01T00:00:00Z"
151+
}
152+
],
153+
expectedResult: {
154+
prescriptionID: "abc",
155+
onboarded: true,
156+
items: []
157+
}
158+
},
159+
{
160+
scenarioDescription: "should use latest post-dated update when multiple have matured",
161+
inputPrescriptions: {
162+
prescriptionID: "abc",
163+
odsCode: "123"
164+
},
165+
queryResults: [
166+
{
167+
itemId: "item_1",
168+
latestStatus: "With pharmacy",
169+
isTerminalState: false,
170+
lastUpdateDateTime: "1970-01-01T00:00:00Z"
171+
},
172+
{
173+
itemId: "item_1",
174+
latestStatus: "Ready to collect",
175+
isTerminalState: false,
176+
lastUpdateDateTime: "1970-01-02T00:00:00Z",
177+
postDatedLastModifiedSetAt: "1970-01-01T00:00:00Z" // first RTC: post-dated and matured
178+
},
179+
{
180+
itemId: "item_1",
181+
latestStatus: "Ready to collect",
182+
isTerminalState: false,
183+
lastUpdateDateTime: futureDateTime,
184+
postDatedLastModifiedSetAt: "1970-01-02T00:00:00Z" // second RTC: post-dated and yet to mature
185+
},
186+
{
187+
itemId: "item_1",
188+
latestStatus: "With pharmacy",
189+
isTerminalState: false,
190+
lastUpdateDateTime: "1970-01-03T00:00:00Z" // Back to 'With pharmacy'
191+
},
192+
{
193+
itemId: "item_1",
194+
latestStatus: "Ready to collect",
195+
isTerminalState: false,
196+
lastUpdateDateTime: "1970-01-04T00:00:00Z",
197+
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z" // third RTC: post-dated and matured
198+
}
199+
],
200+
expectedResult: {
201+
prescriptionID: "abc",
202+
onboarded: true,
203+
items: [
204+
{
205+
itemId: "item_1",
206+
latestStatus: "With pharmacy",
207+
isTerminalState: false,
208+
lastUpdateDateTime: "1970-01-03T00:00:00Z"
209+
},
210+
{
211+
itemId: "item_1",
212+
latestStatus: "Ready to collect",
213+
isTerminalState: false,
214+
lastUpdateDateTime: "1970-01-04T00:00:00Z",
215+
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z"
216+
}
217+
]
218+
}
219+
},
220+
{
221+
scenarioDescription: "should return an item when it _has_ matured even though the post-dated time is in the future",
222+
inputPrescriptions: {
223+
prescriptionID: "abc",
224+
odsCode: "123"
225+
},
226+
queryResults: [
227+
{
228+
itemId: "item_1",
229+
latestStatus: "Ready to collect",
230+
isTerminalState: false,
231+
lastUpdateDateTime: "1970-01-01T00:00:00Z",
232+
postDatedLastModifiedSetAt: futureDateTime
233+
}
234+
],
235+
expectedResult: {
236+
prescriptionID: "abc",
237+
onboarded: true,
238+
items: [
239+
{
240+
itemId: "item_1",
241+
latestStatus: "Ready to collect",
242+
isTerminalState: false,
243+
lastUpdateDateTime: "1970-01-01T00:00:00Z",
244+
postDatedLastModifiedSetAt: futureDateTime
245+
}
246+
]
247+
}
248+
},
249+
{
250+
scenarioDescription: "should return no items when empty item status are found",
251+
inputPrescriptions: {
252+
prescriptionID: "abc",
253+
odsCode: "123"
254+
},
255+
queryResults: [],
256+
expectedResult: {
257+
prescriptionID: "abc",
258+
onboarded: false,
259+
items: []
260+
}
135261
}
136262
]
137263
describe("Unit tests for buildResults", () => {
138264
it.each<scenariosType>(scenarios)("$scenarioDescription", ({inputPrescriptions, queryResults, expectedResult}) => {
139-
const result = buildResult(inputPrescriptions, queryResults)
265+
// Use a fixed time of 2000-01-01 for tests (946684800000 ms since epoch)
266+
const fixedCurrentTime = new Date("2000-01-01T00:00:00Z").getTime()
267+
const result = filterOutFutureReduceToLatestUpdates(inputPrescriptions, queryResults, fixedCurrentTime)
140268
expect(result).toMatchObject(expectedResult)
141269
})
142270
})

0 commit comments

Comments
 (0)