Skip to content

Commit

Permalink
Update: [AEA-3987] - return details from fhir validator on validation…
Browse files Browse the repository at this point in the history
… failure (#118)

## Summary

- Routine Change

### Details

- in step function return details from FHIR validator on validation
failure
- in step function add catch step to return 500 error if there was a
problem
- handle missing resource in input in update prescription status lambda
  • Loading branch information
anthony-nhs authored Apr 17, 2024
1 parent 8666618 commit 0da44b9
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"Comment": "Update Prescription Status State Machine",
"StartAt": "FHIR Validation",
"StartAt": "Call FHIR Validation",
"States": {
"FHIR Validation": {
"Call FHIR Validation": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
Expand All @@ -12,34 +12,74 @@
"ResultPath": "$.FhirValidationResult",
"ResultSelector": {
"Response.$": "$.Payload",
"NoFailed.$": "States.ArrayLength($.Payload.issue[?(@.severity ==error)])"
"NumberFailedValidation.$": "States.ArrayLength($.Payload.issue[?(@.severity ==error)])"
},
"Next": "FHIR Validation Result",
"InputPath": "$.body"
"Next": "Do FHIR Validation Errors Exist",
"InputPath": "$.body",
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Next": "CatchAllError"
}
]
},
"FHIR Validation Result": {
"Do FHIR Validation Errors Exist": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.FhirValidationResult.NoFailed",
"Variable": "$.FhirValidationResult.NumberFailedValidation",
"NumericGreaterThan": 0,
"Next": "Failed FHIR Validation"
"Next": "Return Failed FHIR Validation Errors"
}
],
"Default": "Update Prescription Status"
"Default": "Call Update Prescription Status"
},
"Failed FHIR Validation": {
"Return Failed FHIR Validation Errors": {
"Type": "Pass",
"End": true,
"InputPath": "$.FhirValidationResult.Response"
"InputPath": "$.FhirValidationResult.Response",
"Parameters": {
"Payload": {
"statusCode": 400,
"headers": {
"Content-Type": "application/fhir+json",
"Cache-Control": "no-cache"
},
"body.$": "States.JsonToString($)"
}
}
},
"Update Prescription Status": {
"Call Update Prescription Status": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"Payload.$": "$",
"FunctionName": "${UpdatePrescriptionStatusFunctionArn}"
},
"End": true,
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Next": "CatchAllError"
}
]
},
"CatchAllError": {
"Type": "Pass",
"Result": {
"Payload": {
"statusCode": 500,
"headers": {
"Content-Type": "application/fhir+json",
"Cache-Control": "no-cache"
},
"body": "{\"resourceType\":\"OperationOutcome\",\"issue\":[{\"severity\":\"error\",\"code\":\"processing\",\"diagnostics\":\"System error\"}]}"
}
},
"End": true
}
}
Expand Down
22 changes: 11 additions & 11 deletions SAMtemplates/state_machines/main.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
AWSTemplateFormatVersion: '2010-09-09'
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: |
PSU state machines and related resources
Expand All @@ -7,18 +7,18 @@ Parameters:
StackName:
Type: String
Default: none

UpdatePrescriptionStatusFunctionName:
Type: String
Default: none

UpdatePrescriptionStatusFunctionArn:
Type: String
Default: none

LogRetentionInDays:
Type: Number

EnableSplunk:
Type: String

Expand All @@ -32,7 +32,7 @@ Resources:
DefinitionUri: UpdatePrescriptionStatusStateMachine.asl.json
DefinitionSubstitutions:
FhirValidationFunctionArn: !Join
- ":"
- ":"
- - !ImportValue fhir-validator:FHIRValidatorUKCoreLambdaArn
- snap
UpdatePrescriptionStatusFunctionArn: !Sub ${UpdatePrescriptionStatusFunctionArn}:$LATEST
Expand All @@ -44,7 +44,7 @@ Resources:
Level: ALL
Tracing:
Enabled: true

UpdatePrescriptionStatusStateMachineResources:
Type: AWS::Serverless::Application
Properties:
Expand All @@ -54,20 +54,20 @@ Resources:
StateMachineName: !Sub ${StackName}-UpdatePrescriptionStatus
StateMachineArn: !Sub arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${StackName}-UpdatePrescriptionStatus
AdditionalPolicies: !Join
- ','
- ","
- - Fn::ImportValue: !Sub ${StackName}:functions:${UpdatePrescriptionStatusFunctionName}:ExecuteLambdaPolicyArn
- !ImportValue fhir-validator:FHIRValidatorUKCoreExecuteLambdaPolicyArn
LogRetentionInDays: !Ref LogRetentionInDays
CloudWatchKMSKeyId: !ImportValue account-resources:CloudwatchLogsKmsKeyArn
EnableSplunk: !Ref EnableSplunk
SplunkSubscriptionFilterRole: !ImportValue lambda-resources:SplunkSubscriptionFilterRole
SplunkDeliveryStreamArn: !ImportValue lambda-resources:SplunkDeliveryStream

