Interop Service — User Stories
Status: populated Owner: TBD Last updated: 2026-04-18 Companion: Service Template · EPICS · 03 platform-services
INTEROP-US-001 — Route FHIR Read Request to Owning Service
Epic: INTEROP-EPIC-01
Priority: Critical
Labels: fhir-gateway, routing
Story As an external EHR system, I want to retrieve any FHIR resource using the platform's single FHIR base URL, so that I do not need to know the internal service topology.
Acceptance Criteria
Given an authenticated client with a valid Keycloak JWT
When the client sends GET /fhir/R4/Observation/OBS_01JXXX
Then interop-service looks up the routing rule for Observation
And proxies the request to the owning service (laboratory-service or patient-chart-service)
And returns the FHIR Observation resource with HTTP 200
And the response includes a valid FHIR resource with correct resourceType
Given the target owning service is unreachable
When the client sends GET /fhir/R4/Observation/OBS_01JXXX
Then interop-service returns HTTP 503
And the body is a valid OperationOutcome with issue[0].severity = "error"
INTEROP-US-002 — Serve FHIR CapabilityStatement
Epic: INTEROP-EPIC-01
Priority: High
Labels: fhir-gateway, capability-statement
Story As an integration developer, I want to retrieve the platform's CapabilityStatement so that I know which FHIR resources and operations are supported before building my integration.
Acceptance Criteria
Given interop-service is running with a populated routing table
When a client sends GET /fhir/R4/metadata
Then interop-service returns HTTP 200 with Content-Type: application/fhir+json
And the body is a valid FHIR CapabilityStatement
And it lists all 14 resource types in routing table under rest.resource
And it lists $export, $lookup, $expand, $validate-code, $translate operations
And the response is served from Redis cache within 10ms if cached
Given the routing table changes (new rule added)
When the cache TTL (5 min) expires
Then the next GET /fhir/R4/metadata rebuilds and re-caches the CapabilityStatement
INTEROP-US-003 — Enforce ABAC on Patient-Linked FHIR Reads
Epic: INTEROP-EPIC-01
Priority: Critical
Labels: fhir-gateway, abac, security
Story As a security architect, I want patient-linked FHIR resource reads to be authorised by the access-policy-service, so that clinicians only access records they are permitted to view.
Acceptance Criteria
Given a clinician's JWT claims roles: ["doctor"] and facility: "FAC_01J"
When the clinician sends GET /fhir/R4/Observation?patient=PAT_01JXXX
Then interop-service calls access-policy-service with {subject: <userId>, resource: "Observation", patient: "PAT_01JXXX"}
And if access-policy-service returns permit, the request is proxied to the owning service
And if access-policy-service returns deny, HTTP 403 is returned with OperationOutcome
Given access-policy-service is unavailable
When any patient-linked FHIR read arrives
Then interop-service denies the request with HTTP 503 OperationOutcome
And logs abac_check_failures counter increment
And does NOT fall back to permit (fail-safe deny)
INTEROP-US-004 — Fan-Out FHIR Search Across Multiple Services
Epic: INTEROP-EPIC-01
Priority: High
Labels: fhir-gateway, fan-out, search
Story As an external health information system, I want to search for all Observations for a patient regardless of category, so that I get a unified result set without knowing which internal service owns each category.
Acceptance Criteria
Given routing rules map Observation/vital-signs to patient-chart-service and Observation/laboratory to laboratory-service
When a client sends GET /fhir/R4/Observation?patient=PAT_01J
Then interop-service fans out to both owning services in parallel
And merges responses into a single FHIR Bundle (type: searchset)
And all entries include the correct tenantId in meta.tag
And the total reflects the combined count
Given one of the fan-out targets returns 500
When interop-service merges results
Then the merged Bundle still includes results from the healthy service
And an OperationOutcome issue with severity=warning is appended to Bundle.entry
And HTTP 200 is returned (not 500)
INTEROP-US-005 — Receive Inbound ADT Message via MLLP
Epic: INTEROP-EPIC-02
Priority: Critical
Labels: hl7v2, mllp, adt
Story As an external HIS, I want to send ADT admit/discharge/transfer messages to the platform over MLLP, so that patient registration events are reflected in the Ghasi-eHealth platform in real time.
Acceptance Criteria
Given a registered inbound MLLP connector with valid TLS client certificate
When the HIS sends an ADT^A01 message for patient "Ahmad Karimi" with MRN "MRN-12345"
Then interop-service replies with ACK AA within 500ms
And stores the raw HL7 message in hl7_messages with status = "processed"
And publishes event hl7v2.adt.received to NATS subject interop.hl7v2.adt.received
And forwards FHIR Patient resource to registration-service via PUT /fhir/R4/Patient
Given the same ADT^A01 message is retransmitted (same MSH-10 control ID)
When interop-service receives the duplicate
Then it replies ACK AA (idempotent)
And does NOT create a duplicate NATS event or FHIR write
And hl7_messages row dedup_key matches; no new row inserted
INTEROP-US-006 — Handle Unparseable HL7 v2 Message Safely
Epic: INTEROP-EPIC-02
Priority: High
Labels: hl7v2, dlq, error-handling
Story As an integration administrator, I want unparseable HL7 v2 messages to be safely stored and alerted on, so that I can fix the mapping and replay them without data loss.
Acceptance Criteria
Given an inbound MLLP connector
When a sender transmits an HL7 message with an unknown segment structure that fails parsing
Then interop-service returns ACK AE to the sender
And stores the raw message bytes in hl7_messages with status = "dead_lettered"
And increments interop_hl7_dead_lettered_total counter
And publishes interop.message.dead_lettered event to NATS
Given a dead-lettered message in hl7_messages
When an integration admin calls POST /v1/interop/messages/{id}/reprocess after a mapping fix is deployed
Then interop-service re-parses the raw payload with the updated parser
And if parsing succeeds, transitions message status to "processed" and publishes downstream events
And if parsing still fails, status remains "dead_lettered" with updated error detail
INTEROP-US-007 — Deliver Outbound ORU via MLLP Connector
Epic: INTEROP-EPIC-03
Priority: High
Labels: hl7v2, mllp, outbound, lab-results
Story As an external reference laboratory system, I want to receive HL7 ORU R01 messages for released lab results, so that I can update my own records without polling the Ghasi-eHealth API.
Acceptance Criteria
Given a registered outbound MLLP connector pointing to an external lab system
And the connector subscribes to lab.result.released events
When laboratory-service publishes lab.result.released for result RES_01JXXX
Then interop-service maps the FHIR Observation + DiagnosticReport to an ORU^R01 message
And delivers it to the connector's host:port over TLS MLLP
And records delivery status in hl7_messages as "delivered"
And publishes hl7v2.oru.sent event to NATS
Given the MLLP destination is temporarily unavailable
When delivery fails
Then interop-service retries with exponential back-off: 30s, 5m, 30m
And if all 3 retries fail, dead-letters the message and fires connector.delivery_failure_rate alert
And logs each retry attempt with connector_id, message_id, and error
INTEROP-US-008 — Rotate MLLP Client Certificate Without Restart
Epic: INTEROP-EPIC-03
Priority: Medium
Labels: connector-registry, tls, cert-rotation
Story As an integration administrator, I want to update a connector's TLS client certificate without restarting the service, so that certificate rotations are zero-downtime operations.
Acceptance Criteria
Given an active outbound MLLP connector with an expiring certificate
When the admin sends PATCH /v1/interop/connectors/{id} with a new client_cert and client_key
Then the connector reloads its TLS context in-process (no service restart)
And subsequent MLLP deliveries use the new certificate
And the old certificate is removed from memory
And a connector.cert_rotated audit event is logged
Given a connector's certificate expires within 30 days
When the certificate expiry prober runs
Then interop-service emits a connector.cert_expiry_warning alert
And logs the connector_id and days_until_expiry
INTEROP-US-009 — Start FHIR Bulk Export Job
Epic: INTEROP-EPIC-04
Priority: High
Labels: fhir-bulk, export
Story As a national health data warehouse, I want to initiate a bulk FHIR export and retrieve all resources as NDJSON files, so that I can load national data for population analytics.
Acceptance Criteria
Given an authenticated system client with bulk-export scope
When the client sends POST /fhir/R4/$export with _type=Patient,Observation,DiagnosticReport
Then interop-service returns HTTP 202 Accepted
And the Content-Location header contains the polling URL /fhir/R4/$export/{jobId}
Given the export job is running
When the client polls GET /fhir/R4/$export/{jobId}
Then if in progress, HTTP 202 is returned with X-Progress header
And when complete, HTTP 200 is returned with output array listing NDJSON files
And each file URL is a pre-signed MinIO URL with 24h TTL
Given the export completes
When the 7-day retention period elapses
Then MinIO files are deleted by the cleanup job
And the job status is updated to "expired"
INTEROP-US-010 — Enforce AFG-Core Profile on External Writes
Epic: INTEROP-EPIC-05
Priority: High
Labels: afg-core, fhir-conformance, clinical-safety
Story As a clinical informatics lead, I want all externally sourced FHIR resources to be validated against the AFG-Core national profile before being stored, so that non-conformant data does not enter the platform.
Acceptance Criteria
Given FHIR_PROFILE_VALIDATION_ENABLED=true (default)
When an external system sends POST /fhir/R4/Patient with a resource missing the AFG-Core national-id extension
Then interop-service validates the resource against the AFG-Core StructureDefinition
And returns HTTP 422 Unprocessable Entity
And the body is a valid OperationOutcome listing each validation failure with location and message
And increments interop_profile_validation_failures{resource_type="Patient"} counter
Given validation mode is set to "warn" for a specific connector
When the same non-conformant Patient resource is submitted via that connector
Then interop-service logs the validation failure and increments the counter
And forwards the resource to registration-service (not blocked)
And the response to the sender is HTTP 201 Created
INTEROP-US-011 — Update AFG-Core Profile Version
Epic: INTEROP-EPIC-05
Priority: Medium
Labels: afg-core, admin, profile-management
Story As an integration architect, I want to update the pinned AFG-Core profile version when MoPH releases a new conformance package, so that validation stays current without a service deployment.
Acceptance Criteria
Given the platform is running AFG-Core v1.2.0
When an admin sends PATCH /v1/interop/connectors/{id} with afg_core_version = "1.3.0"
Then interop-service loads the new StructureDefinition package from the profile registry
And subsequent writes for that connector are validated against v1.3.0
And validation against v1.2.0 is retained for connectors not yet migrated
Given a profile version that does not exist in the registry
When an admin attempts to set afg_core_version = "9.9.9"
Then interop-service returns HTTP 422 with error detail "profile version not found"
And the connector's existing profile version is unchanged
INTEROP-US-012 — Register an Inbound MLLP Connector
Epic: INTEROP-EPIC-06
Priority: High
Labels: connector-registry, admin
Story As an integration administrator, I want to register a new inbound MLLP connector for a facility's HIS, so that the facility can start sending HL7 v2 messages to the platform.
Acceptance Criteria
Given an admin with role interop:admin
When the admin sends POST /v1/interop/connectors with direction=inbound, port=2575, facility_id=FAC_01J, tls_cert=<PEM>
Then interop-service creates the connector with status = "inactive"
And returns HTTP 201 with the connector record (auth_config redacted)
And validates the port is not already assigned to another active connector
Given the connector is inactive
When the admin sends PATCH /v1/interop/connectors/{id}/activate
Then interop-service starts the MLLP listener on the configured port
And sets connector status = "active"
And logs connector.activated event
Given port 2575 is already used by another active connector
When the admin attempts to activate the new connector on the same port
Then HTTP 409 Conflict is returned with detail "port already in use by connector CONN_01JXXX"
And the new connector remains inactive
INTEROP-US-013 — Monitor Connector Health and Delivery Metrics
Epic: INTEROP-EPIC-06
Priority: Medium
Labels: connector-registry, observability
Story As an integration administrator, I want to see delivery statistics and health status for each MLLP connector, so that I can identify and resolve integration issues quickly.
Acceptance Criteria
Given an active outbound MLLP connector
When the admin sends GET /v1/interop/connectors/{id}/metrics
Then the response includes: messages_sent_24h, messages_failed_24h, messages_dead_lettered_24h, last_successful_delivery_at, connector_status
Given the connector has not delivered any message in 60 minutes
When the health prober runs
Then connector_status is updated to "degraded"
And an alert is triggered if the connector is configured as critical
Given the delivery failure rate exceeds 5% in a 15-minute window
When Prometheus evaluates the alert rule
Then the interop_connector_delivery_failure_rate_high alert fires with connector_id label
And the on-call SRE receives a notification
INTEROP-US-014 — Import FHIR Resources in Bulk from External System
Epic: INTEROP-EPIC-04
Priority: High
Labels: fhir-bulk, import, onboarding
Story As an integration architect, I want to bulk-import historical FHIR resources from a legacy system during facility onboarding, so that the facility's existing patient history is available in the platform from day one.
Acceptance Criteria
Given an NDJSON file uploaded to the import staging area
When an admin sends POST /fhir/R4/$import with a manifest pointing to the file
Then interop-service validates each resource against AFG-Core profile
And routes valid resources to the correct owning service via the FHIR routing table
And records per-resource success/failure in the import job manifest
Given the import job completes with some validation failures
When the admin retrieves GET /fhir/R4/$import/{jobId}
Then the response lists: total_resources, succeeded, failed, validation_errors[]
And each failed resource includes its line number, resourceType, and OperationOutcome detail
And the successfully imported resources are visible in the platform
Given a resource fails AFG-Core validation during import
When the import processor encounters it
Then the resource is skipped (not forwarded to owning service)
And the failure is recorded in the error manifest with the specific constraint violated
INTEROP-US-015 — Proxy FHIR Terminology $lookup to Terminology Service
Epic: INTEROP-EPIC-08
Priority: Medium
Labels: terminology, fhir-proxy
Story As an external FHIR client, I want to look up a SNOMED or LOINC code via the FHIR base URL, so that I can resolve clinical codes without needing a separate connection to the terminology service.
Acceptance Criteria
Given terminology-service is available
When a client sends GET /fhir/R4/CodeSystem/$lookup?system=http://loinc.org&code=2345-7
Then interop-service proxies the request to terminology-service
And returns the terminology-service response (Parameters resource) with HTTP 200
And caches the response in Redis with 1h TTL
Given the same $lookup request is repeated within 1 hour
When interop-service receives the request
Then it serves the response from Redis cache without forwarding to terminology-service
And the response is returned within 20ms
Given terminology-service is unavailable
When a client sends a $lookup request
Then interop-service returns HTTP 503 with OperationOutcome
And does NOT cache the error response
INTEROP-US-016 — Proxy FHIR ValueSet $expand
Epic: INTEROP-EPIC-08
Priority: Medium
Labels: terminology, fhir-proxy
Story As a clinical application developer, I want to expand a ValueSet via the FHIR base URL so that my application can present valid code choices to clinicians without an internal service dependency.
Acceptance Criteria
Given a ValueSet with id "ghasi-observation-categories" is loaded in terminology-service
When a client sends GET /fhir/R4/ValueSet/ghasi-observation-categories/$expand
Then interop-service proxies the request to terminology-service
And returns the expanded ValueSet with HTTP 200
Given the client adds count=10&offset=20 query parameters
When the request is proxied
Then both parameters are forwarded to terminology-service unchanged
And the paged expansion is returned correctly
Given the ValueSet does not exist
When the request is proxied
Then terminology-service returns HTTP 404
And interop-service relays the 404 OperationOutcome to the client unchanged
INTEROP-US-017 — Deactivate MLLP Connector Gracefully
Epic: INTEROP-EPIC-06
Priority: Medium
Labels: connector-registry, lifecycle
Story As an integration administrator, I want to deactivate an MLLP connector without dropping in-flight messages, so that integration relationships can be ended cleanly.
Acceptance Criteria
Given an active outbound MLLP connector with messages in the outbox
When the admin sends PATCH /v1/interop/connectors/{id}/deactivate
Then interop-service stops accepting new outbox items for the connector
And waits for in-flight deliveries to complete (up to 30s drain timeout)
And sets connector status = "inactive"
And returns HTTP 200 with the updated connector record
Given the drain timeout is reached with messages still pending
When deactivation proceeds
Then remaining pending messages are marked "cancelled" in hl7_messages
And a connector.deactivated_with_pending_messages audit event is logged
And the admin is informed via the API response of how many messages were cancelled
INTEROP-US-018 — View HL7 Message Audit Log
Epic: INTEROP-EPIC-02 / INTEROP-EPIC-03
Priority: Medium
Labels: hl7v2, audit, compliance
Story As an integration administrator, I want to search the HL7 message log by connector, date range, and status, so that I can audit integration activity and investigate message delivery issues.
Acceptance Criteria
Given hl7_messages table contains messages across multiple connectors
When an admin sends GET /v1/interop/messages?connector_id=CONN_01J&status=dead_lettered&from=2026-04-01
Then the response returns a paginated list of matching messages
And each record includes: id, connector_id, message_type, status, received_at, error_detail
And the raw_payload field is NOT included in list responses (PII protection)
Given an admin needs to inspect a specific dead-lettered message
When the admin sends GET /v1/interop/messages/{id}
Then the full message record is returned including raw_payload (access requires interop:admin role)
And access is logged in the audit trail
Given a message has status = "dead_lettered"
When the admin sends POST /v1/interop/messages/{id}/reprocess
Then the message is re-submitted to the parsing pipeline
And the response includes the new processing status
INTEROP-US-019 — Configure EMR Segment Mapping Overrides
Epic: INTEROP-EPIC-07
Priority: Medium
Labels: emr-coexistence, adapter
Story As an integration engineer, I want to configure field-level mapping overrides for a specific EMR partner connector, so that non-standard HL7 v2 segment usage from that EMR is handled correctly without changing the default parser.
Acceptance Criteria
Given a connector for an OpenMRS instance that uses a custom Z-segment for national ID
When an integration engineer sends PATCH /v1/interop/connectors/{id}/mapping-overrides with {"PID.3": "national_id", "ZGH.1": "mrn"}
Then interop-service stores the overrides in connector.mapping_overrides
And subsequent inbound messages from this connector are parsed using the overrides
And the override schema is published at GET /v1/interop/connectors/{id}/mapping-schema
Given an upgrade notification is received for the OpenMRS instance
When the admin sends POST /v1/interop/connectors/{id}/upgrade-notice with {"emr_version": "3.1.0"}
Then interop-service runs the connector's regression test suite
And returns {"status": "passed", "tests_run": 24} if all pass
And blocks activation update and returns {"status": "failed", "failures": [...]} if any test fails
INTEROP-US-020 — Receive and Route VXU Immunisation Messages
Epic: INTEROP-EPIC-02
Priority: High
Labels: hl7v2, immunisation, vxu
Story As a facility immunisation officer, I want immunisation records submitted by legacy systems via HL7 VXU V04 to be automatically routed to the immunisations-service, so that the national immunisation registry stays current.
Acceptance Criteria
Given a registered inbound MLLP connector
When the facility system sends a VXU^V04 message for patient "Fatima Noori" with vaccine "COVID-19"
Then interop-service ACKs AA within 500ms
And stores the raw message in hl7_messages with message_type = "VXU_V04"
And maps RXA segment to FHIR Immunization resource
And forwards the Immunization via PUT /fhir/R4/Immunization to immunizations-service
And publishes hl7v2.vxu.received to NATS
Given the VXU message contains an unrecognised vaccine code
When interop-service processes the message
Then it forwards the Immunization resource to immunizations-service with the raw code preserved
And logs a validation warning (not an error) in the message processing audit
And the message status is set to "processed_with_warnings"