Skip to content

Commit

Permalink
New: [AEA-4047] - Set statuses to temporarily unavailable if nppts fa…
Browse files Browse the repository at this point in the history
…ils (#1024)

## Summary

- Routine Change

### Details

No longer use catch all error for nppts call. Always progress to enrich
prescriptions. Update items expecting status updates with temporary
message.

---------

Co-authored-by: MatthewPopat-NHS <[email protected]>
  • Loading branch information
originalphil and MatthewPopat-NHS authored Jul 11, 2024
1 parent c98bcf4 commit de52064
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 75 deletions.
3 changes: 1 addition & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .vscode/prescriptionsforpatients.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"nhsdlogin",
"nhslogin",
"NOSONAR",
"nppt",
"OIDC",
"onboarded",
"Organisation",
Expand Down
1 change: 1 addition & 0 deletions SAMtemplates/functions/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ Resources:
Environment:
Variables:
LOG_LEVEL: !Ref LogLevel
EXPECT_STATUS_UPDATES: !Ref ToggleGetStatusUpdates
Metadata:
BuildMethod: esbuild
BuildProperties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"FunctionName": "${GetStatusUpdatesFunctionArn}"
},
"InputPath": "$.statusUpdateData",
"Next": "Get Status Updates Result",
"Next": "Enrich Prescriptions",
"ResultSelector": {
"Payload.$": "$.Payload"
},
Expand All @@ -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",
Expand Down
33 changes: 26 additions & 7 deletions packages/enrichPrescriptions/src/enrichPrescriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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)
Expand Down
22 changes: 19 additions & 3 deletions packages/enrichPrescriptions/src/fhirUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
Bundle,
BundleEntry,
FhirResource,
MedicationRequest
MedicationRequest,
Organization
} from "fhir/r4"

export type Entry = BundleEntry<FhirResource>
Expand Down Expand Up @@ -32,8 +33,23 @@ export function isolatePrescriptions(searchsetBundle: Bundle): Array<Bundle> {
}

export function isolateMedicationRequests(prescription: Bundle): Array<MedicationRequest> | 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<MedicationRequest>): 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<Organization>(prescription, filter)[0]
}

function filterAndTypeBundleEntries<T>(bundle: Bundle, filter: (entry: Entry) => boolean): Array<T> {
Expand Down
79 changes: 69 additions & 10 deletions packages/enrichPrescriptions/src/statusUpdates.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand All @@ -30,12 +41,21 @@ export type StatusUpdates = {
schemaVersion: number
}

export type Prescription = {
odsCode: string
prescriptionID: string
}

export type StatusUpdateRequest = {
schemaVersion: number
prescriptions: Array<Prescription>
}

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()
}
}

Expand All @@ -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}.`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
}
})
}
20 changes: 19 additions & 1 deletion packages/enrichPrescriptions/tests/testHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {
SYSTEM_DATETIME,
defaultExtension,
getStatusUpdatesFailedEventAndResponse,
noUpdateDataEventAndResponse,
richEventAndResponse,
simpleEventAndResponse
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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)
})
})
Loading

0 comments on commit de52064

Please sign in to comment.