Outputs:
UpdatePrescriptionStatusStateMachineArn:
Description: UpdatePrescriptionStatus state machine arn
Value: !Ref UpdatePrescriptionStatusStateMachine

UpdatePrescriptionStatusStateMachineName:
Description: UpdatePrescriptionStatus state machine name
Value: !GetAtt UpdatePrescriptionStatusStateMachine.Name
Value: !GetAtt UpdatePrescriptionStatusStateMachine.Name
25 changes: 13 additions & 12 deletions packages/updatePrescriptionStatus/src/validation/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import {validatePrescriptionID} from "../utils/prescriptionID"
import {validateNhsNumber} from "../utils/nhsNumber"
import {validateFields} from "./fields"

export type Validation = (task: Task) => string | undefined
export type TaskValidation = (task: Task) => string | undefined
export type BundleEntryValidation = (bundleEntry: BundleEntry) => string | undefined

export type ValidationOutcome = {
valid: boolean,
valid: boolean
issues: string | undefined
}

Expand Down Expand Up @@ -82,12 +83,12 @@ export function resourceType(task: Task): string | undefined {
}

export function codeSystems(task: Task): string | undefined {
const systems: Array<Validation> = [
(t: Task) => t.focus!.identifier!.system === LINE_ITEM_ID_CODESYSTEM ? undefined : "LineItemID",
(t: Task) => t.for!.identifier!.system === NHS_NUMBER_CODESYSTEM ? undefined : "PatientNHSNumber",
(t: Task) => t.owner!.identifier!.system === ODS_CODE_CODESYSTEM ? undefined : "PharmacyODSCode",
(t: Task) => t.basedOn![0].identifier!.system === PRESCRIPTION_ID_CODESYSTEM ? undefined : "PrescriptionID",
(t: Task) => t.businessStatus!.coding![0].system === STATUS_CODESYSTEM ? undefined : "Status"
const systems: Array<TaskValidation> = [
(t: Task) => (t.focus!.identifier!.system === LINE_ITEM_ID_CODESYSTEM ? undefined : "LineItemID"),
(t: Task) => (t.for!.identifier!.system === NHS_NUMBER_CODESYSTEM ? undefined : "PatientNHSNumber"),
(t: Task) => (t.owner!.identifier!.system === ODS_CODE_CODESYSTEM ? undefined : "PharmacyODSCode"),
(t: Task) => (t.basedOn![0].identifier!.system === PRESCRIPTION_ID_CODESYSTEM ? undefined : "PrescriptionID"),
(t: Task) => (t.businessStatus!.coding![0].system === STATUS_CODESYSTEM ? undefined : "Status")
]
const incorrectCodeSystems: Array<string> = []
for (const system of systems) {
Expand Down Expand Up @@ -123,7 +124,7 @@ export function statuses(task: Task): string | undefined {
}

export function taskContent(task: Task): Array<string> {
const contentValidations: Array<Validation> = [
const contentValidations: Array<TaskValidation> = [
businessStatus,
lastModified,
nhsNumber,
Expand All @@ -134,7 +135,7 @@ export function taskContent(task: Task): Array<string> {
]

const issues: Array<string> = []
contentValidations.forEach((validation: Validation) => {
contentValidations.forEach((validation: TaskValidation) => {
const issue = validation(task)
if (issue) {
issues.push(issue)
Expand All @@ -149,8 +150,8 @@ export function validateContent(entry: BundleEntry): ValidationOutcome {
const issues: Array<string> = []
const task = entry.resource as Task

entryContent(entry).forEach(f => issues.push(f))
taskContent(task).forEach(f => issues.push(f))
entryContent(entry).forEach((f) => issues.push(f))
taskContent(task).forEach((f) => issues.push(f))
if (issues.length > 0) {
validationOutcome.valid = false
validationOutcome.issues = issues.join(" ")
Expand Down
40 changes: 27 additions & 13 deletions packages/updatePrescriptionStatus/src/validation/fields.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import {BundleEntry, Task} from "fhir/r4"
import {Validation, ValidationOutcome} from "./content"
import {TaskValidation, ValidationOutcome, BundleEntryValidation} from "./content"

export function entryFields(entry: BundleEntry): Array<string> {
return entry.fullUrl ? [] : ["FullUrl"]
const requiredFields: Array<BundleEntryValidation> = [
(t: BundleEntry) => (t.fullUrl ? undefined : "FullUrl"),
(t: BundleEntry) => (t.resource ? undefined : "Resource")
]
const missingFields: Array<string> = []
for (const field of requiredFields) {
const missingField = field(entry)
if (missingField) {
missingFields.push(missingField)
}
}
return missingFields
}

export function taskFields(task: Task): Array<string> {
const requiredFields: Array<Validation> = [
(t: Task) => t.lastModified ? undefined : "LastModified",
(t: Task) => t.focus?.identifier?.value ? undefined : "LineItemID",
(t: Task) => t.for?.identifier?.value ? undefined : "PatientNHSNumber",
(t: Task) => t.owner?.identifier?.value ? undefined : "PharmacyODSCode",
(t: Task) => t.basedOn?.[0].identifier?.value ? undefined : "PrescriptionID",
(t: Task) => t.businessStatus?.coding?.[0].code ? undefined : "Status",
(t: Task) => t.id ? undefined : "TaskID",
(t: Task) => t.status ? undefined : "TerminalStatus"
const requiredFields: Array<TaskValidation> = [
(t: Task) => (t.lastModified ? undefined : "LastModified"),
(t: Task) => (t.focus?.identifier?.value ? undefined : "LineItemID"),
(t: Task) => (t.for?.identifier?.value ? undefined : "PatientNHSNumber"),
(t: Task) => (t.owner?.identifier?.value ? undefined : "PharmacyODSCode"),
(t: Task) => (t.basedOn?.[0].identifier?.value ? undefined : "PrescriptionID"),
(t: Task) => (t.businessStatus?.coding?.[0].code ? undefined : "Status"),
(t: Task) => (t.id ? undefined : "TaskID"),
(t: Task) => (t.status ? undefined : "TerminalStatus")
]
const missingFields: Array<string> = []
for (const field of requiredFields) {
Expand All @@ -31,8 +42,11 @@ export function validateFields(entry: BundleEntry): ValidationOutcome {
const missingFields: Array<string> = []
const task = entry.resource as Task

entryFields(entry).forEach(f => missingFields.push(f))
taskFields(task).forEach(f => missingFields.push(f))
entryFields(entry).forEach((f) => missingFields.push(f))
if (task !== undefined) {
taskFields(task).forEach((f) => missingFields.push(f))
}

if (missingFields.length > 0) {
validationOutcome.valid = false
validationOutcome.issues = `Missing required field(s) - ${missingFields.join(", ")}.`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,46 @@ describe("Unit tests for validateFields", () => {
const result = validateFields(entry)
expect(result).toEqual({valid: false, issues: "Missing required field(s) - FullUrl, PrescriptionID, TaskID."})
})

it("when fields missing on entry, return invalid with message", async () => {
const task = validTask()
const entry: BundleEntry = {fullUrl: FULL_URL_0, resource: task}
delete entry.resource

const result = validateFields(entry)
expect(result).toEqual({valid: false, issues: "Missing required field(s) - Resource."})
})
})

describe("Unit tests for validation of individual fields", () => {
it.each([
{
missingField: "LastModified",
operation: ((t: Task) => delete t.lastModified)
operation: (t: Task) => delete t.lastModified
},
{
missingField: "LineItemID",
operation: ((t: Task) => delete t.focus)
operation: (t: Task) => delete t.focus
},
{
missingField: "PatientNHSNumber",
operation: ((t: Task) => delete t.for)
operation: (t: Task) => delete t.for
},
{
missingField: "PharmacyODSCode",
operation: ((t: Task) => delete t.owner)
operation: (t: Task) => delete t.owner
},
{
missingField: "PrescriptionID",
operation: ((t: Task) => delete t.basedOn)
operation: (t: Task) => delete t.basedOn
},
{
missingField: "Status",
operation: ((t: Task) => delete t.businessStatus)
operation: (t: Task) => delete t.businessStatus
},
{
missingField: "TaskID",
operation: ((t: Task) => delete t.id)
operation: (t: Task) => delete t.id
}
])("When $missingField is missing, should return expected issue.", async ({operation, missingField}) => {
const task = validTask()
Expand Down

0 comments on commit 0da44b9

Please sign in to comment.