M2 — HIP Linking & Care-Context Flow: Test Cases
Scope: ABDM M2 / HIP conformance — how a Kulcare facility (HIP) links a patient's ABHA to their health records, creates care contexts, and notifies the patient. Covers the 28 official NHA test cases in
docs/abdm/specs/M2/M2_BUILDING_HIP_WITH_APIS_UPDATED_22_Aug_871c2f7fcd.xlsx.Why this doc exists: the M2 linking layer was scaffolded in the parts→domains migration but the pre-implementation audit (
docs/audits/abdm-m2-swagger-vs-code-2026-08-02.md) found transport + payload defects. This doc maps every mandatory NHA test case to the code path, gives a plain-text flowchart, and states what must be true to pass it.Kulcare role in M2: HIP only. Kulcare is not the PHR app or the HIE-CM.
Last verified: 2026-08-02 against
feature/abdm_integration_port+ the M2 audit.
0. Did we miss anything? (endpoint coverage vs the spec)
The M2 swagger defines 17 paths. We do not implement every path — only what the mandatory NHA tests exercise. Status:
| Path | Implemented? | Needed for a mandatory test? |
|---|---|---|
v3/token/generate-token | ✅ LinkTokenService | ✅ all HIP_INTI_LINK + USER_INIT_LINK |
hip/v3/link/carecontext | ✅ LinkService + CareContextLinkingService (⚠ payload bug → U9) | ✅ HIP_INTI_LINK_205/305/404/505 |
hip/v3/link/context/notify | ✅ CareContextNotifyService (⚠ bug → U10) | ✅ RECORD_CREATION_101 |
hip/v3/link/patient/links/sms/notify2 | ✅ SmsNotifyService (⚠ REQUEST-ID bug → U10) | ✅ RECORD_CREATION_101 (SMS mode) |
hip/v3/link/patient/links (GET linked pts) | ❌ not implemented | ❌ no mandatory test needs it |
patient-share/v3/share + on-share | ✅ HipLinkHandler (M1 scan-and-share) | ⚠ M1, not a mandatory M2 test |
patient-share/v3/profile/getTokenDetails | ❌ not implemented | ❌ no mandatory test needs it (we read profile from the inbound share payload) |
user-initiated-linking/v3/.../{discover,on-discover,on-init,on-confirm,init,confirm} | ✅ HipDiscoveryHandler + DiscoveryService (⚠ bugs → U10) | ✅ USER_INIT_LINK_602–607 |
Verdict: the two unimplemented M2 endpoints are not on any mandatory test path. The work to pass M2 is fixing payloads/correlation (plan U9 + U10), not adding endpoints.
1. The three M2 flows (plain-text flowcharts)
Flow A — HIP-initiated linking (mobile-OTP / Aadhaar-OTP / demographic / direct)
Tested by HIP_INTI_LINK_201–506 (4 auth modes, same skeleton). Kulcare drives it.
HIP counter / CMS
│ 1. staff enters patient ABHA address
│ 2. POST /integration/abdm/link_requests/fetch_modes → LinkService#fetch_modes
▼ └─► gateway v3/link/.../auth-modes (auth modes for the ABHA)
│ 3. POST /integration/abdm/link_requests/auth_init → LinkService#auth_init (OTP)
▼ └─► gateway sends OTP to patient's phone
│ 4. patient tells staff the OTP
│ 5. POST /integration/abdm/link_requests/auth_confirm → LinkService#auth_confirm
▼ └─► gateway validates OTP ─► on success returns link token
│ 6. token stored on integration.abdm_link_requests (link_token, valid_to)
│ 7. POST /integration/abdm/link_requests/add_contexts → LinkService#add_contexts
▼ └─► gateway hip/v3/link/carecontext (care contexts grouped by hiType ⚠ U9)
│ 8. gateway ─► callback /webhooks/hip/on_add_contexts → HipLinkHandler#on_add_contexts
▼ (acknowledgement: SUCCESS / ERRORED)
│ 9. (optional) CareContextNotifyService / SmsNotifyService → patient notified in PHR ⚠ U10
▼
DONE — patient ABHA linked, care contexts visible in PHR appAuth-mode variants: 201–206 = mobile OTP · 301–306 = Aadhaar-linked mobile OTP · 401–405 = direct auth (PHR-app notification) · 501–506 = demographic auth.
Flow B — User-initiated linking (patient scans / searches from PHR app)
Tested by USER_INIT_LINK_602–607. The patient's PHR app drives; Kulcare only answers
callbacks.
Patient PHR app ABDM gateway Kulcare HIP
│ search facility / scan QR ───────►│ │
│ │ POST /webhooks/discovery/discover│
│ │ ─────────────────────────────────►│ HipDiscoveryHandler#discover
│ │ │ ⚠ reads X-HIP-ID header (U10)
│ │ ◄── on-discover (care contexts) ─│ DiscoveryService#on_discover
│ │ ⚠ needs hiType+count+matchedBy (U10)
│ patient picks care contexts │ │
│ ──────────────────────────────────►│ POST /webhooks/discovery/link_init│
│ │ ─────────────────────────────────►│ HipDiscoveryHandler#link_init
│ │ ◄── on-init (linking token) ──────│
│ patient consents │ │
│ ──────────────────────────────────►│ POST /webhooks/discovery/link_confirm│
│ │ ─────────────────────────────────►│ HipDiscoveryHandler#link_confirm
│ │ ◄── on-confirm (linked contexts) ─│ DiscoveryService#on_confirm
│ │ ⚠ needs hiType+count (U10) │
▼ records now linked & visible in PHR │ │Flow C — Health-record creation + patient notification
Tested by RECORD_CREATION_101. Triggered when a visit finalises.
Encounter status → 'finished'
│
▼ EnqueueCareContextLinkJob
CareContextLinkingService
│ • build visit-centric care contexts (U9) • build FHIR bundle per hiType (U1–U8)
│ • POST hip/v3/link/carecontext (grouped by hiType, key 'hiTypes' ⚠ U9)
▼
(patient already linked? yes →) CareContextNotifyService
│ • POST hip/v3/link/context/notify { hip:{id,name,type} ⚠ U10, REQUEST-ID ⚠ U10 }
│ • OR SmsNotifyService /patient/links/sms/notify2 (REQUEST-ID ⚠ U10)
▼
gateway ─► patient's PHR app shows the new care contexts⚠ markers = defects fixed by plan units U9 / U10. Until those land, Flows A/B/C carry non-conformant payloads and the PHR app may not show contexts.
2. Official test-case matrix (28 NHA cases)
Legend: ✅ pass (after plan U9/U10 + FHIR U1–U8 land) · ⚠ code exists but payload broken · 🔧 build needed · ❌ negative
A. Health-record creation
| ID | Scenario | Code path | Status | How to test |
|---|---|---|---|---|
| RECORD_CREATION_101 | Digital health records created per HI type | EncountersController#transition → EnqueueCareContextLinkJob → CareContextLinkingService → builders | 🔧 (FHIR U1–U8 empty {} today) | Finish an encounter with meds/labs → AbdmCareContext.fhir_bundle is a real Composition document bundle, not {} |
B. HIP-initiated linking — mobile OTP (HIP_INTI_LINK_201–206)
| ID | Scenario | Code path | Status |
|---|---|---|---|
| 201 | Link record via mobile OTP | LinkService#fetch_modes + #auth_init | ⚠ notify payload later |
| 202 | Receive OTP | (gateway → patient phone) | ✅ out-of-band |
| 203 | OTP validation | LinkService#auth_confirm → LinkTokenService | ⚠ generate_token dup (U9) |
| 204 | Creation of new link token | LinkTokenService → on_token_generate webhook | ✅ |
| 205 | Linking of health records | LinkService#add_contexts → link/carecontext | ⚠ hiType grouping (U9) |
| 206 | Pull records | patient PHR → contexts visible | ⚠ depends on notify (U10) |
C. HIP-initiated linking — Aadhaar-linked mobile OTP (HIP_INTI_LINK_301–306)
Same skeleton as B; auth mode = Aadhaar OTP. Same code paths, same ⚠ markers.
D. HIP-initiated linking — direct auth (HIP_INTI_LINK_401–405)
| 401 | Direct-auth linking init | LinkService direct path | ⚠ |
| 402 | Notification on PHR app | CareContextNotifyService | ⚠ (U10) |
| 403–405 | Token / link / pull | as B 204–206 | ⚠/✅ |
E. HIP-initiated linking — demographic auth (HIP_INTI_LINK_501–506)
Demographic (name+dob+gender) validation instead of OTP. Same skeleton; LinkService#auth_init
with demographic payload. ⚠ payload conformance (U9/U10).
F. User-initiated linking (USER_INIT_LINK_602–607) — PHR-driven
| ID | Scenario | Code path | Status |
|---|---|---|---|
| 602 | Search for facility/HIP | (PHR → gateway) | ✅ out-of-band |
| 603 | Share user profile with HIP | HipDiscoveryHandler#discover → DiscoveryService#on_discover | ⚠ facility via X-HIP-ID + hiType/count/matchedBy (U10) |
| 604 | Fetch health records | on_discover returns contexts | ⚠ (U10) |
| 605 | Provide consent for linking | HipDiscoveryHandler#link_init → on_init (token) | ⚠ verify |
| 606 | Validate request | HipDiscoveryHandler#link_confirm | ⚠ verify |
| 607 | Pull records | on_confirm returns linked contexts | ⚠ hiType/count (U10) |
NHA rule: at least one auth mode (B/C/D/E) must pass end-to-end; all are "Applicable To = HIP".
3. End-to-end manual walkthrough (Flow A, mobile OTP)
Pre: facility registered as HIP (integration.abdm_facility_registrations, active=true,
service_id set); a real sandbox ABHA (e.g. simmi@sbx); Sidekiq + the webhook endpoint
reachable.
- CMS → patient : open the patient, ABHA-verify →
fetch_modes→auth_init.- ✅ gateway sends OTP; ❌ expect
ABDM-1092/errors logged if facility not HIP.
- ✅ gateway sends OTP; ❌ expect
- Enter OTP →
auth_confirm→LinkTokenService→ 202 → ~2–3s lateron_token_generatewebhook stores the token. Query:SELECT id, abha_address, status, link_token IS NOT NULL, valid_to FROM integration.abdm_link_requests WHERE abha_address='simmi@sbx' ORDER BY created_at DESC LIMIT 1; - Add contexts →
add_contexts→link/carecontext. After U9: payload grouped byhiTypes, one patient block per type. on_add_contextscallback →HipLinkHandler→ acknowledgement.- Notify →
CareContextNotifyService(or SMS) → after U10 the REQUEST-ID correlates andhip.{id,name,type}is present. - Verify in the PHR app: the care contexts appear with the right HI types.
Repeat for demographic mode (E) to cover the second mandatory auth track.
4. DB inspection queries
-- Link requests for an ABHA (token delivered?)
SELECT id, patient_id, abha_address, status, link_token IS NOT NULL AS has_token,
valid_to, created_at
FROM integration.abdm_link_requests
WHERE abha_address = 'simmi@sbx' ORDER BY created_at DESC;
-- Care contexts created for a patient (and their FHIR bundles — must be non-empty after U1–U8)
SELECT cc.id, cc.resource_type, cc.reference, (cc.fhir_bundle IS NOT NULL AND cc.fhir_bundle::text <> '{}') AS has_real_bundle
FROM integration.abdm_care_contexts cc
JOIN clinical.patients p ON p.id = cc.patient_id
JOIN phr.abha_profiles ap ON ap.id = p.phr_abha_profile_id
WHERE ap.abha_address = 'simmi@sbx';
-- Outbound M2 API calls + their REQUEST-ID correlation
SELECT endpoint, status_code, request_payload->>'requestId' AS req_id,
response_payload->>'requestId' AS echoed_req_id, created_at
FROM integration.abdm_api_logs
WHERE endpoint LIKE '%/link/%' OR endpoint LIKE '%/token/%'
ORDER BY created_at DESC LIMIT 30;5. Known gaps to clear before M2 sign-off
These are exactly plan units U9 (linking/granularity/FHIR dispatch) + U10 (transport conformance) + U1–U8 (real FHIR bundles). From the M2 audit:
link/carecontextpayload — singlehiType+ one patient block (should behiTypes, grouped per type). BothLinkService#add_contextsandCareContextLinkingService#add_contexts_to_abdm. → U9.context/notify— sendship:{id}only (needs{id,name,type}); REQUEST-ID regenerated byApiClientso the callback never correlates. → U10. Same REQUEST-ID bug inSmsNotifyService.- Inbound notify/SMS callbacks — read top-level
requestId/error; swagger nests underresponse.requestId/acknowledgement.status. → U10. - User-initiated
discover— readspayload.hip.id(absent in V3); must useX-HIP-IDheader → facility never resolves today. → U10. on-discover/on-confirm— missing per-patienthiType+count;matchedBynested wrong. → U10.- Duplicate
LinkService#generate_token(non-conformant, wired to controller) — retire, route throughLinkTokenService. → U9. - Empty
{}FHIR bundles —RECORD_CREATION_101fails until builders U1–U8 produce real document bundles. → U1–U8.
Items 1–6 are payload/correlation fixes (no new endpoints). Item 7 is the FHIR plan.
6. Quick verification greps
# Both live add_contexts paths (must both group by hiTypes after U9)
grep -rn "hiType\|hiTypes\|add_contexts" backend/domains/integration/services/abdm/hip/*.rb
# REQUEST-ID correlation (one id = header + stored + callback lookup)
grep -rn "request_id\|REQUEST-ID\|last_notify_request_id" backend/domains/integration/services/abdm/hip/{care_context_notify,sms_notify}_service.rb
# Discovery facility source (must be X-HIP-ID after U10)
grep -rn "X-HIP-ID\|dig('hip'" backend/domains/integration/services/abdm/webhooks/hip_discovery_handler.rb
# FHIR dispatch (must cover all 8 types after U9)
grep -n "when '\|generate_fhir_bundle" backend/domains/integration/services/abdm/hip/care_context_linking_service.rb