Kulcare Docs
Abdm

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:

PathImplemented?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 app

Auth-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

IDScenarioCode pathStatusHow to test
RECORD_CREATION_101Digital health records created per HI typeEncountersController#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 {}
IDScenarioCode pathStatus
201Link record via mobile OTPLinkService#fetch_modes + #auth_init⚠ notify payload later
202Receive OTP(gateway → patient phone)✅ out-of-band
203OTP validationLinkService#auth_confirm → LinkTokenService⚠ generate_token dup (U9)
204Creation of new link tokenLinkTokenService → on_token_generate webhook✅
205Linking of health recordsLinkService#add_contexts → link/carecontext⚠ hiType grouping (U9)
206Pull recordspatient PHR → contexts visible⚠ depends on notify (U10)

Same skeleton as B; auth mode = Aadhaar OTP. Same code paths, same ⚠ markers.

| 401 | Direct-auth linking init | LinkService direct path | ⚠ | | 402 | Notification on PHR app | CareContextNotifyService | ⚠ (U10) | | 403–405 | Token / link / pull | as B 204–206 | ⚠/✅ |

Demographic (name+dob+gender) validation instead of OTP. Same skeleton; LinkService#auth_init with demographic payload. ⚠ payload conformance (U9/U10).

IDScenarioCode pathStatus
602Search for facility/HIP(PHR → gateway)✅ out-of-band
603Share user profile with HIPHipDiscoveryHandler#discover → DiscoveryService#on_discover⚠ facility via X-HIP-ID + hiType/count/matchedBy (U10)
604Fetch health recordson_discover returns contexts⚠ (U10)
605Provide consent for linkingHipDiscoveryHandler#link_init → on_init (token)⚠ verify
606Validate requestHipDiscoveryHandler#link_confirm⚠ verify
607Pull recordson_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.

  1. CMS → patient : open the patient, ABHA-verify → fetch_modes → auth_init.
    • ✅ gateway sends OTP; ❌ expect ABDM-1092/errors logged if facility not HIP.
  2. Enter OTP → auth_confirm → LinkTokenService → 202 → ~2–3s later on_token_generate webhook 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;
  3. Add contexts → add_contexts → link/carecontext. After U9: payload grouped by hiTypes, one patient block per type.
  4. on_add_contexts callback → HipLinkHandler → acknowledgement.
  5. Notify → CareContextNotifyService (or SMS) → after U10 the REQUEST-ID correlates and hip.{id,name,type} is present.
  6. 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:

  1. link/carecontext payload — single hiType + one patient block (should be hiTypes, grouped per type). Both LinkService#add_contexts and CareContextLinkingService#add_contexts_to_abdm. → U9.
  2. context/notify — sends hip:{id} only (needs {id,name,type}); REQUEST-ID regenerated by ApiClient so the callback never correlates. → U10. Same REQUEST-ID bug in SmsNotifyService.
  3. Inbound notify/SMS callbacks — read top-level requestId/error; swagger nests under response.requestId / acknowledgement.status. → U10.
  4. User-initiated discover — reads payload.hip.id (absent in V3); must use X-HIP-ID header → facility never resolves today. → U10.
  5. on-discover / on-confirm — missing per-patient hiType+count; matchedBy nested wrong. → U10.
  6. Duplicate LinkService#generate_token (non-conformant, wired to controller) — retire, route through LinkTokenService. → U9.
  7. Empty {} FHIR bundles — RECORD_CREATION_101 fails 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

On this page