Skip to content

Commit 242d654

Browse files
committed
fix: post dated inclusions subsequently revoked
chore: rename test to match func
1 parent 7c245e9 commit 242d654

2 files changed

Lines changed: 173 additions & 36 deletions

File tree

packages/gsul/src/getStatusUpdates.ts

Lines changed: 93 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,53 +45,113 @@ export const filterOutFutureReduceToLatestUpdates = (
4545
items: Array<itemType>,
4646
currentTime: number = Date.now() // injectable for testing
4747
): outputPrescriptionType => {
48+
// Track whether prescription was onboarded (had any items)
49+
const onboarded = items.length > 0
4850

49-
// filter out items with future lastUpdateDateTime
50-
const validTimeUpdates = items.filter(item => {
51-
const updateTime = Date.parse(item.lastUpdateDateTime)
52-
return updateTime <= currentTime
51+
// 1. filter out PostDatedLastModifiedSetAt where LastModified > currentTime
52+
const maturedItems = items.filter((item) => {
53+
if (item.postDatedLastModifiedSetAt) {
54+
const lastModifiedTime = new Date(item.lastUpdateDateTime).getTime()
55+
return lastModifiedTime <= currentTime
56+
}
57+
return true
5358
})
5459

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}
60+
// 2. Filter out post-dated that have been revoked
61+
const filteredItems = maturedItems.filter((item) => {
62+
if (!item.postDatedLastModifiedSetAt) {
63+
return true // Non-post-dated items are always included
6164
}
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
65+
66+
// This is a post-dated item. Check if it's been revoked.
67+
// A post-dated item is revoked if there's a non-post-dated update for the same itemId
68+
// that came after the PostDatedLastModifiedSetAt but before this item's lastUpdateDateTime
69+
const postDatedSetTime = new Date(item.postDatedLastModifiedSetAt).getTime()
70+
const itemLastModified = new Date(item.lastUpdateDateTime).getTime()
71+
72+
const isRevoked = maturedItems.some((otherItem) => {
73+
if (otherItem.itemId !== item.itemId) {
74+
return false // Different item
75+
}
76+
if (otherItem.postDatedLastModifiedSetAt) {
77+
return false // Another post-dated item doesn't revoke this one
7178
}
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+
const otherLastModified = new Date(otherItem.lastUpdateDateTime).getTime()
80+
// Revoked if the other item came after the post-dated was set but before it matured
81+
return otherLastModified > postDatedSetTime && otherLastModified < itemLastModified
82+
})
83+
84+
return !isRevoked
85+
})
86+
87+
// 3: Reduce to latest update for each itemId + status combination
88+
const latestByItemIdAndStatus = filteredItems.reduce((acc, item) => {
89+
const key = `${item.itemId}:${item.latestStatus}`
90+
const existingItem = acc.get(key)
91+
92+
if (!existingItem) {
93+
acc.set(key, item)
94+
} else {
95+
const existingTime = new Date(existingItem.lastUpdateDateTime).getTime()
96+
const itemTime = new Date(item.lastUpdateDateTime).getTime()
97+
98+
if (itemTime > existingTime) {
99+
acc.set(key, item)
79100
}
80101
}
102+
103+
return acc
104+
}, new Map<string, itemType>())
105+
106+
// Convert map to array
107+
let reducedItems = Array.from(latestByItemIdAndStatus.values())
108+
109+
// Step 2b: For each itemId, if there are multiple non-post-dated items, keep only the latest one
110+
const itemsByItemId = reducedItems.reduce((acc, item) => {
111+
if (!acc.has(item.itemId)) {
112+
acc.set(item.itemId, [])
113+
}
114+
acc.get(item.itemId)!.push(item)
115+
return acc
116+
}, new Map<string, Array<itemType>>())
117+
118+
const finalItems: Array<itemType> = []
119+
itemsByItemId.forEach((items) => {
120+
const nonPostDatedItems = items.filter((item) => !item.postDatedLastModifiedSetAt)
121+
const postDatedItems = items.filter((item) => item.postDatedLastModifiedSetAt)
122+
123+
// Keep only the latest non-post-dated item
124+
if (nonPostDatedItems.length > 0) {
125+
const latestNonPostDated = nonPostDatedItems.reduce((latest, item) => {
126+
const latestTime = new Date(latest.lastUpdateDateTime).getTime()
127+
const itemTime = new Date(item.lastUpdateDateTime).getTime()
128+
return itemTime > latestTime ? item : latest
129+
})
130+
finalItems.push(latestNonPostDated)
131+
}
132+
133+
// Keep all post-dated items (already reduced to latest of each status)
134+
finalItems.push(...postDatedItems)
81135
})
82136

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)
137+
// Step 3: Sort by itemId first, then by lastUpdateDateTime within each itemId
138+
const sortedItems = finalItems.sort((a, b) => {
139+
// First sort by itemId
140+
if (a.itemId !== b.itemId) {
141+
return a.itemId.localeCompare(b.itemId)
142+
}
143+
// Then sort by lastUpdateDateTime
144+
const timeA = new Date(a.lastUpdateDateTime).getTime()
145+
const timeB = new Date(b.lastUpdateDateTime).getTime()
146+
return timeA - timeB
88147
})
89148

90149
const result: outputPrescriptionType = {
91150
prescriptionID: inputPrescription.prescriptionID,
92-
onboarded: items.length > 0, // consider onboarded even if all updates were post-dated
93-
items: uniqueItems
151+
onboarded: onboarded,
152+
items: sortedItems
94153
}
154+
95155
return result
96156
}
97157

