From 9810d931ea6a9c9159394a36ddfdf780add4d5db Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Mon, 8 Jun 2026 11:56:02 +0100 Subject: [PATCH 01/18] Add order rejected and cancelled states, add Mermaid diagram --- docs/status-transitions.md | 50 ++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/status-transitions.md b/docs/status-transitions.md index d8388b6..9ba2414 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -5,30 +5,70 @@ - `order-received` - `order-accepted` +- `order-rejected` - `dispatched` +- `cancelled` - `received-at-lab` - `test-processed` - `complete` ## Allowed Transitions +The following state machine shows the allowed transitions for HomeTest orders. -```text -order-received -> order-accepted -> dispatched -> received-at-lab -> test-processed -> complete +The states in green are states that are controlled by the suppliers - i.e. the entry to that state comes from an update from the supplier. + +The states in blue are states that are contolled within HomeTest. + +```mermaid +--- +title: Order State Machine +--- +stateDiagram-v2 + + classDef supplierSent fill:#00A499, color:#fff + classDef homeTestSent fill:#dceefb + + orderReceived: order-received + orderAccepted: order-accepted + orderRejected: order-rejected + receivedAtLab: received-at-lab + testProcessed: test-processed + + [*] --> orderReceived :::homeTestSent + orderReceived --> orderAccepted:::supplierSent + orderReceived --> orderRejected:::supplierSent + orderRejected --> [*] + orderAccepted --> dispatched:::supplierSent + dispatched --> receivedAtLab:::supplierSent + dispatched --> cancelled:::homeTestSent + cancelled --> [*] + receivedAtLab --> testProcessed:::supplierSent + testProcessed --> complete:::homeTestSent + complete --> [*] ``` ## Order Creation and Completion New orders are only created within the HomeTest platform. + Orders can only be marked as 'complete' by the HomeTest platform, usually on receipt of a test result update from the test supplier. -This means that while `order-received` and `complete` are valid status, they are reserved for use within the HomeTest platform itself. -Only the status of `order-accepted`, `dispatched`, `received-at-lab` and `test processed` should be sent by test suppliers. + +This means that while `order-received`, `cancelled` and `complete` are valid order statuses, they shouldn't be sent as order updates by suppliers. Only the status of `order-accepted`, `order-rejected`, `dispatched`, `received-at-lab` and `test processed` should be sent by test suppliers (marked in green on the diagram above) + +## Order Acceptance and Rejection +Orders are accepted or rejected by the suppliers asychronously. This means that orders that are at `order-received` are submitted to the suppliers, and HomeTest then waits for an update from the test supplier, expecting either `order-accepted` or `order-rejected`. + +Once an order has been accepted by a supplier, the order must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. + +## Order Cancellation +Users can cancel an order only when it is in the `dispatched` state. ## Rules 1. **Monotonic progression**: transitions **MUST** move forward only. 2. **Idempotent updates**: re-sending the same status is allowed and **MUST NOT** error. 3. **No skips**: skipping intermediate states is **SHOULD NOT**. If a supplier cannot emit all states, they **MUST** document and obtain approval. -4. **Terminal**: `complete` is terminal; no further transitions allowed. +4. **Terminal**: `order-rejected`, `cancelled` and `complete` are terminal states; no further transitions allowed. No updates to results using `POST /result` are permitted in these states. ## Error Semantics From 0a966125f7894f6884828a0f041e8386fac74078 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Mon, 8 Jun 2026 12:20:29 +0100 Subject: [PATCH 02/18] First draft of order accepted, rejected and cancelled FHIR structures. Needed to make 'contained' no longer required, as we don't have enough information to fully populate the patient at that point, and can only use a reference --- .../fhir/order_cancelled.example copy.json | 27 ++++++++++ .../task_update_order_accepted.example.json | 24 +++++++++ .../task_update_order_rejected.example.json | 24 +++++++++ schemas/home-test-supplier-api.yaml | 49 +++++++++++++++---- schemas/supplier-api-spec.yaml | 5 +- 5 files changed, 117 insertions(+), 12 deletions(-) create mode 100644 examples/fhir/order_cancelled.example copy.json create mode 100644 examples/fhir/task_update_order_accepted.example.json create mode 100644 examples/fhir/task_update_order_rejected.example.json diff --git a/examples/fhir/order_cancelled.example copy.json b/examples/fhir/order_cancelled.example copy.json new file mode 100644 index 0000000..255a149 --- /dev/null +++ b/examples/fhir/order_cancelled.example copy.json @@ -0,0 +1,27 @@ +{ + "resourceType": "ServiceRequest", + "id": "7cb0623e-9cd7-4495-aa66-715c04a81903", + "status": "revoked", + "intent": "order", + "code": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "31676001", + "display": "HIV antigen test" + } + ], + "text": "HIV antigen test" + }, + "subject": { + "reference": "Patient/1d6efc98-78e7-4049-9d4f-e651a95d9727" + }, + "requester": { + "reference": "Organization/ORG001" + }, + "performer": [ + { + "reference": "Organization/SUP001" + } + ] +} diff --git a/examples/fhir/task_update_order_accepted.example.json b/examples/fhir/task_update_order_accepted.example.json new file mode 100644 index 0000000..7f8ec2f --- /dev/null +++ b/examples/fhir/task_update_order_accepted.example.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Task", + "identifier": [ + { + "system": "https://fhir.hometest.nhs.uk/Id/order-uid", + "value": "550e8400-e29b-41d4-a716-446655440000", + "use": "official" + } + ], + "basedOn": [ + { + "reference": "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" + } + ], + "status": "accepted", + "intent": "order", + "for": { + "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" + }, + "lastModified": "2025-11-04T10:35:00Z", + "businessStatus": { + "text": "order-accepted" + } +} diff --git a/examples/fhir/task_update_order_rejected.example.json b/examples/fhir/task_update_order_rejected.example.json new file mode 100644 index 0000000..7f8ec2f --- /dev/null +++ b/examples/fhir/task_update_order_rejected.example.json @@ -0,0 +1,24 @@ +{ + "resourceType": "Task", + "identifier": [ + { + "system": "https://fhir.hometest.nhs.uk/Id/order-uid", + "value": "550e8400-e29b-41d4-a716-446655440000", + "use": "official" + } + ], + "basedOn": [ + { + "reference": "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" + } + ], + "status": "accepted", + "intent": "order", + "for": { + "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" + }, + "lastModified": "2025-11-04T10:35:00Z", + "businessStatus": { + "text": "order-accepted" + } +} diff --git a/schemas/home-test-supplier-api.yaml b/schemas/home-test-supplier-api.yaml index ddecb63..ea2656a 100644 --- a/schemas/home-test-supplier-api.yaml +++ b/schemas/home-test-supplier-api.yaml @@ -71,6 +71,9 @@ paths: application/fhir+json: schema: $ref: '#/components/schemas/FHIRTask' + examples: + FHIRTaskExample: + $ref: '#/components/examples/FHIRTaskExample' responses: '200': description: Status updated successfully @@ -78,6 +81,9 @@ paths: application/fhir+json: schema: $ref: '#/components/schemas/FHIRTask' + examples: + FHIRTaskExample: + $ref: '#/components/examples/FHIRTaskExample' '400': $ref: '#/components/responses/BadRequest' '404': @@ -365,19 +371,25 @@ components: status: type: string description: Current status of the task - FHIR standard values (use businessStatus for domain-specific states) - enum: [draft, requested, received, accepted, rejected, ready, cancelled, in-progress, on-hold, failed, completed, entered-in-error] + enum: [accepted, rejected,in-progress] example: "in-progress" + businessStatus: + allOf: + - $ref: '#/components/schemas/FHIRCodeableConcept' + - description: Domain-specific business status. If the FHIRTask status is "accepted", should be "order-accepted". If FHIRTask status is "rejected", should be "order-rejected". If FHIRTask status is "in-progress", then "dispatched","received-at-lab", and "test-processed" are all valid. + example: + text: "dispatched" intent: type: string description: Indicates the "level" of actionability associated with the Task - enum: [unknown, proposal, plan, order, original-order, reflex-order, filler-order, instance-order, option] + enum: [order] example: "order" statusReason: allOf: - $ref: '#/components/schemas/FHIRCodeableConcept' - description: Reason for current status example: - text: "Sample collected and being processed" + text: "Test kit dispatched to patient" for: allOf: - $ref: '#/components/schemas/FHIRReference' @@ -406,12 +418,7 @@ components: - description: Responsible individual for the task example: reference: "Organization/SUP001" - businessStatus: - allOf: - - $ref: '#/components/schemas/FHIRCodeableConcept' - - description: Domain-specific business status (should be one of: "order-accepted", "dispatched", "received-at-lab", "test-processed") - example: - text: "dispatched" + FHIROperationOutcome: type: object @@ -621,6 +628,30 @@ components: status: "not-done" reasonReference: - reference: "urn:uuid:550e8400-e29b-41d4-a716-446655440001" + FHIRTaskExample: + summary: Example FHIR Task for a 'dispatched' update, see examples folder for further examples. + value: + resourceType: "Task" + id: "task-550e8400-e29b-41d4-a716-446655440000" + identifier: + - system: "https://fhir.hometest.nhs.uk/Id/order-uid" + value: "550e8400-e29b-41d4-a716-446655440000" + basedOn: + - reference: "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" + status: "in-progress" + businessStatus: + text: "dispatched" + intent: "order" + for: + reference: "Patient/123e4567-e89b-12d3-a456-426614174000" + authoredOn: "2025-11-04T10:30:00Z" + lastModified: "2025-11-04T10:35:00Z" + requester: + reference: "Organization/ORG001" + display: "HomeTest" + owner: + reference: "Organization/SUP001" + display: "Test Supplier Ltd" responses: BadRequest: description: Bad request - invalid parameters diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 5d09a3b..38fae98 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -316,7 +316,6 @@ components: - code - subject - requester - - contained properties: resourceType: type: string @@ -329,12 +328,12 @@ components: status: type: string description: Status of the service request - enum: [draft, active, on-hold, revoked, completed, entered-in-error, unknown] + enum: [draft, active, revoked] example: "active" intent: type: string description: Intent of the service request - enum: [proposal, plan, directive, order, original-order, reflex-order, filler-order, instance-order, option] + enum: [ order] example: "order" code: allOf: From 9bbe90ff751d4497bc42a6b08694945a30b7bf33 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Mon, 8 Jun 2026 14:20:11 +0100 Subject: [PATCH 03/18] Adding statusReasons, and using full UUIDs in examples. Some other minor changes to align the examples within the spec to the JSON examples. --- examples/fhir/task_update_dispatched.example.json | 3 ++- .../fhir/task_update_order_accepted.example.json | 3 ++- .../fhir/task_update_order_rejected.example.json | 7 ++++--- schemas/supplier-api-spec.yaml | 14 +++++++------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/examples/fhir/task_update_dispatched.example.json b/examples/fhir/task_update_dispatched.example.json index ecacf39..cf6d87e 100644 --- a/examples/fhir/task_update_dispatched.example.json +++ b/examples/fhir/task_update_dispatched.example.json @@ -24,5 +24,6 @@ "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { "text": "dispatched" - } + }, + "statusReason" : " Test kit dispatched to patient" } diff --git a/examples/fhir/task_update_order_accepted.example.json b/examples/fhir/task_update_order_accepted.example.json index 7f8ec2f..d36b798 100644 --- a/examples/fhir/task_update_order_accepted.example.json +++ b/examples/fhir/task_update_order_accepted.example.json @@ -20,5 +20,6 @@ "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { "text": "order-accepted" - } + }, + "statusReason" : " Supplier has accepted the order, and the kit will be dispatched" } diff --git a/examples/fhir/task_update_order_rejected.example.json b/examples/fhir/task_update_order_rejected.example.json index 7f8ec2f..d99c41f 100644 --- a/examples/fhir/task_update_order_rejected.example.json +++ b/examples/fhir/task_update_order_rejected.example.json @@ -12,13 +12,14 @@ "reference": "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" } ], - "status": "accepted", + "status": "rejected", "intent": "order", "for": { "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" }, "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { - "text": "order-accepted" - } + "text": "order-rejected" + }, + "statusReason" : " Supplier has rejected the order because the address appears on a blocklist. The kit will not be dispatched" } diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 38fae98..2126734 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -365,7 +365,7 @@ components: id: type: string description: Local identifier for contained resource - example: "patient-1" + example: "1d6efc98-78e7-4049-9d4f-e651a95d9727" name: type: array description: Patient name(s) @@ -392,7 +392,7 @@ components: - $ref: '#/components/schemas/FHIRReference' - description: Reference to Patient - use contained resource reference example: - reference: "#patient-1" + reference: "#1d6efc98-78e7-4049-9d4f-e651a95d9727" requester: allOf: - $ref: '#/components/schemas/FHIRReference' @@ -799,17 +799,17 @@ components: family: type: string description: Family/last name - example: "Smith" + example: "Doe" given: type: array description: Given/first names items: type: string - example: ["John"] + example: ["Alex"] text: type: string description: Full name as a single string - example: "John Smith" + example: "Alex Doe" FHIRContactPoint: type: object description: Details for all kinds of technology-mediated contact points (FHIR ContactPoint datatype) @@ -824,7 +824,7 @@ components: value: type: string description: The actual contact point details - example: "+447700900123" + example: "+447700900000" use: type: string description: Purpose of this contact point @@ -852,7 +852,7 @@ components: description: Street address lines items: type: string - example: ["123 Main Street", "Apartment 2B"] + example: ["123 Main Street", "Flat 4B"] city: type: string description: City name From 2813c1d88d489b19b4fce765daf8a3876e13f051 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Mon, 22 Jun 2026 16:42:27 +0100 Subject: [PATCH 04/18] Remove order rejection Generally removing references to order rejection, and clarifying that the failure of the order eligibility check is the only supported way for the suppliers to reject orders based on eligibility. --- docs/status-transitions.md | 22 ++++++++++------- .../fhir/order_cancelled.example copy.json | 4 ++++ schemas/changelog.md | 17 ++++++++++++- schemas/home-test-supplier-api.yaml | 24 +++++++++++++------ schemas/supplier-api-spec.yaml | 4 ++-- 5 files changed, 52 insertions(+), 19 deletions(-) diff --git a/docs/status-transitions.md b/docs/status-transitions.md index 9ba2414..9e76ee2 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -5,7 +5,6 @@ - `order-received` - `order-accepted` -- `order-rejected` - `dispatched` - `cancelled` - `received-at-lab` @@ -24,19 +23,22 @@ The states in blue are states that are contolled within HomeTest. title: Order State Machine --- stateDiagram-v2 - classDef supplierSent fill:#00A499, color:#fff classDef homeTestSent fill:#dceefb + state eligibility_check <> + orderEligibilityCheck: Supplier Order Eligibility Check orderReceived: order-received orderAccepted: order-accepted - orderRejected: order-rejected + orderRejected: No order created receivedAtLab: received-at-lab testProcessed: test-processed - [*] --> orderReceived :::homeTestSent + [*] --> orderEligibilityCheck + orderEligibilityCheck --> eligibility_check + eligibility_check --> orderReceived:::homeTestSent : if Eligible + eligibility_check --> orderRejected:::homeTestSent : if Not Eligible orderReceived --> orderAccepted:::supplierSent - orderReceived --> orderRejected:::supplierSent orderRejected --> [*] orderAccepted --> dispatched:::supplierSent dispatched --> receivedAtLab:::supplierSent @@ -48,20 +50,22 @@ stateDiagram-v2 ``` ## Order Creation and Completion - New orders are only created within the HomeTest platform. Orders can only be marked as 'complete' by the HomeTest platform, usually on receipt of a test result update from the test supplier. -This means that while `order-received`, `cancelled` and `complete` are valid order statuses, they shouldn't be sent as order updates by suppliers. Only the status of `order-accepted`, `order-rejected`, `dispatched`, `received-at-lab` and `test processed` should be sent by test suppliers (marked in green on the diagram above) +This means that while `order-received`, `cancelled` and `complete` are valid order statuses, they shouldn't be sent as order updates by suppliers. Only the status of `order-accepted`, `dispatched`, `received-at-lab` and `test-processed` should be sent by test suppliers (marked in green on the diagram above) ## Order Acceptance and Rejection -Orders are accepted or rejected by the suppliers asychronously. This means that orders that are at `order-received` are submitted to the suppliers, and HomeTest then waits for an update from the test supplier, expecting either `order-accepted` or `order-rejected`. +Before an order is formally created, a 'draft' order is sent to the supplier. This known as the 'Supplier Order Eligibility Check', and is the moment where a supplier decides whether to accept to reject the order. + +If an order is rejected in the supplier eligibility check, a HomeTest order is not created, and the user is directed to other avenues. For example, this is a direction to the user's closest sexual health clinic for HIV tests. -Once an order has been accepted by a supplier, the order must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. +If the order is accepted through the supplier eligibility check, the order must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. In other words, a test kit MUST be dispatched if the eligibility check has passed successfully. ## Order Cancellation Users can cancel an order only when it is in the `dispatched` state. +Further updates to a 'cancelled' order are rejected by HomeTest, and an error is raised. Results for cancelled orders not currently handled by the HomeTest API, and are also rejected if they're received for a cancelled order. ## Rules diff --git a/examples/fhir/order_cancelled.example copy.json b/examples/fhir/order_cancelled.example copy.json index 255a149..1a07866 100644 --- a/examples/fhir/order_cancelled.example copy.json +++ b/examples/fhir/order_cancelled.example copy.json @@ -1,6 +1,10 @@ { "resourceType": "ServiceRequest", "id": "7cb0623e-9cd7-4495-aa66-715c04a81903", + "text": { + "status": "generated", + "div": "
ServiceRequest: HIV antigen test order cancelled by user
" + }, "status": "revoked", "intent": "order", "code": { diff --git a/schemas/changelog.md b/schemas/changelog.md index 5b90dda..01a0353 100644 --- a/schemas/changelog.md +++ b/schemas/changelog.md @@ -22,7 +22,8 @@ All notable changes to the NHS Home Test Supplier Integration Framework API sche - [Version 1.1.2 - May 18, 2026 - Additional DataAbsent Result reason](#version-112---may-18-2026---additional-dataabsent-result-reason) - [Version 1.1.3 - June 1, 2026 - Change handling of non-definitive results](#version-113---june-1-2026---change-handling-of-non-definitive-results) - [Version 1.1.4 - June 10, 2026 - Resolve OpenAPI spec Spectral validation warnings](#version-114---june-10-2026---resolve-openapi-spec-spectral-validation-warnings) - - [Version 1.1.5 - June 15, 2026 - FHIR Example File Compliance Fixes](#version-115---june-15-2026---fhir-example-file-compliance-fixes) + - [Version 1.1.5 - June 15, 2026 - FHIR Example File Compliance Fixes\*\*](#version-115---june-15-2026---fhir-example-file-compliance-fixes) + - [Version 1.1.6 - June 22, 2026 - Add order cancellation\*\*](#version-116---june-22-2026---add-order-cancellation) --- @@ -360,3 +361,17 @@ Changes to schemas/fhir-schemas/: - Changed `link.self` URL from `/results?order_uid=...` to `Bundle?identifier=...` (resource-type-qualified URL required for FHIR type checking) - Added `search.mode: match` to the entry (required for searchset bundles) - Added `text` narrative to the inner Observation resource + +--- + +## Version 1.1.6 - June 22, 2026 - Add order cancellation** + +1. Add order cancellation process + - Allow 'revoked' as a status through the /receiveTestOrder API. + - Add documentation for rejection of further updates to cancelled orders + +2. Clarify the order eligibility check and other order states + - Remove mentions of order rejection + - Add enum for allowed 'businessStatus' in FHIRTask + - Add diagram for order states + - Add documentation for order cancellation, and order acceptance (via eligibility check) diff --git a/schemas/home-test-supplier-api.yaml b/schemas/home-test-supplier-api.yaml index ea2656a..85d1ab1 100644 --- a/schemas/home-test-supplier-api.yaml +++ b/schemas/home-test-supplier-api.yaml @@ -3,7 +3,7 @@ openapi: 3.0.3 info: title: Home Test Supplier API description: API for supplier domain operations - managing test results and order status updates - version: 1.1.4 + version: 1.1.6 contact: name: NHS England - Digital Prevention Services Portfolio - Home Test Team email: england.hometest@nhs.net @@ -371,12 +371,12 @@ components: status: type: string description: Current status of the task - FHIR standard values (use businessStatus for domain-specific states) - enum: [accepted, rejected,in-progress] + enum: [accepted, in-progress] example: "in-progress" businessStatus: allOf: - - $ref: '#/components/schemas/FHIRCodeableConcept' - - description: Domain-specific business status. If the FHIRTask status is "accepted", should be "order-accepted". If FHIRTask status is "rejected", should be "order-rejected". If FHIRTask status is "in-progress", then "dispatched","received-at-lab", and "test-processed" are all valid. + - $ref: '#/components/schemas/FHIRBusinessStatus' + - description: Domain-specific business status, expected to be one of "dispatched","received-at-lab", or "test-processed" for HomeTest. example: text: "dispatched" intent: @@ -418,8 +418,6 @@ components: - description: Responsible individual for the task example: reference: "Organization/SUP001" - - FHIROperationOutcome: type: object description: FHIR OperationOutcome resource for reporting errors and warnings @@ -477,7 +475,6 @@ components: items: type: string example: ["Observation.status"] - FHIRReference: type: object description: A reference from one resource to another (FHIR Reference datatype) @@ -556,6 +553,19 @@ components: description: The purpose of this identifier enum: [usual, official, temp, secondary, old] example: "official" + FHIRBusinessStatus: + description: A specific example of a CodeableConcept for HomeTest status updates, using the businessStatus field of the FHIRTask + allOf: + - $ref: '#/components/schemas/FHIRCodeableConcept' + - type: object + required: + - text + properties: + text: + type: string + enum: [dispatched,received-at-lab,test-processed] + example: + text: dispatched examples: FHIRBundleResultsExample: summary: Example FHIR Bundle for test results submission diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 2126734..0eabf2e 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -3,7 +3,7 @@ openapi: 3.0.3 info: title: Supplier API description: API for medical test suppliers to provide in order to allow Home Test platform integration - version: 1.1.4 + version: 1.1.6 contact: name: NHS England - Digital Prevention Services Portfolio - Home Test Team email: england.hometest@nhs.net @@ -101,7 +101,7 @@ paths: post: summary: Receive Test Order operationId: receiveTestOrder - description: Receive a new test order from the home test platform + description: Receive a new test order from the home test platform. This endpoint is also used to cancel orders, by sending an order with a status of 'revoked'. tags: - Order Management parameters: From 5f05e40a5fbfd119b02d793e5366e3b1c260a46a Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Tue, 23 Jun 2026 09:08:48 +0100 Subject: [PATCH 05/18] Syntax errors / spacing in YAML files. --- schemas/home-test-supplier-api.yaml | 2 +- schemas/supplier-api-spec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/schemas/home-test-supplier-api.yaml b/schemas/home-test-supplier-api.yaml index 85d1ab1..d7fe42a 100644 --- a/schemas/home-test-supplier-api.yaml +++ b/schemas/home-test-supplier-api.yaml @@ -563,7 +563,7 @@ components: properties: text: type: string - enum: [dispatched,received-at-lab,test-processed] + enum: [dispatched, received-at-lab, test-processed] example: text: dispatched examples: diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 0eabf2e..e428b7d 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -333,7 +333,7 @@ components: intent: type: string description: Intent of the service request - enum: [ order] + enum: [order] example: "order" code: allOf: From 717aa17f6c99a1c8dca34d448d4b7d44c821ea43 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Tue, 23 Jun 2026 09:18:42 +0100 Subject: [PATCH 06/18] Model statusReason as a codeable concept (text field) rather than just a raw string. This caused a FHIR validator error --- examples/fhir/task_update_dispatched.example.json | 4 +++- examples/fhir/task_update_order_accepted.example.json | 4 +++- examples/fhir/task_update_order_rejected.example.json | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/fhir/task_update_dispatched.example.json b/examples/fhir/task_update_dispatched.example.json index cf6d87e..276c6b9 100644 --- a/examples/fhir/task_update_dispatched.example.json +++ b/examples/fhir/task_update_dispatched.example.json @@ -25,5 +25,7 @@ "businessStatus": { "text": "dispatched" }, - "statusReason" : " Test kit dispatched to patient" + "statusReason" : { + "text": "Test kit dispatched to patient" + } } diff --git a/examples/fhir/task_update_order_accepted.example.json b/examples/fhir/task_update_order_accepted.example.json index d36b798..cb8431f 100644 --- a/examples/fhir/task_update_order_accepted.example.json +++ b/examples/fhir/task_update_order_accepted.example.json @@ -21,5 +21,7 @@ "businessStatus": { "text": "order-accepted" }, - "statusReason" : " Supplier has accepted the order, and the kit will be dispatched" + "statusReason" : { + "text": " Supplier has accepted the order, and the kit will be dispatched" + } } diff --git a/examples/fhir/task_update_order_rejected.example.json b/examples/fhir/task_update_order_rejected.example.json index d99c41f..9385bbe 100644 --- a/examples/fhir/task_update_order_rejected.example.json +++ b/examples/fhir/task_update_order_rejected.example.json @@ -21,5 +21,7 @@ "businessStatus": { "text": "order-rejected" }, - "statusReason" : " Supplier has rejected the order because the address appears on a blocklist. The kit will not be dispatched" + "statusReason" : { + "text": " Supplier has rejected the order because the address appears on a blocklist. The kit will not be dispatched" + } } From ca14d25cfb45657350f40c7b5bac9b3344c2b3d8 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Tue, 23 Jun 2026 09:34:13 +0100 Subject: [PATCH 07/18] Fix typos that error the pre-commit --- docs/status-transitions.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/status-transitions.md b/docs/status-transitions.md index 9e76ee2..6fa90f7 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -16,7 +16,7 @@ The following state machine shows the allowed transitions for HomeTest orders. The states in green are states that are controlled by the suppliers - i.e. the entry to that state comes from an update from the supplier. -The states in blue are states that are contolled within HomeTest. +The states in blue are states that are controlled within HomeTest. ```mermaid --- @@ -50,6 +50,7 @@ stateDiagram-v2 ``` ## Order Creation and Completion + New orders are only created within the HomeTest platform. Orders can only be marked as 'complete' by the HomeTest platform, usually on receipt of a test result update from the test supplier. @@ -57,6 +58,7 @@ Orders can only be marked as 'complete' by the HomeTest platform, usually on rec This means that while `order-received`, `cancelled` and `complete` are valid order statuses, they shouldn't be sent as order updates by suppliers. Only the status of `order-accepted`, `dispatched`, `received-at-lab` and `test-processed` should be sent by test suppliers (marked in green on the diagram above) ## Order Acceptance and Rejection + Before an order is formally created, a 'draft' order is sent to the supplier. This known as the 'Supplier Order Eligibility Check', and is the moment where a supplier decides whether to accept to reject the order. If an order is rejected in the supplier eligibility check, a HomeTest order is not created, and the user is directed to other avenues. For example, this is a direction to the user's closest sexual health clinic for HIV tests. @@ -64,6 +66,7 @@ If an order is rejected in the supplier eligibility check, a HomeTest order is n If the order is accepted through the supplier eligibility check, the order must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. In other words, a test kit MUST be dispatched if the eligibility check has passed successfully. ## Order Cancellation + Users can cancel an order only when it is in the `dispatched` state. Further updates to a 'cancelled' order are rejected by HomeTest, and an error is raised. Results for cancelled orders not currently handled by the HomeTest API, and are also rejected if they're received for a cancelled order. From 08a125be6e130d5c15554cd42ba33cc78009a251 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Tue, 23 Jun 2026 09:35:29 +0100 Subject: [PATCH 08/18] Another extra markdown empty line --- docs/status-transitions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/status-transitions.md b/docs/status-transitions.md index 6fa90f7..430c24a 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -12,6 +12,7 @@ - `complete` ## Allowed Transitions + The following state machine shows the allowed transitions for HomeTest orders. The states in green are states that are controlled by the suppliers - i.e. the entry to that state comes from an update from the supplier. From 224a17da682b13f43b192145c5ada5f4b20a7b3b Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 10:17:39 +0100 Subject: [PATCH 09/18] Remove bussinessStatus enum - this is being modified in another PR --- schemas/home-test-supplier-api.yaml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/schemas/home-test-supplier-api.yaml b/schemas/home-test-supplier-api.yaml index d7fe42a..d9f6457 100644 --- a/schemas/home-test-supplier-api.yaml +++ b/schemas/home-test-supplier-api.yaml @@ -375,7 +375,7 @@ components: example: "in-progress" businessStatus: allOf: - - $ref: '#/components/schemas/FHIRBusinessStatus' + - $ref: '#/components/schemas/FHIRCodeableConcept' - description: Domain-specific business status, expected to be one of "dispatched","received-at-lab", or "test-processed" for HomeTest. example: text: "dispatched" @@ -553,19 +553,6 @@ components: description: The purpose of this identifier enum: [usual, official, temp, secondary, old] example: "official" - FHIRBusinessStatus: - description: A specific example of a CodeableConcept for HomeTest status updates, using the businessStatus field of the FHIRTask - allOf: - - $ref: '#/components/schemas/FHIRCodeableConcept' - - type: object - required: - - text - properties: - text: - type: string - enum: [dispatched, received-at-lab, test-processed] - example: - text: dispatched examples: FHIRBundleResultsExample: summary: Example FHIR Bundle for test results submission From c82ed279f628d98cd47d655096391127867f826f Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 10:22:11 +0100 Subject: [PATCH 10/18] Removing of order accepted and rejected examples, replace with received at lab and test-processed --- ...xample.json => task_update_received-at-lab.example.json} | 6 +++--- ...example.json => task_update_test-processed.example.json} | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename examples/fhir/{task_update_order_rejected.example.json => task_update_received-at-lab.example.json} (74%) rename examples/fhir/{task_update_order_accepted.example.json => task_update_test-processed.example.json} (79%) diff --git a/examples/fhir/task_update_order_rejected.example.json b/examples/fhir/task_update_received-at-lab.example.json similarity index 74% rename from examples/fhir/task_update_order_rejected.example.json rename to examples/fhir/task_update_received-at-lab.example.json index 9385bbe..f3e559f 100644 --- a/examples/fhir/task_update_order_rejected.example.json +++ b/examples/fhir/task_update_received-at-lab.example.json @@ -12,16 +12,16 @@ "reference": "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" } ], - "status": "rejected", + "status": "in-progress", "intent": "order", "for": { "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" }, "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { - "text": "order-rejected" + "text": "received-at-lab" }, "statusReason" : { - "text": " Supplier has rejected the order because the address appears on a blocklist. The kit will not be dispatched" + "text": " Test kit has been received at the lab and is being processed" } } diff --git a/examples/fhir/task_update_order_accepted.example.json b/examples/fhir/task_update_test-processed.example.json similarity index 79% rename from examples/fhir/task_update_order_accepted.example.json rename to examples/fhir/task_update_test-processed.example.json index cb8431f..28a8df8 100644 --- a/examples/fhir/task_update_order_accepted.example.json +++ b/examples/fhir/task_update_test-processed.example.json @@ -12,16 +12,16 @@ "reference": "ServiceRequest/550e8400-e29b-41d4-a716-446655440000" } ], - "status": "accepted", + "status": "in-progress", "intent": "order", "for": { "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" }, "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { - "text": "order-accepted" + "text": "test-processed" }, "statusReason" : { - "text": " Supplier has accepted the order, and the kit will be dispatched" + "text": " Test kit has been processed" } } From 6f7e68ed4b089888133ef84d24f0038193354f33 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 10:24:11 +0100 Subject: [PATCH 11/18] Update changelog.md --- schemas/changelog.md | 1 - 1 file changed, 1 deletion(-) diff --git a/schemas/changelog.md b/schemas/changelog.md index 01a0353..462ea98 100644 --- a/schemas/changelog.md +++ b/schemas/changelog.md @@ -372,6 +372,5 @@ Changes to schemas/fhir-schemas/: 2. Clarify the order eligibility check and other order states - Remove mentions of order rejection - - Add enum for allowed 'businessStatus' in FHIRTask - Add diagram for order states - Add documentation for order cancellation, and order acceptance (via eligibility check) From 8d8635d633fcf239ce84a5407a3960bf9544e6b6 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 11:21:35 +0100 Subject: [PATCH 12/18] Move order cancellation to a different verb on the same endpoint Also added specific failure codes for some obvious possible errors (order not found, order already processed) --- schemas/supplier-api-spec.yaml | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index e428b7d..1c4d77b 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -197,6 +197,72 @@ paths: details: text: "Individual not eligible due to local authority rules" diagnostics: "Patient does not meet local authority eligibility criteria for this test" + delete: + summary: Cancel Test Order + operationId: cancelTestOrder + description: This endpoint is used to cancel orders, by sending an order with a status of 'revoked'. + tags: + - Order Management + parameters: + - name: X-Correlation-ID + in: header + required: true + description: Unique identifier for request tracking and idempotency + schema: + type: string + format: uuid + example: "550e8400-e29b-41d4-a716-446655440000" + requestBody: + required: true + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/FHIRServiceRequest' + responses: + '200': + description: Order cancellation received and processed successfully + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/FHIRServiceRequest' + '400': + $ref: '#/components/responses/BadRequest' + '422': + $ref: '#/components/responses/UnprocessableEntity' + '404': + description: Order not found + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/FHIROperationOutcome' + examples: + order_not_found: + summary: Order not found + value: + resourceType: "OperationOutcome" + issue: + - severity: "error" + code: "not-found" + details: + text: "Order not found" + diagnostics: "The requested order was not found" + '409': + description: Business rule conflict - order cancellation cannot be processed + content: + application/fhir+json: + schema: + $ref: '#/components/schemas/FHIROperationOutcome' + examples: + order_already_processed: + summary: Order already processed + value: + resourceType: "OperationOutcome" + issue: + - severity: "error" + code: "business-rule" + details: + text: "Order already processed" + diagnostics: "The requested order has already been processed, and cannot be cancelled" /results: get: From bc36317de3b730c519ac75b4ef91b83ab134b92e Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 11:26:31 +0100 Subject: [PATCH 13/18] Fix whitespace --- schemas/supplier-api-spec.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 1c4d77b..345dd3e 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -241,10 +241,10 @@ paths: value: resourceType: "OperationOutcome" issue: - - severity: "error" - code: "not-found" - details: - text: "Order not found" + - severity: "error" + code: "not-found" + details: + text: "Order not found" diagnostics: "The requested order was not found" '409': description: Business rule conflict - order cancellation cannot be processed @@ -258,10 +258,10 @@ paths: value: resourceType: "OperationOutcome" issue: - - severity: "error" - code: "business-rule" - details: - text: "Order already processed" + - severity: "error" + code: "business-rule" + details: + text: "Order already processed" diagnostics: "The requested order has already been processed, and cannot be cancelled" /results: From 383760bc2a9a9c8342d4a33e62832438513a78d3 Mon Sep 17 00:00:00 2001 From: Tom Rawlinson Date: Wed, 24 Jun 2026 11:28:17 +0100 Subject: [PATCH 14/18] Apply suggestion from @lewisbirks clarify wording of first order needing to go to supplier before being moved to dispatch Co-authored-by: Lewis Birks <22620804+lewisbirks@users.noreply.github.com> --- docs/status-transitions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/status-transitions.md b/docs/status-transitions.md index 430c24a..810c8e2 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -64,7 +64,7 @@ Before an order is formally created, a 'draft' order is sent to the supplier. Th If an order is rejected in the supplier eligibility check, a HomeTest order is not created, and the user is directed to other avenues. For example, this is a direction to the user's closest sexual health clinic for HIV tests. -If the order is accepted through the supplier eligibility check, the order must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. In other words, a test kit MUST be dispatched if the eligibility check has passed successfully. +If the order is accepted through the supplier eligibility check, the order is sent to the supplier and must then move through to `dispatched`, and it cannot be later cancelled by the test supplier. In other words, a test kit MUST be dispatched if the eligibility check has passed successfully. ## Order Cancellation From 2e14b5dbedd10831f1ba8e75d5275d28e5a14e01 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 11:29:23 +0100 Subject: [PATCH 15/18] Correct name of order cancelled file --- ...r_cancelled.example copy.json => order_cancelled.example.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename examples/fhir/{order_cancelled.example copy.json => order_cancelled.example.json} (100%) diff --git a/examples/fhir/order_cancelled.example copy.json b/examples/fhir/order_cancelled.example.json similarity index 100% rename from examples/fhir/order_cancelled.example copy.json rename to examples/fhir/order_cancelled.example.json From c4d47dd287912291cbd47f9adf6dc2557dc941cc Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Wed, 24 Jun 2026 11:38:24 +0100 Subject: [PATCH 16/18] Change to use 'PATCH' as the HTTP verb for cancelling --- schemas/changelog.md | 3 ++- schemas/supplier-api-spec.yaml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/schemas/changelog.md b/schemas/changelog.md index 462ea98..0273a21 100644 --- a/schemas/changelog.md +++ b/schemas/changelog.md @@ -367,7 +367,8 @@ Changes to schemas/fhir-schemas/: ## Version 1.1.6 - June 22, 2026 - Add order cancellation** 1. Add order cancellation process - - Allow 'revoked' as a status through the /receiveTestOrder API. + - Allow 'revoked' as a status of the ServiceRequest + - Use 'PATCH' verb on the /order endpoint when orders are being cancelled. This allows specific errors to be defined, and helps to separate cancellation from creating a new order. - Add documentation for rejection of further updates to cancelled orders 2. Clarify the order eligibility check and other order states diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index 345dd3e..fdadd2f 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -197,7 +197,7 @@ paths: details: text: "Individual not eligible due to local authority rules" diagnostics: "Patient does not meet local authority eligibility criteria for this test" - delete: + patch: summary: Cancel Test Order operationId: cancelTestOrder description: This endpoint is used to cancel orders, by sending an order with a status of 'revoked'. From 1db4b13a6aaac94ec3148880ae205744a5f1b6b0 Mon Sep 17 00:00:00 2001 From: trawlinson-kainos Date: Thu, 25 Jun 2026 14:04:48 +0100 Subject: [PATCH 17/18] Use 'delete' as the HTTP verb --- schemas/supplier-api-spec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/supplier-api-spec.yaml b/schemas/supplier-api-spec.yaml index fdadd2f..345dd3e 100644 --- a/schemas/supplier-api-spec.yaml +++ b/schemas/supplier-api-spec.yaml @@ -197,7 +197,7 @@ paths: details: text: "Individual not eligible due to local authority rules" diagnostics: "Patient does not meet local authority eligibility criteria for this test" - patch: + delete: summary: Cancel Test Order operationId: cancelTestOrder description: This endpoint is used to cancel orders, by sending an order with a status of 'revoked'. From 9251fee5c9ad34a1953bc1b4fd13bba4c55584bf Mon Sep 17 00:00:00 2001 From: Lewis Birks <22620804+lewisbirks@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:06:45 +0100 Subject: [PATCH 18/18] Update changelog --- schemas/changelog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/changelog.md b/schemas/changelog.md index 0273a21..d360854 100644 --- a/schemas/changelog.md +++ b/schemas/changelog.md @@ -368,7 +368,7 @@ Changes to schemas/fhir-schemas/: 1. Add order cancellation process - Allow 'revoked' as a status of the ServiceRequest - - Use 'PATCH' verb on the /order endpoint when orders are being cancelled. This allows specific errors to be defined, and helps to separate cancellation from creating a new order. + - Use 'DELETE' verb on the /order endpoint when orders are being cancelled. This allows specific errors to be defined, and helps to separate cancellation from creating a new order. - Add documentation for rejection of further updates to cancelled orders 2. Clarify the order eligibility check and other order states