Skip to content
Merged
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
"streetsidesoftware.code-spell-checker",
"timonwong.shellcheck",
"mkhl.direnv",
"github.vscode-github-actions"
"github.vscode-github-actions",
"sonarsource.sonarlint-vscode"
],
"settings": {
"python.defaultInterpreterPath": "/workspaces/eps-prescription-status-update-api/.venv/bin/python",
Expand Down
7 changes: 5 additions & 2 deletions packages/gsul/src/dynamoDBclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ export async function getItemsUpdatesForPrescription(
return items.map((singleUpdate) => ({
itemId: String(singleUpdate.LineItemID),
latestStatus: String(singleUpdate.Status),
isTerminalState: String(singleUpdate.TerminalStatus) === "completed",
lastUpdateDateTime: String(singleUpdate.LastModified)
isTerminalState: String(singleUpdate.TerminalStatus).toLowerCase() === "completed",
lastUpdateDateTime: String(singleUpdate.LastModified),
...(singleUpdate.PostDatedLastModifiedSetAt && {
postDatedLastModifiedSetAt: String(singleUpdate.PostDatedLastModifiedSetAt)
})
}))
}

Expand Down
57 changes: 46 additions & 11 deletions packages/gsul/src/getStatusUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const lambdaHandler = async (event: requestType): Promise<responseType> => {
// this is an async map so it returns an array of promises
const itemResults = event.prescriptions.map(async (prescription) => {
const queryResult = await getItemsUpdatesForPrescription(prescription.prescriptionID, prescription.odsCode, logger)
return buildResult(prescription, queryResult)
return filterOutFutureReduceToLatestUpdates(prescription, queryResult)
})

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

export const buildResult = (
export const filterOutFutureReduceToLatestUpdates = (
inputPrescription: inputPrescriptionType,
items: Array<itemType>
items: Array<itemType>,
currentTime: number = Date.now() // injectable for testing
Comment thread
wildjames marked this conversation as resolved.
): outputPrescriptionType => {
// get unique item ids with the latest update based on lastUpdateDateTime
const uniqueItems: Array<itemType> = Object.values(
items.reduce(function (r, e) {
if (!r[e.itemId] || Date.parse(e.lastUpdateDateTime) > Date.parse(r[e.itemId].lastUpdateDateTime)) r[e.itemId] = e
return r
}, {})
)

// filter out items with future lastUpdateDateTime
const validTimeUpdates = items.filter(item => {
const updateTime = Date.parse(item.lastUpdateDateTime)
return updateTime <= currentTime
})

// group by itemId and separate post-dated from regular updates
const itemGroups: Record<string, {regular: itemType | null, postDated: itemType | null}> = {}

validTimeUpdates.forEach(item => {
if (!itemGroups[item.itemId]) {
itemGroups[item.itemId] = {regular: null, postDated: null}
}
const group = itemGroups[item.itemId]

if (item.postDatedLastModifiedSetAt && !group.postDated) { // this is a post-dated update
group.postDated = item
} else if (item.postDatedLastModifiedSetAt && group.postDated) { // also a post-dated update
const existingTime = Date.parse(group.postDated.postDatedLastModifiedSetAt)
const newTime = Date.parse(item.postDatedLastModifiedSetAt)
if (newTime > existingTime) {
group.postDated = item
}
} else if (!group.regular) { // this is a regular update
group.regular = item
} else if (group.regular) { // also a regular update
const existingTime = Date.parse(group.regular.lastUpdateDateTime)
const newTime = Date.parse(item.lastUpdateDateTime)
if (newTime > existingTime) {
group.regular = item
}
}
})

// flatten both regular and post-dated updates into single array
const uniqueItems: Array<itemType> = []
Object.values(itemGroups).forEach(group => {
if (group.regular) uniqueItems.push(group.regular)
if (group.postDated) uniqueItems.push(group.postDated)
})

const result: outputPrescriptionType = {
prescriptionID: inputPrescription.prescriptionID,
onboarded: items.length > 0,
onboarded: items.length > 0, // consider onboarded even if all updates were post-dated
items: uniqueItems
}
return result
Expand Down
3 changes: 3 additions & 0 deletions packages/gsul/src/schema/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const itemSchema = {
},
lastUpdateDateTime: {
type: "string"
},
postDatedLastModifiedSetAt: {
type: "string"
}
}
} as const
Expand Down
144 changes: 136 additions & 8 deletions packages/gsul/tests/testBuildResult.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {buildResult} from "../src/getStatusUpdates"
import {filterOutFutureReduceToLatestUpdates} from "../src/getStatusUpdates"
import {inputPrescriptionType} from "../src/schema/request"
import {outputPrescriptionType, itemType} from "../src/schema/response"

Expand All @@ -8,6 +8,8 @@ type scenariosType = {
queryResults: Array<itemType>
expectedResult: outputPrescriptionType
}
const now = new Date()
const futureDateTime = new Date(now.valueOf() + (24 * 60 * 60 * 1000)).toISOString()
const scenarios: Array<scenariosType> = [
{
scenarioDescription: "should return correct data when a matched prescription found",
Expand Down Expand Up @@ -83,7 +85,7 @@ const scenarios: Array<scenariosType> = [
}
},
{
scenarioDescription: "should return correct data for multiple items",
scenarioDescription: "should return latest item for multiple updates for each of multiple statuses",
inputPrescriptions: {
prescriptionID: "abc",
odsCode: "123"
Expand All @@ -97,9 +99,9 @@ const scenarios: Array<scenariosType> = [
},
{
itemId: "item_1",
latestStatus: "latest_item_1_status",
latestStatus: "item_1_status",
isTerminalState: true,
lastUpdateDateTime: "1972-01-01T00:00:00Z"
lastUpdateDateTime: "1972-01-01T00:00:00Z" // newer update for item_1
},
{
itemId: "item_2",
Expand All @@ -109,9 +111,9 @@ const scenarios: Array<scenariosType> = [
},
{
itemId: "item_2",
latestStatus: "early_item_2_status",
latestStatus: "item_2_status",
isTerminalState: true,
lastUpdateDateTime: "1970-01-01T00:00:00Z"
lastUpdateDateTime: "1970-01-01T00:00:00Z" // older update for item_2
}
],
expectedResult: {
Expand All @@ -120,7 +122,7 @@ const scenarios: Array<scenariosType> = [
items: [
{
itemId: "item_1",
latestStatus: "latest_item_1_status",
latestStatus: "item_1_status",
isTerminalState: true,
lastUpdateDateTime: "1972-01-01T00:00:00Z"
},
Expand All @@ -132,11 +134,137 @@ const scenarios: Array<scenariosType> = [
}
]
}
},
{
scenarioDescription: "should exclude item when post-dated update hasn't matured",
inputPrescriptions: {
prescriptionID: "abc",
odsCode: "123"
},
queryResults: [
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "2030-01-01T00:00:00Z", // Future, no fallback
postDatedLastModifiedSetAt:"1972-01-01T00:00:00Z"
}
],
expectedResult: {
prescriptionID: "abc",
onboarded: true,
items: []
}
},
{
scenarioDescription: "should use latest post-dated update when multiple have matured",
inputPrescriptions: {
prescriptionID: "abc",
odsCode: "123"
},
queryResults: [
{
itemId: "item_1",
latestStatus: "With pharmacy",
isTerminalState: false,
lastUpdateDateTime: "1970-01-01T00:00:00Z"
},
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "1970-01-02T00:00:00Z",
postDatedLastModifiedSetAt: "1970-01-01T00:00:00Z" // first RTC: post-dated and matured
},
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: futureDateTime,
postDatedLastModifiedSetAt: "1970-01-02T00:00:00Z" // second RTC: post-dated and yet to mature
},
{
itemId: "item_1",
latestStatus: "With pharmacy",
isTerminalState: false,
lastUpdateDateTime: "1970-01-03T00:00:00Z" // Back to 'With pharmacy'
},
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "1970-01-04T00:00:00Z",
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z" // third RTC: post-dated and matured
}
],
expectedResult: {
prescriptionID: "abc",
onboarded: true,
items: [
{
itemId: "item_1",
latestStatus: "With pharmacy",
isTerminalState: false,
lastUpdateDateTime: "1970-01-03T00:00:00Z"
},
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "1970-01-04T00:00:00Z",
postDatedLastModifiedSetAt: "1970-01-03T00:00:00Z"
}
]
}
},
{
scenarioDescription: "should return an item when it _has_ matured even though the post-dated time is in the future",
inputPrescriptions: {
prescriptionID: "abc",
odsCode: "123"
},
queryResults: [
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "1970-01-01T00:00:00Z",
postDatedLastModifiedSetAt: futureDateTime
}
],
expectedResult: {
prescriptionID: "abc",
onboarded: true,
items: [
{
itemId: "item_1",
latestStatus: "Ready to collect",
isTerminalState: false,
lastUpdateDateTime: "1970-01-01T00:00:00Z",
postDatedLastModifiedSetAt: futureDateTime
}
]
}
},
{
scenarioDescription: "should return no items when empty item status are found",
inputPrescriptions: {
prescriptionID: "abc",
odsCode: "123"
},
queryResults: [],
expectedResult: {
prescriptionID: "abc",
onboarded: false,
items: []
}
}
]
describe("Unit tests for buildResults", () => {
it.each<scenariosType>(scenarios)("$scenarioDescription", ({inputPrescriptions, queryResults, expectedResult}) => {
const result = buildResult(inputPrescriptions, queryResults)
// Use a fixed time of 2000-01-01 for tests (946684800000 ms since epoch)
const fixedCurrentTime = new Date("2000-01-01T00:00:00Z").getTime()
const result = filterOutFutureReduceToLatestUpdates(inputPrescriptions, queryResults, fixedCurrentTime)
expect(result).toMatchObject(expectedResult)
})
})