Kulcare Docs
Abdm

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.

PathImplemented?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#revoke201 (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}/approvedeny`❌ 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.

IDScenarioMandatoryCode pathStatus
101Patient Discovery (find by ABHA)yesHIU → patients/find (M1 HIU discovery)⚠ verify HIU-discovery wired
102Consent Request InitiationyesConsentRequestService#create → request/init → on-init⚠ on-init resp→response (U12)
103Notification to PHRyesgateway → patient PHR (out-of-band)✅
104Listing of Consent Requestsyesconsents#index (ABHA, name, created/expiry, status, hiTypes)🔧 (U13)
105Consent Request Deniedyesconsent_notify{DENIED} → HiuConsentHandler → ACK✅ (handler correct)
106Consent Request Approved + fetch datayesconsent_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.

IDHI TypeBuilder (plan)Status
107DiagnosticReport (structured/unstructured)U5 DiagnosticReportRecordBuilder🔧 U5 + U11
108PrescriptionU3 PrescriptionRecordBuilder🔧 U3 + U11
109OPConsultationU4 OPConsultationRecordBuilder🔧 U4 + U11
110DischargeSummaryU6 DischargeSummaryRecordBuilder🔧 U6 + U11
111ImmunizationU8 ImmunizationRecordBuilder🔧 U8 + U11
112WellnessRecordU7 WellnessRecordBuilder🔧 U7 + U11
113HealthDocumentU7 HealthDocumentRecordBuilder🔧 U7 + U11

Invoice is NOT an HIU-requestable type (HIP-emission only) — not in this set.

C. Revoke & expiry (Mandatory)

IDScenarioMandatoryCode pathStatus
201Patient revokes consent (PHR)MandatoryPHR → gateway → consent_notify{REVOKED} → HiuConsentHandler🔧 purge (U12)
202HIU cannot view revoked recordsMandatoryhealth_records#show denies after purge🔧 (U12 + U13)
301Consent Expiry (dataEraseAt)MandatoryAbdmConsentPurgeJob (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.

  1. 101 discover — HIU UI finds the patient by ABHA. ✅
  2. 102 init — create consent (purpose CAREMGT, hiTypes ["Prescription"], short expiry).
    • ✅ request/init → 202 → on-init echoes consentRequest.id.
  3. 103 — patient grants in the PHR app. (out-of-band)
  4. 106 grant — consent_notify{GRANTED} → ArtefactService#fetch → on-fetch.
    • After U12: fetch sends X-HIU-ID.
    • AbdmConsentArtefact row created with consentDetail (careContexts, permission).
  5. request data — HiuDataFetchJob → DataFetchService#request_data.
    • After U11: body wrapped in hiRequest + top-level nonce.
  6. on-request — HiuDataHandler#health_information_on_request reconciles the transfer (stores transactionId on the existing transfer — receiver key preserved). After U11.
  7. transfer — HIP pushes encrypted FHIR to dataPushUrl → data_notification → Fidelius decrypt → AbdmConsentArtefact.fhir_records populated. 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;
  8. notify RECEIVED — DataFetchService#notify_data_received. After U11: full notification shape, sessionStatus:'RECEIVED', one statusResponses per care-context.
  9. 104 + 108 display — FE consents#index + health_records#show shows the decrypted Prescription bundle. After U13.
  10. 201 revoke — patient revokes in PHR → consent_notify{REVOKED} → after U12, fhir_records nil'd + audit row. 202: health_records#show now denies.
  11. 301 expiry — wait past dataEraseAt (or run AbdmConsentPurgeJob) → 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:

  1. 🔴 request_data body flat — must be wrapped { hiRequest:{ consent,dateRange,dataPushUrl,keyMaterial,nonce } } + top-level nonce. → U11.
  2. 🔴 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 transactionId only. → U11.
  3. 🔴 HIU notify wrong shape — flat {transactionId,status,sessionStatus:'TRANSFERRED'}; must be {notification:{…sessionStatus:'RECEIVED'…statusResponses[]}}. → U11.
  4. 🔴 No mandatory purge — revoke/expiry/dataEraseAt must hard-null fhir_records + audit; add hourly AbdmConsentPurgeJob. Blocks 201/202/301. → U12.
  5. ⚠ fetch missing X-HIU-ID; on-init + hip/on-request ACKs use resp not response; host-prefix inconsistency. → U12 (extends U10).
  6. 🔧 No listing/display surfaces — consents#index + health_records#show. Blocks 104/107–113. → U13.
  7. 🔧 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).
  8. ⚠ 7-type hiTypes allowlist — Invoice must be rejected at ConsentRequestService. → 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

On this page