packages/gsul/tests/testBuildResult.test.ts renamed to packages/gsul/tests/testFilterOutFutureReduceToLatest.test.ts

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -194,14 +194,14 @@ const scenarios: Array<scenariosType> = [
194194
itemId: "item_1",
195195
latestStatus: "With pharmacy",
196196
isTerminalState: false,
197-
lastUpdateDateTime: "1970-01-03T00:00:00Z" // Back to 'With pharmacy'
197+
lastUpdateDateTime: "1970-01-03T00:00:00Z" // revoke RTC, back to 'With pharmacy'
198198
},
199199
{
200200
itemId: "item_1",
201201
latestStatus: "Ready to collect",
202202
isTerminalState: false,
203203
lastUpdateDateTime: "1970-01-04T00:00:00Z",
204-
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z" // third RTC: post-dated and matured
204+
postDatedLastModifiedSetAt: "1970-01-03T00:00:01Z" // third RTC: post-dated and matured
205205
}
206206
],
207207
expectedResult: {
@@ -219,7 +219,7 @@ const scenarios: Array<scenariosType> = [
219219
latestStatus: "Ready to collect",
220220
isTerminalState: false,
221221
lastUpdateDateTime: "1970-01-04T00:00:00Z",
222-
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z"
222+
postDatedLastModifiedSetAt: "1970-01-03T00:00:01Z"
223223
}
224224
]
225225
}
@@ -267,6 +267,83 @@ const scenarios: Array<scenariosType> = [
267267
onboarded: false,
268268
items: []
269269
}
270+
},
271+
{
272+
scenarioDescription: "should return With pharmacy when RTC has been revoked",
273+
currentTime: new Date("2025-12-11T12:00:00Z").getTime(),
274+
inputPrescriptions: {
275+
prescriptionID: "abc",
276+
odsCode: "123"
277+
},
278+
queryResults: [
279+
{
280+
itemId: "item_1",
281+
latestStatus: "Ready to collect",
282+
isTerminalState: false,
283+
lastUpdateDateTime: "2025-12-11T10:00:00Z",
284+
postDatedLastModifiedSetAt: "2025-12-10T10:00:00Z"
285+
},
286+
{
287+
itemId: "item_1",
288+
latestStatus: "With pharmacy",
289+
isTerminalState: false,
290+
lastUpdateDateTime: "2025-12-10T11:00:00Z"
291+
}
292+
],
293+
expectedResult: {
294+
prescriptionID: "abc",
295+
onboarded: true,
296+
items: [
297+
{
298+
itemId: "item_1",
299+
latestStatus: "With pharmacy",
300+
isTerminalState: false,
301+
lastUpdateDateTime: "2025-12-10T11:00:00Z"
302+
}
303+
]
304+
}
305+
},
306+
{
307+
scenarioDescription: "should return With pharmacy when RTC has been revoked but later RTC has matured",
308+
currentTime: new Date("2025-12-11T19:00:00Z").getTime(),
309+
inputPrescriptions: {
310+
prescriptionID: "abc",
311+
odsCode: "123"
312+
},
313+
queryResults: [
314+
{
315+
itemId: "item_1",
316+
latestStatus: "Ready to collect",
317+
isTerminalState: false,
318+
lastUpdateDateTime: "2025-12-11T19:00:00Z",
319+
postDatedLastModifiedSetAt: "2025-12-10T13:00:00Z"
320+
},
321+
{
322+
itemId: "item_1",
323+
latestStatus: "With pharmacy",
324+
isTerminalState: false,
325+
lastUpdateDateTime: "2025-12-10T11:00:00Z"
326+
}
327+
],
328+
expectedResult: {
329+
prescriptionID: "abc",
330+
onboarded: true,
331+
items: [
332+
{
333+
itemId: "item_1",
334+
latestStatus: "With pharmacy",
335+
isTerminalState: false,
336+
lastUpdateDateTime: "2025-12-10T11:00:00Z"
337+
},
338+
{
339+
itemId: "item_1",
340+
latestStatus: "Ready to collect",
341+
isTerminalState: false,
342+
lastUpdateDateTime: "2025-12-11T19:00:00Z",
343+
postDatedLastModifiedSetAt: "2025-12-10T13:00:00Z"
344+
}
345+
]
346+
}
270347
}
271348
]
272349
describe("Unit tests for buildResults", () => {

0 commit comments

Comments
 (0)