Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: [AEA-4047] - Set statuses to temporarily unavailable if nppts fails #1024

Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
813b4cc
AEA-4047 Rough first draft.
originalphil Jun 24, 2024
b5c9ecd
AEA-4047 Further correction to Prescriber Cancelled status.
originalphil Jun 25, 2024
107b569
AEA-4047 Use canned data for Spine response.
originalphil Jun 25, 2024
75349da
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jun 25, 2024
a46f7c8
AEA-4047 Skip new test.
originalphil Jun 25, 2024
d365d01
AEA-4047 Use gsul from temp stack.
originalphil Jun 25, 2024
94fc0ab
AEA-4047 SAM template corrections.
originalphil Jun 25, 2024
f1af639
AEA-4047 Add status extension to canned data.
originalphil Jun 26, 2024
50cdccf
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jun 26, 2024
f0a8170
AEA-4047 Remove test re-introduced by merge.
originalphil Jun 26, 2024
1da0409
AEA-4047 Simple test cases.
originalphil Jun 26, 2024
90dcb19
AEA-4047 Complex test case.
originalphil Jun 26, 2024
4d9ae99
AEA-4047 Complex test case improved.
originalphil Jun 26, 2024
4f2d258
AEA-4047 Removed unnecessary test file.
originalphil Jun 26, 2024
c978d6a
AEA-4047 Test ods code and prescription id combo.
originalphil Jun 26, 2024
4afc9d2
AEA-4047 Working handler test.
originalphil Jun 27, 2024
d4cca56
AEA-4047 Testing tweaks.
originalphil Jun 27, 2024
5b85be2
AEA-4047 More testing tweaks.
originalphil Jun 27, 2024
76e083a
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jun 27, 2024
d60b539
AEA-4047 More complex canned data.
originalphil Jun 27, 2024
98a3992
AEA-4047 Improvements to Big Test and comments to make it more readable.
originalphil Jun 27, 2024
ad979bc
AEA-4047 Revert temporary changes and change StatusUpdateData to Stat…
originalphil Jun 28, 2024
60aab6e
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jun 28, 2024
5a2c774
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jul 8, 2024
6ba705e
AEA-4047 Stub-out getMyPrescriptions and point at failing gsu lambda.
originalphil Jul 8, 2024
5c1c223
AEA-4047 Introduce env var and switch in enrich prescriptions. Send c…
originalphil Jul 8, 2024
a243555
AEA-4047 Test additions for update scenarios.
originalphil Jul 9, 2024
902f268
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
originalphil Jul 9, 2024
5e624e6
AEA-4047 Revert stubbing and pointing at failing lambda.
originalphil Jul 9, 2024
39fdc7c
AEA-4047 Un-skipped test.
originalphil Jul 11, 2024
9573bfe
Merge branch 'main' into AEA-4047-set-statuses-to-temporarily-unavail…
MatthewPopat-NHS Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block should also be updated to point to "Enrich Prescriptions".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted.

"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