diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3b0620de0..48cc1a46e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -52,8 +52,7 @@ "python.analysis.extraPaths": [], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, + "flake8.enabled": true, "python.linting.enabled": true, // required to format on save "editor.defaultFormatter": "rvest.vs-code-prettier-eslint", "editor.formatOnPaste": false, // required diff --git a/.vscode/prescriptionsforpatients.code-workspace b/.vscode/prescriptionsforpatients.code-workspace index 13c4d1bd9..379a5b55b 100644 --- a/.vscode/prescriptionsforpatients.code-workspace +++ b/.vscode/prescriptionsforpatients.code-workspace @@ -77,6 +77,7 @@ "nhsdlogin", "nhslogin", "NOSONAR", + "nppt", "OIDC", "onboarded", "Organisation", diff --git a/SAMtemplates/functions/main.yaml b/SAMtemplates/functions/main.yaml index 60f4c19cd..1e2094203 100644 --- a/SAMtemplates/functions/main.yaml +++ b/SAMtemplates/functions/main.yaml @@ -115,6 +115,7 @@ Resources: Environment: Variables: LOG_LEVEL: !Ref LogLevel + EXPECT_STATUS_UPDATES: !Ref ToggleGetStatusUpdates Metadata: BuildMethod: esbuild BuildProperties: diff --git a/SAMtemplates/state_machines/GetMyPrescriptionsStateMachine.asl.json b/SAMtemplates/state_machines/GetMyPrescriptionsStateMachine.asl.json index 59d927755..31b422aac 100644 --- a/SAMtemplates/state_machines/GetMyPrescriptionsStateMachine.asl.json +++ b/SAMtemplates/state_machines/GetMyPrescriptionsStateMachine.asl.json @@ -60,7 +60,7 @@ "FunctionName": "${GetStatusUpdatesFunctionArn}" }, "InputPath": "$.statusUpdateData", - "Next": "Get Status Updates Result", + "Next": "Enrich Prescriptions", "ResultSelector": { "Payload.$": "$.Payload" }, @@ -70,23 +70,11 @@ "ErrorEquals": [ "States.ALL" ], - "Next": "Catch All Error" + "Next": "Enrich Prescriptions", + "ResultPath": "$.error" } ] }, - "Get Status Updates Result": { - "Type": "Choice", - "Choices": [ - { - "Not": { - "Variable": "$.StatusUpdates.Payload.isSuccess", - "BooleanEquals": true - }, - "Next": "Catch All Error" - } - ], - "Default": "Enrich Prescriptions" - }, "Enrich Prescriptions": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke", diff --git a/packages/enrichPrescriptions/src/enrichPrescriptions.ts b/packages/enrichPrescriptions/src/enrichPrescriptions.ts index 7d19f9739..8cf05c3b6 100644 --- a/packages/enrichPrescriptions/src/enrichPrescriptions.ts +++ b/packages/enrichPrescriptions/src/enrichPrescriptions.ts @@ -4,14 +4,22 @@ import {LogLevel} from "@aws-lambda-powertools/logger/types" import middy from "@middy/core" import inputOutputLogger from "@middy/input-output-logger" import {Bundle} from "fhir/r4" -import {StatusUpdates, applyStatusUpdates} from "./statusUpdates" +import { + StatusUpdateRequest, + StatusUpdates, + UpdatesScenario, + applyStatusUpdates, + applyTemporaryStatusUpdates, + getUpdatesScenario +} from "./statusUpdates" import {TraceIDs, lambdaResponse} from "./responses" export const LOG_LEVEL = process.env.LOG_LEVEL as LogLevel export const logger = new Logger({serviceName: "enrichPrescriptions", logLevel: LOG_LEVEL}) export type EnrichPrescriptionsEvent = { - fhir: Bundle, + fhir: Bundle + statusUpdateData: StatusUpdateRequest StatusUpdates?: {Payload: StatusUpdates} traceIDs: TraceIDs } @@ -28,12 +36,23 @@ export async function lambdaHandler(event: EnrichPrescriptionsEvent) { const searchsetBundle = event.fhir const statusUpdates = event.StatusUpdates?.Payload + const updatesScenario = getUpdatesScenario(statusUpdates) - if (statusUpdates) { - logger.info("Applying status updates.") - applyStatusUpdates(searchsetBundle, statusUpdates) - } else { - logger.info("No status updates to apply.") + switch (updatesScenario) { + case UpdatesScenario.Present: { + logger.info("Applying status updates.") + applyStatusUpdates(searchsetBundle, statusUpdates!) + break + } + case UpdatesScenario.ExpectedButAbsent: { + logger.info("Call to get status updates was unsuccessful. Applying temporary status updates.") + const statusUpdateRequest = event.statusUpdateData! + applyTemporaryStatusUpdates(searchsetBundle, statusUpdateRequest) + break + } + default: { + logger.info("Get Status Updates is toggled-off. No status updates to apply.") + } } return lambdaResponse(200, searchsetBundle, event.traceIDs) diff --git a/packages/enrichPrescriptions/src/fhirUtils.ts b/packages/enrichPrescriptions/src/fhirUtils.ts index 88e6ee01c..c08a02480 100644 --- a/packages/enrichPrescriptions/src/fhirUtils.ts +++ b/packages/enrichPrescriptions/src/fhirUtils.ts @@ -2,7 +2,8 @@ import { Bundle, BundleEntry, FhirResource, - MedicationRequest + MedicationRequest, + Organization } from "fhir/r4" export type Entry = BundleEntry @@ -32,8 +33,23 @@ export function isolatePrescriptions(searchsetBundle: Bundle): Array { } export function isolateMedicationRequests(prescription: Bundle): Array | undefined { - return prescription.entry?.filter(entry => entry?.resource?.resourceType === "MedicationRequest") - .map(entry => entry?.resource as MedicationRequest) + return prescription.entry + ?.filter((entry) => entry?.resource?.resourceType === "MedicationRequest") + .map((entry) => entry?.resource as MedicationRequest) +} + +export function isolatePerformerReference(medicationRequests: Array): string | undefined { + for (const medicationRequest of medicationRequests) { + const reference = medicationRequest.dispenseRequest?.performer?.reference + if (reference !== undefined) { + return reference + } + } +} + +export function isolatePerformerOrganisation(reference: string, prescription: Bundle): Organization { + const filter = (entry: Entry) => entry.fullUrl! === reference + return filterAndTypeBundleEntries(prescription, filter)[0] } function filterAndTypeBundleEntries(bundle: Bundle, filter: (entry: Entry) => boolean): Array { diff --git a/packages/enrichPrescriptions/src/statusUpdates.ts b/packages/enrichPrescriptions/src/statusUpdates.ts index 40eb53ba1..6c73c14e6 100644 --- a/packages/enrichPrescriptions/src/statusUpdates.ts +++ b/packages/enrichPrescriptions/src/statusUpdates.ts @@ -1,19 +1,30 @@ import {Bundle, Extension, MedicationRequest} from "fhir/r4" import moment, {Moment} from "moment" -import {isolateMedicationRequests, isolatePrescriptions} from "./fhirUtils" +import { + isolateMedicationRequests, + isolatePerformerOrganisation, + isolatePerformerReference, + isolatePrescriptions +} from "./fhirUtils" import {logger} from "./enrichPrescriptions" export const EXTENSION_URL = "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory" -export const DEFAULT_EXTENSION_STATUS = "With Pharmacy" -export const NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS = "With Pharmacy but Tracking not Supported" export const VALUE_CODING_SYSTEM = "https://fhir.nhs.uk/CodeSystem/task-businessStatus-nppt" export const ONE_WEEK_IN_MS = 604800000 +export const DEFAULT_EXTENSION_STATUS = "With Pharmacy" +export const NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS = "With Pharmacy but Tracking not Supported" +export const TEMPORARILY_UNAVAILABLE_STATUS = "Tracking Temporarily Unavailable" +export const APPROVED_STATUS = "Prescriber Approved" +export const CANCELLED_STATUS = "Prescriber Cancelled" + +export const expectStatusUpdates = () => process.env.EXPECT_STATUS_UPDATES === "true" + type MedicationRequestStatus = "completed" | "active" type UpdateItem = { isTerminalState: boolean - itemId: string + itemId?: string lastUpdateDateTime: string latestStatus: string } @@ -30,12 +41,21 @@ export type StatusUpdates = { schemaVersion: number } +export type Prescription = { + odsCode: string + prescriptionID: string +} + +export type StatusUpdateRequest = { + schemaVersion: number + prescriptions: Array +} + function defaultUpdate(onboarded: boolean = true): UpdateItem { return { isTerminalState: false, latestStatus: onboarded ? DEFAULT_EXTENSION_STATUS : NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS, - lastUpdateDateTime: moment().utc().toISOString(), - itemId: "" + lastUpdateDateTime: moment().utc().toISOString() } } @@ -56,7 +76,7 @@ function updateMedicationRequest(medicationRequest: MedicationRequest, updateIte const relevantExtension = medicationRequest.extension?.find((ext) => ext.url === EXTENSION_URL) const statusCoding = relevantExtension?.extension?.find((innerExt) => innerExt.url === "status")?.valueCoding?.code - if (statusCoding && (statusCoding === "Prescriber Approved" || statusCoding === "Prescriber Cancelled")) { + if (statusCoding && (statusCoding === APPROVED_STATUS || statusCoding === CANCELLED_STATUS)) { logger.info( `Status update for prescription ${updateItem.itemId} has been skipped because the current status is already ` + `${statusCoding}.` @@ -146,10 +166,8 @@ export function applyStatusUpdates(searchsetBundle: Bundle, statusUpdates: Statu // set status as Prescriber Approved const update: UpdateItem = { isTerminalState: false, - itemId: "", - //Placeholder now datetime lastUpdateDateTime: moment().utc().toISOString(), - latestStatus: "Prescriber Approved" + latestStatus: APPROVED_STATUS } updateMedicationRequest(medicationRequest, update) return @@ -215,3 +233,44 @@ function getStatus(statusExtension: Extension): string | undefined { .map((coding) => coding?.code) .pop() } + +export enum UpdatesScenario { + Present, + ExpectedButAbsent, + NotExpected +} + +export function getUpdatesScenario(statusUpdates: StatusUpdates | undefined): UpdatesScenario { + if (expectStatusUpdates() && statusUpdates) { + return statusUpdates.isSuccess ? UpdatesScenario.Present : UpdatesScenario.ExpectedButAbsent + } else if (expectStatusUpdates() && !statusUpdates) { + return UpdatesScenario.ExpectedButAbsent + } + return UpdatesScenario.NotExpected +} + +export function applyTemporaryStatusUpdates(searchsetBundle: Bundle, statusUpdateRequest: StatusUpdateRequest) { + const update: UpdateItem = { + isTerminalState: false, + lastUpdateDateTime: moment().utc().toISOString(), + latestStatus: TEMPORARILY_UNAVAILABLE_STATUS + } + isolatePrescriptions(searchsetBundle).forEach((prescription) => { + const medicationRequests = isolateMedicationRequests(prescription) + const prescriptionID = medicationRequests![0].groupIdentifier!.value!.toUpperCase() + + const performerReference = isolatePerformerReference(medicationRequests!) + if (performerReference) { + const performer = isolatePerformerOrganisation(performerReference, prescription) + const odsCode = performer.identifier![0].value!.toUpperCase() + + const updates = statusUpdateRequest.prescriptions.filter( + (data) => data.prescriptionID.toUpperCase() === prescriptionID && data.odsCode.toUpperCase() === odsCode + ) + if (updates.length > 0) { + logger.info(`Updates expected for medication requests in prescription ${prescriptionID}. Applying temporary.`) + medicationRequests?.forEach((medicationRequest) => updateMedicationRequest(medicationRequest, update)) + } + } + }) +} diff --git a/packages/enrichPrescriptions/tests/testHandler.test.ts b/packages/enrichPrescriptions/tests/testHandler.test.ts index ef480ec6e..4fa589582 100644 --- a/packages/enrichPrescriptions/tests/testHandler.test.ts +++ b/packages/enrichPrescriptions/tests/testHandler.test.ts @@ -9,6 +9,7 @@ import { import { SYSTEM_DATETIME, defaultExtension, + getStatusUpdatesFailedEventAndResponse, noUpdateDataEventAndResponse, richEventAndResponse, simpleEventAndResponse @@ -19,6 +20,7 @@ import {lambdaHandler} from "../src/enrichPrescriptions" describe("Unit tests for handler", function () { beforeEach(() => { jest.useFakeTimers().setSystemTime(SYSTEM_DATETIME) + process.env.EXPECT_STATUS_UPDATES = "true" }) it("when event contains a bundle with one prescription, one MedicationRequest and status updates, updates are applied", async () => { @@ -51,10 +53,26 @@ describe("Unit tests for handler", function () { expect(actualResponse).toEqual(expectedResponse) }) - it("when no status update data (call to GetStatusUpdates toggled-off), no updates are applied", async () => { + it("when no status update data (GetStatusUpdates toggled-off), no updates are applied", async () => { + process.env.EXPECT_STATUS_UPDATES = "false" const {event, expectedResponse} = noUpdateDataEventAndResponse() const actualResponse = await lambdaHandler(event) expect(actualResponse).toEqual(expectedResponse) }) + + it("when status updates are expected but unsuccessful (GetStatusUpdates fails at code level), temporary updates are applied", async () => { + const {event, expectedResponse} = getStatusUpdatesFailedEventAndResponse() + const actualResponse = await lambdaHandler(event) + + expect(actualResponse).toEqual(expectedResponse) + }) + + it("when status updates are expected but not present (GetStatusUpdates fails at state machine level), temporary updates are applied", async () => { + const {event, expectedResponse} = getStatusUpdatesFailedEventAndResponse() + delete event.StatusUpdates + const actualResponse = await lambdaHandler(event) + + expect(actualResponse).toEqual(expectedResponse) + }) }) diff --git a/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts b/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts index 839e27f36..9a4e150a9 100644 --- a/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts +++ b/packages/enrichPrescriptions/tests/testStatusUpdate.test.ts @@ -17,17 +17,27 @@ import { simpleStatusUpdatesPayload, addExtensionToMedicationRequest, getStatusExtensions, - simpleUpdateWithStatus + simpleUpdateWithStatus, + OUTER_EXTENSION_URL, + createStatusUpdateRequest } from "./utils" import { + APPROVED_STATUS, + CANCELLED_STATUS, + NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS, ONE_WEEK_IN_MS, StatusUpdates, + TEMPORARILY_UNAVAILABLE_STATUS, + UpdatesScenario, applyStatusUpdates, + applyTemporaryStatusUpdates, delayWithPharmacyStatus, - getStatusDate + getStatusDate, + getUpdatesScenario } from "../src/statusUpdates" import {Bundle, MedicationRequest} from "fhir/r4" import {Logger} from "@aws-lambda-powertools/logger" +import {isolateMedicationRequests, isolatePrescriptions} from "../src/fhirUtils" describe("Unit tests for statusUpdate", function () { beforeEach(() => { @@ -360,4 +370,131 @@ describe("Unit tests for statusUpdate", function () { expect(getStatusDate(incomplete_extension)).toEqual(undefined) }) }) + + describe("Temporary status updates", () => { + beforeEach(() => { + jest.useFakeTimers().setSystemTime(SYSTEM_DATETIME) + }) + + it("Item with no status, that expects an update, is given the temporary update and has its status set as active", async () => { + const requestBundle = simpleRequestBundle() + const prescriptions = isolatePrescriptions(requestBundle) + const medicationRequests = isolateMedicationRequests(prescriptions[0]) + const medicationRequest = medicationRequests![0] + + const prescriptionID = medicationRequest.groupIdentifier!.value!.toUpperCase() + const statusUpdateRequest = createStatusUpdateRequest([{odsCode: "FLM49", prescriptionID: prescriptionID}]) + + applyTemporaryStatusUpdates(requestBundle, statusUpdateRequest) + const statusExtension = medicationRequest.extension![0].extension!.filter((e) => e.url === "status")[0] + + expect(statusExtension.valueCoding!.code!).toEqual(TEMPORARILY_UNAVAILABLE_STATUS) + expect(medicationRequest.status).toEqual("active") + }) + + it("No temporary update if ods code or prescription ID doesn't match", async () => { + const requestBundle = simpleRequestBundle() + const prescriptions = isolatePrescriptions(requestBundle) + const medicationRequests = isolateMedicationRequests(prescriptions[0]) + const medicationRequest = medicationRequests![0] + + const prescriptionID = medicationRequest.groupIdentifier!.value!.toUpperCase() + const statusUpdateRequest = createStatusUpdateRequest([ + {odsCode: "NOPE", prescriptionID: prescriptionID}, + {odsCode: "FLM49", prescriptionID: "NOPE"} + ]) + + applyTemporaryStatusUpdates(requestBundle, statusUpdateRequest) + expect(medicationRequest.extension).toBeUndefined() + }) + + it.each([ + {status: "Prescriber Approved", shouldUpdate: false}, + {status: "Prescriber Cancelled", shouldUpdate: false}, + {status: "With Pharmacy but Tracking not Supported", shouldUpdate: true} + ])( + "Item with existing status, that expects an update, is given the temporary update when its existing status is appropriate", + async ({status, shouldUpdate}) => { + const requestBundle = simpleRequestBundle() + const prescriptions = isolatePrescriptions(requestBundle) + const medicationRequests = isolateMedicationRequests(prescriptions[0]) + const medicationRequest = medicationRequests![0] + + const updateTime = new Date().toISOString() + addExtensionToMedicationRequest(medicationRequest, status, updateTime) + + const prescriptionID = medicationRequests![0].groupIdentifier!.value!.toUpperCase() + const statusUpdateRequest = createStatusUpdateRequest([{odsCode: "FLM49", prescriptionID: prescriptionID}]) + + applyTemporaryStatusUpdates(requestBundle, statusUpdateRequest) + const statusExtension = medicationRequest.extension![0].extension!.filter((e) => e.url === "status")[0]! + + expect(statusExtension.valueCoding!.code!).toEqual(shouldUpdate ? TEMPORARILY_UNAVAILABLE_STATUS : status) + } + ) + }) + + it("Prescriptions with multiple items, that expect updates, have temporary updates applied to appropriate items", async () => { + // The richRequestBundle gives us three prescriptions with a total of six items + const requestBundle = richRequestBundle() + const prescriptions = isolatePrescriptions(requestBundle) + + // We modify one prescription here. One other will get the temporary update without being modified, + // and the remaining one will not get the temporary update + const prescriptionToBeModified = prescriptions[0] + + const medicationRequests = isolateMedicationRequests(prescriptionToBeModified) + const updateTime = new Date().toISOString() + + // These two items will be updated with the temporary update + addExtensionToMedicationRequest(medicationRequests![0], NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS, updateTime) + addExtensionToMedicationRequest(medicationRequests![1], NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS, updateTime) + + // These two items will not + addExtensionToMedicationRequest(medicationRequests![2], APPROVED_STATUS, updateTime) + addExtensionToMedicationRequest(medicationRequests![3], CANCELLED_STATUS, updateTime) + + // These represent the modified prescription and the one that will update without being modified + const statusUpdateRequest = createStatusUpdateRequest([ + {odsCode: "FLM49", prescriptionID: "24F5DA-A83008-7EFE6Z"}, + {odsCode: "FEW08", prescriptionID: "16B2E0-A83008-81C13H"} + ]) + + applyTemporaryStatusUpdates(requestBundle, statusUpdateRequest) + + const tempStatusUpdateFilter = (medicationRequest: MedicationRequest) => { + const outerExtension = medicationRequest.extension?.filter( + (extension) => extension.url === OUTER_EXTENSION_URL + )[0] + const statusExtension = outerExtension?.extension?.filter((extension) => extension.url === "status")[0] + return statusExtension?.valueCoding!.code === TEMPORARILY_UNAVAILABLE_STATUS + } + + // Checking just the items from the modified prescription + const medicationRequestsWithTemporaryUpdates = medicationRequests!.filter(tempStatusUpdateFilter) + expect(medicationRequests!.length).toEqual(4) + expect(medicationRequestsWithTemporaryUpdates.length).toEqual(2) + + // Checking all items, which will include the single item from the unmodified prescription that we expect to get the temporary update + const allMedicationRequests = prescriptions.flatMap((prescription) => + isolateMedicationRequests(prescription) + ) as Array + const allMedicationRequestsWithTemporaryUpdates = allMedicationRequests.filter(tempStatusUpdateFilter) + + expect(allMedicationRequests.length).toEqual(6) + expect(allMedicationRequestsWithTemporaryUpdates.length).toEqual(3) + }) + + it.each([ + {expectUpdates: true, updatesPresent: true, expected: UpdatesScenario.Present}, + {expectUpdates: true, updatesPresent: false, expected: UpdatesScenario.ExpectedButAbsent}, + {expectUpdates: false, updatesPresent: false, expected: UpdatesScenario.NotExpected} + ])("getUpdatesScenario returns as expected", async ({expectUpdates, updatesPresent, expected}) => { + process.env.EXPECT_STATUS_UPDATES = expectUpdates ? "true" : "false" + const statusUpdates = updatesPresent ? {isSuccess: true, prescriptions: [], schemaVersion: 1} : undefined + + const scenario = getUpdatesScenario(statusUpdates) + + expect(scenario).toEqual(expected) + }) }) diff --git a/packages/enrichPrescriptions/tests/utils.ts b/packages/enrichPrescriptions/tests/utils.ts index c8d10c715..76c611191 100644 --- a/packages/enrichPrescriptions/tests/utils.ts +++ b/packages/enrichPrescriptions/tests/utils.ts @@ -10,7 +10,10 @@ import { DEFAULT_EXTENSION_STATUS, EXTENSION_URL, NOT_ONBOARDED_DEFAULT_EXTENSION_STATUS, + Prescription, + StatusUpdateRequest, StatusUpdates, + TEMPORARILY_UNAVAILABLE_STATUS, VALUE_CODING_SYSTEM } from "../src/statusUpdates" @@ -38,6 +41,9 @@ export const richRequestBundle = () => JSON.parse(richRequestString) as Bundle export const richStatusUpdatesPayload = () => JSON.parse(richStatusUpdatesString) as StatusUpdates export const richResponseBundle = () => JSON.parse(richResponseString) as Bundle +export const OUTER_EXTENSION_URL = "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory" +export const INNER_EXTENSION_URL = "https://fhir.nhs.uk/CodeSystem/task-businessStatus-nppt" + type RequestAndResponse = { event: EnrichPrescriptionsEvent expectedResponse: APIGatewayProxyResult @@ -56,11 +62,13 @@ const TRACE_IDS: TraceIDs = { function eventAndResponse( requestBundle: Bundle, responseBundle: Bundle, - statusUpdates?: StatusUpdates + statusUpdates?: StatusUpdates, + statusUpdateRequest?: StatusUpdateRequest ): RequestAndResponse { const requestAndResponse: RequestAndResponse = { event: { fhir: requestBundle, + statusUpdateData: {schemaVersion: 1, prescriptions: []}, traceIDs: TRACE_IDS }, expectedResponse: { @@ -72,6 +80,9 @@ function eventAndResponse( if (statusUpdates) { requestAndResponse.event.StatusUpdates = {Payload: statusUpdates} } + if (statusUpdateRequest) { + requestAndResponse.event.statusUpdateData = statusUpdateRequest + } return requestAndResponse } @@ -94,6 +105,24 @@ export function noUpdateDataEventAndResponse(): RequestAndResponse { return eventAndResponse(simpleRequestBundle(), simpleRequestBundle()) } +export function getStatusUpdatesFailedEventAndResponse(): RequestAndResponse { + const requestBundle = simpleRequestBundle() + + const responseBundle = simpleResponseBundle() + const collectionBundle = responseBundle.entry![0].resource as Bundle + const medicationRequest = collectionBundle.entry![0].resource as MedicationRequest + medicationRequest.extension![0].extension![0].valueCoding!.code = TEMPORARILY_UNAVAILABLE_STATUS + + const statusUpdatesPayload = simpleStatusUpdatesPayload() + statusUpdatesPayload.isSuccess = false + + const statusUpdateData = { + schemaVersion: 1, + prescriptions: [{odsCode: "FLM49", prescriptionID: "727066-A83008-2EFE36"}] + } + return eventAndResponse(requestBundle, responseBundle, statusUpdatesPayload, statusUpdateData) +} + export function defaultExtension(onboarded: boolean = true): Array { return [ { @@ -122,12 +151,12 @@ export function addExtensionToMedicationRequest( ) { medicationRequest.extension = [ { - url: "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory", + url: OUTER_EXTENSION_URL, extension: [ { url: "status", valueCoding: { - system: "https://fhir.nhs.uk/CodeSystem/task-businessStatus-nppt", + system: INNER_EXTENSION_URL, code: status } }, @@ -149,3 +178,10 @@ export function simpleUpdateWithStatus(status: string): StatusUpdates { update.prescriptions[0].items[0].latestStatus = status return update } + +export function createStatusUpdateRequest(prescriptions: Array): StatusUpdateRequest { + return { + schemaVersion: 1, + prescriptions: prescriptions + } +} diff --git a/packages/getMyPrescriptions/src/statusUpdate.ts b/packages/getMyPrescriptions/src/statusUpdate.ts index e58027f5e..c5b79ffd6 100644 --- a/packages/getMyPrescriptions/src/statusUpdate.ts +++ b/packages/getMyPrescriptions/src/statusUpdate.ts @@ -10,18 +10,18 @@ import {logger} from "./getMyPrescriptions" export const EXTENSION_URL = "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory" export const shouldGetStatusUpdates = () => process.env.GET_STATUS_UPDATES === "true" -export type StatusUpdateData = {odsCode: string, prescriptionID: string} +export type StatusUpdateData = {odsCode: string; prescriptionID: string} export function buildStatusUpdateData(searchsetBundle: Bundle): Array { const statusUpdateData: Array = [] - isolatePrescriptions(searchsetBundle).forEach(prescription => { + isolatePrescriptions(searchsetBundle).forEach((prescription) => { const medicationRequests = isolateMedicationRequests(prescription) const hasApprovedOrCancelledStatus = (medicationRequest: MedicationRequest) => { - const relevantExtension = medicationRequest.extension?.find(ext => ext.url === EXTENSION_URL) - const statusExtension = relevantExtension?.extension?.find(innerExt => innerExt.url === "status") + const relevantExtension = medicationRequest.extension?.find((ext) => ext.url === EXTENSION_URL) + const statusExtension = relevantExtension?.extension?.find((innerExt) => innerExt.url === "status") const valueCodingCode = statusExtension?.valueCoding?.code - return valueCodingCode === "Prescriber Approved" || valueCodingCode === "Cancelled" + return valueCodingCode === "Prescriber Approved" || valueCodingCode === "Prescriber Cancelled" } const allItemsApprovedOrCancelled = medicationRequests.every(hasApprovedOrCancelledStatus) @@ -30,7 +30,7 @@ export function buildStatusUpdateData(searchsetBundle: Bundle): Array { expect(result).toEqual([]) }) - test("excludes prescriptions where all items have a status of either 'Prescriber Approved' or 'Cancelled'", async () => { - const bundle = mockInteractionResponseBody as Bundle - - const collectionBundle = bundle.entry![2].resource as Bundle - const medicationRequest = collectionBundle.entry![0].resource as MedicationRequest - medicationRequest.extension = [ - { - url: "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory", - extension: [ - { - url: "status", - valueCoding: {code: "Prescriber Approved"} - }, - { - url: "statusDate", - valueDateTime: new Date().toISOString() - } - ] - } - ] + test.each([ + {status: "Prescriber Cancelled", expectedLength: 1}, + {status: "Prescriber Approved", expectedLength: 1}, + {status: "Other", expectedLength: 2} + ])( + "excludes prescriptions where all items have a status of either 'Prescriber Approved' or 'Prescriber Cancelled'", + async ({status, expectedLength}) => { + const bundle = mockInteractionResponseBody as Bundle + + const collectionBundle = bundle.entry![2].resource as Bundle + const medicationRequest = collectionBundle.entry![0].resource as MedicationRequest + medicationRequest.extension = [ + { + url: "https://fhir.nhs.uk/StructureDefinition/Extension-DM-PrescriptionStatusHistory", + extension: [ + { + url: "status", + valueCoding: {code: status} + }, + { + url: "statusDate", + valueDateTime: new Date().toISOString() + } + ] + } + ] - const result = buildStatusUpdateData(bundle) + const result = buildStatusUpdateData(bundle) - // Expect result length to be 1, since only one prescription should be processed for status updates - expect(result.length).toBe(1) - }) + expect(result.length).toBe(expectedLength) + } + ) }) describe("Unit tests for statusUpdate, via handler", function () { @@ -119,8 +125,12 @@ describe("Unit tests for statusUpdate, via handler", function () { it("when event is processed, statusUpdateData is included in the response", async () => { const event: GetMyPrescriptionsEvent = JSON.parse(exampleEvent) - mock.onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "flm49"}}).reply(200, JSON.parse(pharmacy2uResponse)) - mock.onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "few08"}}).reply(200, JSON.parse(pharmicaResponse)) + mock + .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "flm49"}}) + .reply(200, JSON.parse(pharmacy2uResponse)) + mock + .onGet("https://service-search/service-search", {params: {...SERVICE_SEARCH_PARAMS, search: "few08"}}) + .reply(200, JSON.parse(pharmicaResponse)) mock.onGet("https://spine/mm/patientfacingprescriptions").reply(200, JSON.parse(exampleInteractionResponse))