M3 — HIU Consent & Data-Flow: Test Cases
Scope: ABDM M3 / HIU conformance — how a Kulcare practitioner (HIU) requests a patient's records from other HIPs, stores consent artefacts, fetches + decrypts the FHIR health data, displays it, and purges it on revoke/expiry. Covers the 16 official NHA test cases in
docs/abdm/specs/M3/M3_BUILDING_HIU_WITH_APIS_UPDATED_22_August_de6a02460a.xlsx(HIU_FLOW_101–113,201,202,301).Why this doc exists: the M3 audit (
docs/audits/abdm-m3-swagger-vs-code-2026-08-02.md) found the HIU-side data-receive path broken (3 compounding P1 payload defects) and a missing mandatory purge. This doc maps every NHA test case to the code path, a plain-text flowchart, and what must be true to pass.Kulcare role in M3: both — HIU requests + receives (this doc) and HIP serves (its own patients' records; the serving side is exercised when another HIU asks Kulcare, and depends on the M2/FHIR plan).
Last verified: 2026-08-02 against
feature/abdm_integration_port+ the M3 audit.
0. Did we miss anything? (endpoint coverage vs the spec)
The M3 consent-management-data-flow.yaml swagger defines 21 paths; subscription.yaml
defines 15 more. We implement what the mandatory tests exercise.
| Path | Implemented? | Needed for a mandatory test? |
|---|---|---|
consent/v3/request/init · request/status | ✅ ConsentRequestService | ✅ 102, 104 |
request/hiu/on-notify (HIU ACK) | ✅ ConsentRequestService#ack_notification | ✅ 103/105/106 |
consent/v3/fetch | ✅ ArtefactService#fetch (⚠ missing X-HIU-ID → U12) | ✅ 106 |
| on-init / on-status / consent_notify / on-fetch callbacks | ✅ HiuConsentHandler (⚠ on-init resp→response → U12) | ✅ 102/103/105/106 |
data-flow/v3/health-information/request | ✅ DataFetchService#request_data (🔴 flat body → U11) | ✅ 106/107–113 |
data-flow/v3/health-information/notify (HIU) | ✅ DataFetchService#notify_data_received (🔴 wrong shape → U11) | ✅ 106/107–113 |
HIU on-request callback (/v3/hiu/health-information/on-request) | ✅ HiuDataHandler#health_information_on_request (🔴 role confusion → U11) | ✅ 106/107–113 |
/health-information/transfer (HIP→HIU push) + receive | ✅ Hip::DataTransferService + HiuDataHandler#data_notification | ✅ 106/107–113 |
HIP side: hip/on-request + consent/v3/request/hip/on-notify + hip consent_notify | ✅ HipDataHandler / HipConsentHandler / Hip::DataTransferService | ✅ (when Kulcare is the HIP) |
consent/v3/revoke (outbound) | ✅ Cm::ConsentService#revoke | 201 (patient revokes via PHR → inbound notify) |
data-flow/v3/health-information/request/status/{transaction-id} | ❌ not implemented | ❌ async notify covers it; no mandatory test needs it |
consent/v3/request/{id} GET, consent/v3/artefact*, auto/approve*, `request/{id}/approve | deny` | ❌ not implemented |
subscription.yaml (15 paths) | ❌ deferred | ❌ separate certification track (consent-expiry auto-subscription); not in the 16 HIU_FLOW cases |
Verdict: passing M3 needs fixing the HIU data-flow payloads + adding the purge + display surfaces (plan U11–U13) and the FHIR bundles to actually carry data (U1–U8). It does not need subscription, artefact-admin, or data-flow status endpoints.
1. The M3 end-to-end flow (plain-text flowchart)
Kulcare HIU ABDM gateway / CM Patient PHR other HIP
│
│ 101: discover patient by ABHA
│ ─────► patients/find ──────────►│ validates ABHA ◄─────────────│
│ ◄──── patient reference ────────│
│
│ 102: create consent request (purpose, dateRange, expiry, hiTypes[7], permission)
│ ─────► consent/v3/request/init ─►│
│ ◄──── on-init (consentRequest.id)─│
│
│ │ 103: notify patient ──────────►│ (PHR shows request)
│ │ ◄──── patient GRANTS/DENIES ────│
│ │
│ ◄──── consent_notify {status} ───│ (105 DENIED / 106 GRANTED)
│ HiuConsentHandler#consent_notify → ACK via request/hiu/on-notify
│ on GRANTED ─► Cm::ArtefactService#fetch ─► consent/v3/fetch (⚠ X-HIU-ID → U12)
│ ◄──── on-fetch {consent.consentDetail{careContexts,permission,hip}} ──│
│ store AbdmConsentArtefact; enqueue HiuDataFetchJob
│
│ 106/107–113: request the health data
│ ─────► data-flow/v3/health-information/request { hiRequest:{ consent,dateRange,
│ dataPushUrl, keyMaterial, nonce } } (🔴 flat today → U11)
│ │ forwards to HIP ──────────────────────────────►│ HIP
│ ◄──── on-request ACK { hiRequest:{transactionId} } ──│ (🔴 reconcile transfer → U11)
│ │ ◄──── encrypted FHIR pushed to dataPushUrl ─────│ HIP
│ ◄──── /health-information/transfer { entries[].content(encrypted), keyMaterial } ───│
│ HiuDataHandler#data_notification → Fidelius decrypt → store fhir_records
│
│ ─────► data-flow/v3/health-information/notify { notification:{…sessionStatus:'RECEIVED'…} }
│ (🔴 wrong shape today → U11)
│
│ 107–113 / 104: display
│ FE reads consents#index (status/timestamps/hiTypes) + health_records#show (decrypted FHIR)
│ (🔧 surfaces → U13)
│
│ 201/202: patient REVOKES (via PHR)
│ ◄──── consent_notify {status:REVOKED} ──│
│ HiuConsentHandler → purge fhir_records (nil) + audit (🔧 → U12)
│ records no longer viewable in HIU
│
│ 301: consent EXPIRES (dataEraseAt)
│ AbdmConsentPurgeJob (hourly) purges past dataEraseAt (🔧 → U12)2. Official test-case matrix (16 NHA HIU cases)
Legend: ✅ pass · 🔧 build/fix needed · ❌ negative · ⚠ partial. "After plan" = once U11–U13 (+ U1–U8 for content) land.
A. Consent lifecycle (mandatory functional)
| ID | Scenario | Mandatory | Code path | Status |
|---|---|---|---|---|
| 101 | Patient Discovery (find by ABHA) | yes | HIU → patients/find (M1 HIU discovery) | ⚠ verify HIU-discovery wired |
| 102 | Consent Request Initiation | yes | ConsentRequestService#create → request/init → on-init | ⚠ on-init resp→response (U12) |
| 103 | Notification to PHR | yes | gateway → patient PHR (out-of-band) | ✅ |
| 104 | Listing of Consent Requests | yes | consents#index (ABHA, name, created/expiry, status, hiTypes) | 🔧 (U13) |
| 105 | Consent Request Denied | yes | consent_notify{DENIED} → HiuConsentHandler → ACK | ✅ (handler correct) |
| 106 | Consent Request Approved + fetch data | yes | consent_notify{GRANTED} → ArtefactService#fetch → on-fetch → request_data → decrypt | 🔴 (U11 + U12) |
B. Fetch health data per HI type — "any one of them is mandatory"
NHA rule: at least one HI-type fetch must succeed end-to-end (request → decrypt → display). All seven share the same code path; the delta is the FHIR builder for that type.
| ID | HI Type | Builder (plan) | Status |
|---|---|---|---|
| 107 | DiagnosticReport (structured/unstructured) | U5 DiagnosticReportRecordBuilder | 🔧 U5 + U11 |
| 108 | Prescription | U3 PrescriptionRecordBuilder | 🔧 U3 + U11 |
| 109 | OPConsultation | U4 OPConsultationRecordBuilder | 🔧 U4 + U11 |
| 110 | DischargeSummary | U6 DischargeSummaryRecordBuilder | 🔧 U6 + U11 |
| 111 | Immunization | U8 ImmunizationRecordBuilder | 🔧 U8 + U11 |
| 112 | WellnessRecord | U7 WellnessRecordBuilder | 🔧 U7 + U11 |
| 113 | HealthDocument | U7 HealthDocumentRecordBuilder | 🔧 U7 + U11 |
Invoice is NOT an HIU-requestable type (HIP-emission only) — not in this set.
C. Revoke & expiry (Mandatory)
| ID | Scenario | Mandatory | Code path | Status |
|---|---|---|---|---|
| 201 | Patient revokes consent (PHR) | Mandatory | PHR → gateway → consent_notify{REVOKED} → HiuConsentHandler | 🔧 purge (U12) |
| 202 | HIU cannot view revoked records | Mandatory | health_records#show denies after purge | 🔧 (U12 + U13) |
| 301 | Consent Expiry (dataEraseAt) | Mandatory | AbdmConsentPurgeJob (hourly) | 🔧 (U12) |
The xlsx marks 201/202 explicitly "Mandatory" with note "No API" — i.e. a functional check that revoked/expired records are gone from the HIU. Without the purge (U12) this fails.
3. End-to-end manual walkthrough (106 + one HI type, e.g. 108 Prescription)
Pre: a sandbox patient ABHA already linked to an external HIP that has a Prescription; Fidelius
running (docker ps → fidelius on :8090); Sidekiq + webhook endpoints reachable.
- 101 discover — HIU UI finds the patient by ABHA. ✅
- 102 init — create consent (purpose
CAREMGT, hiTypes["Prescription"], short expiry).- ✅
request/init→ 202 →on-initechoesconsentRequest.id.
- ✅
- 103 — patient grants in the PHR app. (out-of-band)
- 106 grant —
consent_notify{GRANTED}→ArtefactService#fetch→on-fetch.- After U12:
fetchsendsX-HIU-ID. AbdmConsentArtefactrow created withconsentDetail(careContexts, permission).
- After U12:
- request data —
HiuDataFetchJob→DataFetchService#request_data.- After U11: body wrapped in
hiRequest+ top-levelnonce.
- After U11: body wrapped in
- on-request —
HiuDataHandler#health_information_on_requestreconciles the transfer (storestransactionIdon the existing transfer — receiver key preserved). After U11. - transfer — HIP pushes encrypted FHIR to
dataPushUrl→data_notification→ Fidelius decrypt →AbdmConsentArtefact.fhir_recordspopulated. Verify:SELECT id, consent_id, fhir_records IS NOT NULL AS has_data, length(fhir_records::text) AS bytes FROM integration.abdm_consent_artefacts WHERE consent_id = (SELECT id FROM integration.abdm_consents WHERE consent_id='<ABDM-consentId>') ORDER BY created_at DESC; - notify RECEIVED —
DataFetchService#notify_data_received. After U11: fullnotificationshape,sessionStatus:'RECEIVED', onestatusResponsesper care-context. - 104 + 108 display — FE
consents#index+health_records#showshows the decrypted Prescription bundle. After U13. - 201 revoke — patient revokes in PHR →
consent_notify{REVOKED}→ after U12,fhir_recordsnil'd + audit row. 202:health_records#shownow denies. - 301 expiry — wait past
dataEraseAt(or runAbdmConsentPurgeJob) → records purged.
4. DB inspection queries
-- Consents the HIU requested, with status + timestamps (104 listing)
SELECT id, consent_id, patient_id, status, purpose, hi_types,
granted_at, consent_expiry, data_erase_at, created_at
FROM integration.abdm_consents
WHERE direction = 'hiu' ORDER BY created_at DESC;
-- Artefacts + decrypted data presence (107–113 fetch)
SELECT a.id, a.consent_id, a.fhir_records IS NOT NULL AS has_data,
length(a.fhir_records::text) AS bytes, a.created_at
FROM integration.abdm_consent_artefacts a ORDER BY created_at DESC;
-- Data transfers + decrypt outcome (106 request/receive)
SELECT id, transaction_id, direction, status, data_push_url,
key_material ? 'receiver_private_key' AS has_recv_key, created_at
FROM integration.abdm_data_transfers WHERE direction='hiu' ORDER BY created_at DESC;
-- Purge audit (201/202/301)
SELECT abdm_consent_id, previous_status, new_status, change_reason, created_at
FROM integration.abdm_consent_audit_logs
WHERE new_status IN ('purged','revoked','expired') ORDER BY created_at DESC;5. Known gaps to clear before M3 sign-off
These are exactly plan units U11–U13 (+ U1–U8 for content + the shared U10 transport pass). From the M3 audit:
- 🔴
request_databody flat — must be wrapped{ hiRequest:{ consent,dateRange,dataPushUrl,keyMaterial,nonce } }+ top-levelnonce. → U11. - 🔴 HIU on-request handler role confusion — parses HIP-style fields + creates a conflicting transfer (no receiver key → decrypt fails). Must reconcile the existing transfer and store
transactionIdonly. → U11. - 🔴 HIU
notifywrong shape — flat{transactionId,status,sessionStatus:'TRANSFERRED'}; must be{notification:{…sessionStatus:'RECEIVED'…statusResponses[]}}. → U11. - 🔴 No mandatory purge — revoke/expiry/
dataEraseAtmust hard-nullfhir_records+ audit; add hourlyAbdmConsentPurgeJob. Blocks 201/202/301. → U12. - ⚠
fetchmissingX-HIU-ID; on-init + hip/on-request ACKs userespnotresponse; host-prefix inconsistency. → U12 (extends U10). - 🔧 No listing/display surfaces —
consents#index+health_records#show. Blocks 104/107–113. → U13. - 🔧 Empty
{}bundles — 107–113 carry no data until FHIR builders U1–U8 land (served by the HIP side, or by an external HIP in live testing). - ⚠ 7-type
hiTypesallowlist —Invoicemust be rejected atConsentRequestService. → U12.
Items 1–5 are payload/transport fixes; 4 + 8 are compliance; 6–7 are content/display. No new gateway endpoints are required to pass the 16 mandatory cases.
6. Quick verification greps
# HIU data-flow request shape (must be hiRequest-wrapped after U11)
grep -n "hiRequest\|dataPushUrl\|keyMaterial\|nonce" backend/domains/integration/services/abdm/hiu/data_fetch_service.rb
# on-request handler must NOT read HIP-style fields after U11
grep -n "consent.*id\|dataPushUrl\|transactionId\|find_or_initialize_by" backend/domains/integration/services/abdm/webhooks/hiu_data_handler.rb
# Purge wiring (after U12)
grep -rn "fhir_records = nil\|fhir_records=\|AbdmConsentPurgeJob\|purged" backend/domains/integration
# ACK key (resp -> response after U12)
grep -rn "resp:\|response:" backend/domains/integration/services/abdm/{hiu,hip,cm}/*.rb backend/domains/integration/services/abdm/webhooks/*.rb
# Display surfaces (after U13)
grep -rn "consents_controller\|health_records_controller" backend/domains/integration/controllers backend/domains/integration/routes.rb