diff --git a/docs/status-transitions.md b/docs/status-transitions.md index d8388b6..810c8e2 100644 --- a/docs/status-transitions.md +++ b/docs/status-transitions.md @@ -6,29 +6,77 @@ - `order-received` - `order-accepted` - `dispatched` +- `cancelled` - `received-at-lab` - `test-processed` - `complete` ## Allowed Transitions -```text -order-received -> order-accepted -> dispatched -> received-at-lab -> test-processed -> complete +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 controlled within HomeTest. + +```mermaid +--- +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: No order created + receivedAtLab: received-at-lab + testProcessed: test-processed + + [*] --> orderEligibilityCheck + orderEligibilityCheck --> eligibility_check + eligibility_check --> orderReceived:::homeTestSent : if Eligible + eligibility_check --> orderRejected:::homeTestSent : if Not Eligible + orderReceived --> orderAccepted:::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`, `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. + +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 + +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 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 diff --git a/examples/fhir/order_cancelled.example.json b/examples/fhir/order_cancelled.example.json new file mode 100644 index 0000000..1a07866 --- /dev/null +++ b/examples/fhir/order_cancelled.example.json @@ -0,0 +1,31 @@ +{ + "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": { + "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_dispatched.example.json b/examples/fhir/task_update_dispatched.example.json index ecacf39..276c6b9 100644 --- a/examples/fhir/task_update_dispatched.example.json +++ b/examples/fhir/task_update_dispatched.example.json @@ -24,5 +24,8 @@ "lastModified": "2025-11-04T10:35:00Z", "businessStatus": { "text": "dispatched" + }, + "statusReason" : { + "text": "Test kit dispatched to patient" } } diff --git a/examples/fhir/task_update_received-at-lab.example.json b/examples/fhir/task_update_received-at-lab.example.json new file mode 100644 index 0000000..f3e559f --- /dev/null +++ b/examples/fhir/task_update_received-at-lab.example.json @@ -0,0 +1,27 @@ +{ + "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": "in-progress", + "intent": "order", + "for": { + "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" + }, + "lastModified": "2025-11-04T10:35:00Z", + "businessStatus": { + "text": "received-at-lab" + }, + "statusReason" : { + "text": " Test kit has been received at the lab and is being processed" + } +} diff --git a/examples/fhir/task_update_test-processed.example.json b/examples/fhir/task_update_test-processed.example.json new file mode 100644 index 0000000..28a8df8 --- /dev/null +++ b/examples/fhir/task_update_test-processed.example.json @@ -0,0 +1,27 @@ +{ + "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": "in-progress", + "intent": "order", + "for": { + "reference": "Patient/123e4567-e89b-12d3-a456-426614174000" + }, + "lastModified": "2025-11-04T10:35:00Z", + "businessStatus": { + "text": "test-processed" + }, + "statusReason" : { + "text": " Test kit has been processed" + } +} diff --git a/schemas/changelog.md b/schemas/changelog.md index 5b90dda..d360854 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 of the ServiceRequest + - 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 + - Remove mentions of order rejection + - 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 ddecb63..d9f6457 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 @@ -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, in-progress] example: "in-progress" + businessStatus: + allOf: + - $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" 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,13 +418,6 @@ 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 description: FHIR OperationOutcome resource for reporting errors and warnings @@ -470,7 +475,6 @@ components: items: type: string example: ["Observation.status"] - FHIRReference: type: object description: A reference from one resource to another (FHIR Reference datatype) @@ -621,6 +625,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..345dd3e 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: @@ -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: @@ -316,7 +382,6 @@ components: - code - subject - requester - - contained properties: resourceType: type: string @@ -329,12 +394,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: @@ -366,7 +431,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) @@ -393,7 +458,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' @@ -800,17 +865,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) @@ -825,7 +890,7 @@ components: value: type: string description: The actual contact point details - example: "+447700900123" + example: "+447700900000" use: type: string description: Purpose of this contact point @@ -853,7 +918,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