Skip to content

Commit

Permalink
Update: [AEA-4180] - Update custom PSU transformation lambda to handl…
Browse files Browse the repository at this point in the history
…e DispenseComplete (#544)

## Summary

- Routine Change

### Details

Two additions.

#### Update the schema for prescription items

The item entry in an incoming cPSU request can contain a
`completedStatus` field, which can take the values `["Cancelled",
"Expired", "NotDispensed", "Collected"]`. The schema has been updated to
include this option (it is not mandatory, and may not be given).

#### Handle excess messages

External providers will send additional messages after we consider a
prescription to be completed. We may receive updates to the cPSU
translation layer that mark an item as both `status:
"DispensingComplete"`, and `completedStatus: "<Cancelled, Expired,
NotDispensed>"`. In these cases, we will already have received a
previous update flagging the item as failed in some way, so we want to
ignore such messages. To handle that, we simply do not populate the
entry array with the relevant item.

Note that downstream interfaces need to behave well with empty `entry`
arrays, for the case where all items are ignored.

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: anthony-nhs <[email protected]>
Co-authored-by: Tom Smith <[email protected]>
Co-authored-by: Jack Spagnoli <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kris Szlapa <[email protected]>
  • Loading branch information
6 people committed Jul 24, 2024
1 parent 8c1f0d5 commit cb958b5
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 57 deletions.
8 changes: 2 additions & 6 deletions packages/cpsuLambda/src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import {Bundle, Task} from "fhir/r4"
import {wrap_with_status} from "./utils"

export type Validator<Event, Message> = (event: Event, logger: Logger) => Result<Message, APIGatewayProxyResult>
export type Transformer<Message> = (
requestBody: Message,
logger: Logger,
headers: {[key: string]: string}
) => Result<Bundle<Task>, APIGatewayProxyResult>
export type Transformer<Message> = (requestBody: Message, logger: Logger) => Result<Bundle<Task>, APIGatewayProxyResult>

type EventWithHeaders = {
headers: {
Expand Down Expand Up @@ -45,7 +41,7 @@ async function generic_handler<Event extends EventWithHeaders, Message>(
append_headers(event.headers, logger)

const validator = (event: Event) => params.validator(event, logger)
const transformer = (requestBody: Message) => params.transformer(requestBody, logger, event.headers)
const transformer = (requestBody: Message) => params.transformer(requestBody, logger)
return validator(event).chain(transformer).map(wrap_with_status(200, event.headers)).value()
}

Expand Down
4 changes: 3 additions & 1 deletion packages/cpsuLambda/src/schema/format_1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ export {
itemSchema,
itemType,
itemStatusSchema,
itemStatusType
itemStatusType,
completedStatusSchema,
completedStatusType
} from "./request"
export {validator} from "./validator"
export {transformer} from "./transformer"
13 changes: 11 additions & 2 deletions packages/cpsuLambda/src/schema/format_1/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ const itemStatusSchema = {
]
} as const satisfies JSONSchema

const completedStatusSchema = {
type: "string",
enum: ["Cancelled", "Expired", "NotDispensed", "Collected"]
} as const satisfies JSONSchema

const itemSchema = {
type: "object",
required: ["itemID", "status"],
properties: {
itemID: {
type: "string"
},
status: itemStatusSchema
status: itemStatusSchema,
completedStatus: completedStatusSchema
}
} as const satisfies JSONSchema

Expand Down Expand Up @@ -82,6 +88,7 @@ type deliveryType = FromSchema<typeof deliveryTypeSchema>
type prescriptionStatusType = FromSchema<typeof prescriptionStatusSchema>
type itemType = FromSchema<typeof itemSchema>
type itemStatusType = FromSchema<typeof itemStatusSchema>
type completedStatusType = FromSchema<typeof completedStatusSchema>
export {
eventSchema,
eventType,
Expand All @@ -94,5 +101,7 @@ export {
itemSchema,
itemType,
itemStatusSchema,
itemStatusType
itemStatusType,
completedStatusSchema,
completedStatusType
}
43 changes: 33 additions & 10 deletions packages/cpsuLambda/src/schema/format_1/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,30 @@ import {
requestType,
deliveryType
} from "./request"
import {Ok, Result, collectResult} from "pratica"
import {
collectMaybe,
Just,
Maybe,
Nothing,
Ok
} from "pratica"
import {v4 as uuidv4} from "uuid"
import {Transformer} from "../../handler"
import {wrap_with_status} from "../../utils"
import "../../utils"
import {Md5} from "ts-md5"
import {Logger} from "@aws-lambda-powertools/logger"

export const transformer: Transformer<requestType> = (requestBody, _logger, headers) => {
export const transformer: Transformer<requestType> = (requestBody, _logger) => {
const bundle_entry_template = generateTemplate(requestBody)

const populated_templates = requestBody.items.map((item) =>
populateTemplate(bundle_entry_template, item, requestBody)
)
const populated_templates = requestBody.items
.map((item) => populateTemplate(bundle_entry_template, item, requestBody, _logger))
.filter((entry) => entry.isJust())

return collectResult(populated_templates).map(bundle_entries).mapErr(wrap_with_status(400, headers))
const bundleEntries = bundle_entries(
collectMaybe(populated_templates).cata({Just: (entries) => entries, Nothing: () => []})
)
return Ok(bundleEntries)
}

function bundle_entries(entries: Array<BundleEntry<Task>>): Bundle<Task> {
Expand Down Expand Up @@ -78,10 +88,23 @@ export function generateTemplate(requestBody: requestType): string {
export function populateTemplate(
template: string,
prescriptionItem: itemType,
prescriptionDetails: requestType
): Result<BundleEntry<Task>, string> {
prescriptionDetails: requestType,
_logger: Logger
): Maybe<BundleEntry<Task>> {
const entry = JSON.parse(template) as BundleEntry<Task>

if (prescriptionItem.status === "DispensingComplete") {
const forbiddenStatuses = ["Expired", "NotDispensed", "Cancelled"]

if (prescriptionItem.completedStatus && forbiddenStatuses.includes(prescriptionItem.completedStatus)) {
_logger.info("Skipping data store update for DispensingComplete - completedStatus is an ignored value", {
itemID: prescriptionItem.itemID,
completedStatus: prescriptionItem.completedStatus
})
return Nothing
}
}

const businessStatus = getBusinessStatus(prescriptionDetails.deliveryType, prescriptionItem.status)

entry.resource!.businessStatus!.coding![0].code = businessStatus
Expand All @@ -103,7 +126,7 @@ export function populateTemplate(
const urn = `urn:uuid:${uuid}`
entry.fullUrl = urn

return Ok(entry)
return Just(entry)
}

/**
Expand Down
125 changes: 87 additions & 38 deletions packages/cpsuLambda/tests/format_1/business_states.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import {
itemType,
requestType,
deliveryType,
itemStatusType
itemStatusType,
completedStatusType
} from "../../src/schema/format_1"
import {Logger} from "@aws-lambda-powertools/logger"

interface BusinessStatusTestCase {
itemStatus: itemStatusType
Expand All @@ -16,8 +18,11 @@ interface BusinessStatusTestCase {
interface PopulateTemplateTestCase {
itemStatus: itemStatusType
deliveryType: deliveryType
expectedBusinessStatus: string
expectedTaskStatus: string
expectItemDefined: boolean
// Optional fields, which may or may not be present
expectedBusinessStatus?: string
expectedTaskStatus?: string
itemCompletedStatus?: completedStatusType
}

describe("getBusinessStatus function", () => {
Expand All @@ -44,52 +49,96 @@ describe("populateTemplate function", () => {
itemStatus: "ReadyForCollection",
deliveryType: "Robot Collection",
expectedBusinessStatus: "Ready to Dispatch",
expectedTaskStatus: "in-progress"
expectedTaskStatus: "in-progress",
expectItemDefined: true
},
{
itemStatus: "DispensingComplete",
deliveryType: "Robot Collection",
expectedBusinessStatus: "Dispatched",
expectedTaskStatus: "completed"
expectedTaskStatus: "completed",
expectItemDefined: true
},
{
itemStatus: "DispensingComplete",
itemCompletedStatus: "Cancelled",
deliveryType: "Not known",
expectItemDefined: false
},
{
itemStatus: "DispensingComplete",
itemCompletedStatus: "Expired",
deliveryType: "Not known",
expectItemDefined: false
},
{
itemStatus: "DispensingComplete",
itemCompletedStatus: "NotDispensed",
deliveryType: "Not known",
expectItemDefined: false
},
{
itemStatus: "DispensingComplete",
itemCompletedStatus: "Collected",
deliveryType: "Robot Collection",
expectedBusinessStatus: "Dispatched",
expectedTaskStatus: "completed",
expectItemDefined: true
}
]

testCases.forEach(({itemStatus, deliveryType, expectedBusinessStatus, expectedTaskStatus}) => {
it(`should populate template correctly for itemStatus: ${itemStatus} and deliveryType: ${deliveryType}`, () => {
const template: string = generateTemplate({
MessageType: "ExampleMessageType",
items: [{itemID: "item1", status: itemStatus}],
prescriptionUUID: "123456789",
nHSCHI: "123456",
messageDate: new Date().toISOString(),
oDSCode: "XYZ",
deliveryType: deliveryType,
repeatNo: 1
})
testCases.forEach(
({
itemStatus,
deliveryType,
expectedBusinessStatus,
expectedTaskStatus,
expectItemDefined,
itemCompletedStatus
}) => {
it(`should populate template for itemStatus: ${itemStatus} and completedStatus: ${itemCompletedStatus}`, () => {
const template: string = generateTemplate({
MessageType: "ExampleMessageType",
items: [{itemID: "item1", status: itemStatus, completedStatus: itemCompletedStatus}],
prescriptionUUID: "123456789",
nHSCHI: "123456",
messageDate: new Date().toISOString(),
oDSCode: "XYZ",
deliveryType: deliveryType,
repeatNo: 1
})

const prescriptionItem: itemType = {
itemID: "item1",
status: itemStatus
}
const prescriptionItem: itemType = {
itemID: "item1",
status: itemStatus,
completedStatus: itemCompletedStatus
}

const prescriptionDetails: requestType = {
MessageType: "ExampleMessageType",
items: [{itemID: "item1", status: itemStatus}],
prescriptionUUID: "123456789",
nHSCHI: "123456",
messageDate: new Date().toISOString(),
oDSCode: "XYZ",
deliveryType: deliveryType,
repeatNo: 1
}
const prescriptionDetails: requestType = {
MessageType: "ExampleMessageType",
items: [{itemID: "item1", status: itemStatus, completedStatus: itemCompletedStatus}],
prescriptionUUID: "123456789",
nHSCHI: "123456",
messageDate: new Date().toISOString(),
oDSCode: "XYZ",
deliveryType: deliveryType,
repeatNo: 1
}

const result = populateTemplate(template, prescriptionItem, prescriptionDetails)
const logger = new Logger()
const result = populateTemplate(template, prescriptionItem, prescriptionDetails, logger)

if (result.isOk()) {
const entry: BundleEntry<Task> = result.value() as BundleEntry<Task>
expect(entry.resource!.businessStatus!.coding![0].code).toEqual(expectedBusinessStatus)
expect(entry.resource!.status).toEqual(expectedTaskStatus)
}
})
})

if (expectItemDefined) {
// Check that the template is correctly populated
expect(entry.resource!.businessStatus!.coding![0].code).toEqual(expectedBusinessStatus)
expect(entry.resource!.status).toEqual(expectedTaskStatus)
} else {
// If expectItemDefined is false, we expect Nothing
expect(result.isNothing()).toBeTruthy()
}
})
}
)
})
75 changes: 75 additions & 0 deletions packages/cpsuLambda/tests/testHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,79 @@ describe("format_1 handler", () => {
expect(response_1_body.entry[1].fullUrl).toBe(response_2_body.entry[1].fullUrl)
expect(response_1_body.entry[1].resource.id).toBe(response_2_body.entry[1].resource.id)
})

describe("format_1 handler with completedStatus checks", () => {
const testData = [
{
status: "DispensingComplete",
completedStatus: "Expired",
expectedEntriesCount: 0,
description: "should exclude items with Expired completedStatus"
},
{
status: "DispensingComplete",
completedStatus: "NotDispensed",
expectedEntriesCount: 0,
description: "should exclude items with NotDispensed completedStatus"
},
{
status: "DispensingComplete",
completedStatus: "Cancelled",
expectedEntriesCount: 0,
description: "should exclude items with Cancelled completedStatus"
},
{
status: "DispensingComplete",
completedStatus: "Collected",
expectedEntriesCount: 2,
description: "should include items with Collected completedStatus"
},
{
status: "Pending",
expectedEntriesCount: 2,
description: "should include items with no completedStatus definition"
}
]

testData.forEach(({status, completedStatus, expectedEntriesCount, description}) => {
test(description, async () => {
const body = format_1_request()
body.items.forEach((item: {completedStatus?: string; status: string}) => {
item.status = status
if (completedStatus !== undefined) {
item.completedStatus = completedStatus
}
})

const event = {
headers: {},
body
}

const response = await format_1_handler(event as format_1.eventType, dummyContext)
const responseBody = JSON.parse(response.body)
const entries = responseBody.entry
expect(entries.length).toBe(expectedEntriesCount)
})
})

test("Should handle mixed statuses correctly", async () => {
const body = format_1_request()
// Assigning different statuses to different items
body.items[1] = {...body.items[1], status: "DispensingComplete", completedStatus: "Expired"}

const event = {
headers: {},
body
}

const response = await format_1_handler(event as format_1.eventType, dummyContext)
const responseBody = JSON.parse(response.body)
const entries = responseBody.entry

// Only the valid item should be included
expect(entries.length).toBe(1)
expect(entries[0].resource.status).toBe("in-progress")
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
"qty": "12",
"dosage": "As Directed",
"status": "Pending"
},
{
"itemID": "73014c50-1bd1-4361-9c9f-d587d7d03e66",
"dMDCode": "323416001",
"dMDDesc": "Phenoxymethylpenicillin 250mg tablets",
"uOMDesc": "tablet",
"qty": "28",
"dosage": "As Directed",
"status": "NotDispensed",
"completedStatus": "Expired"
}
],
"MessageType": "PrescriptionStatusChanged"
Expand Down

0 comments on commit cb958b5

Please sign in to comment.