mirror of
https://github.com/AmanTahiliani/FHIR-Sandbox.git
synced 2026-08-08 04:06:16 -04:00
160 lines
5.9 KiB
Markdown
160 lines
5.9 KiB
Markdown
# Bidirectional Patient-Matching API — Rimidi ↔ HRS (FHIR Sandbox)
|
||
|
||
## Overview
|
||
|
||
Rimidi was acquired by HRS, another healthcare company. Both platforms need to
|
||
discover overlapping patients so clinical staff in either system can see which
|
||
patients also exist in the other. This document defines the shared API contract,
|
||
matching algorithm, auth model, and phased rollout plan.
|
||
|
||
For the **demo** the FHIR Sandbox plays the role of HRS.
|
||
|
||
---
|
||
|
||
## Shared JSON Contract
|
||
|
||
Both systems expose `POST /api/patient-match/` behind API-key auth.
|
||
|
||
### Request
|
||
|
||
```json
|
||
{
|
||
"first_name": "Jane",
|
||
"last_name": "Smith",
|
||
"email": "jane.smith@email.com",
|
||
"dob": "1990-04-22",
|
||
"sex": "F"
|
||
}
|
||
```
|
||
|
||
| Field | Type | Required | Notes |
|
||
|--------------|--------|----------|----------------------------------------|
|
||
| `first_name` | string | yes | Will be lowercased & trimmed |
|
||
| `last_name` | string | yes | Will be lowercased & trimmed |
|
||
| `email` | string | no | Will be lowercased & trimmed |
|
||
| `dob` | string | yes | ISO 8601 `YYYY-MM-DD` |
|
||
| `sex` | string | yes | Normalized: `M`/`F`/`O`/`U` |
|
||
|
||
### Response
|
||
|
||
```json
|
||
{
|
||
"source_system": "rimidi",
|
||
"matches": [
|
||
{
|
||
"patient_ref": "opaque-signed-token",
|
||
"score": 4,
|
||
"fields": {
|
||
"first_name": { "value": "Jane", "match": true },
|
||
"last_name": { "value": "Smith", "match": true },
|
||
"email": { "value": "jane.smith@email.com", "match": true },
|
||
"dob": { "value": "1990-04-22", "match": true },
|
||
"sex": { "value": "F", "match": false }
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
| Field | Notes |
|
||
|----------------------------|-------------------------------------------------|
|
||
| `source_system` | `"rimidi"` or `"hrs"` — identifies the responder |
|
||
| `matches[].patient_ref` | Opaque token — never exposes raw DB PK |
|
||
| `matches[].score` | Count of exactly matching fields (2–5) |
|
||
| `matches[].fields.*.value` | The **remote** system's value for this field |
|
||
| `matches[].fields.*.match` | Whether the field matched exactly |
|
||
|
||
### Rules
|
||
|
||
- Only patients with **≥ 2 exact field matches** are returned.
|
||
- Comparisons are **case-insensitive, whitespace-stripped**.
|
||
- Sex is normalized before comparison:
|
||
- Provider: `"M"` / `"F"` → canonical `"M"` / `"F"` / `"O"` / `"U"`
|
||
- Sandbox: `"male"` / `"female"` / `"other"` / `"unknown"` → `"M"` / `"F"` / `"O"` / `"U"`
|
||
- DOB is always ISO 8601 `"YYYY-MM-DD"`.
|
||
- Results are sorted by `score` descending.
|
||
- `patient_ref` is an HMAC-signed / Django-signed token of the internal PK.
|
||
|
||
---
|
||
|
||
## Auth Strategy
|
||
|
||
Both systems validate a **pre-shared API key** in the `X-Api-Key` header.
|
||
|
||
- **Provider** adds `PATIENT_MATCH_API_KEY` to Django settings.
|
||
- **FHIR Sandbox** adds a `PatientMatchAPIKey` config field and an API-key
|
||
middleware that applies to `/api/*` routes only.
|
||
- Keys are **directional** — each system holds the key for the *other* system.
|
||
|
||
---
|
||
|
||
## Field Mapping
|
||
|
||
| Provider (`RimidiUser`) | FHIR Sandbox (`User`) | Normalization |
|
||
|----------------------------|------------------------|------------------------|
|
||
| `first_name` (encrypted) | `first_name` | lowercase + trim |
|
||
| `last_name` (encrypted) | `last_name` | lowercase + trim |
|
||
| `email` (encrypted) | `email` | lowercase + trim |
|
||
| `birth_date` (encrypted) | `dob` (string) | both → `YYYY-MM-DD` |
|
||
| `sex` (`"M"` / `"F"`) | `gender` (FHIR codes) | both → `M/F/O/U` |
|
||
|
||
---
|
||
|
||
## Critical Constraint: Provider PII Encryption
|
||
|
||
All matchable demographic fields in Provider (`first_name`, `last_name`, `email`,
|
||
`birth_date`, `sex`) are **AES-encrypted** at the column level via
|
||
`django-encrypted-model-fields`. No SQL-level filtering is possible.
|
||
|
||
Matching must be done in **Python application memory**: load all patients for the
|
||
provider, decrypt them via Django ORM, and compare. This works for typical
|
||
provider panels (hundreds to low-thousands of patients).
|
||
|
||
---
|
||
|
||
## Phased Rollout
|
||
|
||
### Phase 1 — Contract, Auth & Field Normalization (2–3 days)
|
||
|
||
- Lock the shared JSON schema (this document).
|
||
- Add `PATIENT_MATCH_API_KEY` to Provider settings (cs_hub app).
|
||
- Add API-key middleware + stub handler in FHIR Sandbox.
|
||
- Build field normalization utilities in both systems (unit-testable).
|
||
|
||
### Phase 2 — Provider Patient Match API (3–4 days)
|
||
|
||
- Full `POST /cshub/api/patient-match/` endpoint in `cs_hub`.
|
||
- In-memory matching loop (decrypt all patients for the provider, compare).
|
||
- Opaque `patient_ref` via Django `TimestampSigner`.
|
||
- Unit tests for matching logic, normalization, and auth.
|
||
|
||
### Phase 3 — FHIR Sandbox Patient Match API (2–3 days)
|
||
|
||
- `POST /api/patient-match` endpoint in Sandbox.
|
||
- `ListAllPatients()` DB query for matching.
|
||
- API-key middleware.
|
||
- Unit tests.
|
||
|
||
### Phase 4 — Provider UI: "Find in HRS" (3–4 days)
|
||
|
||
- Proxy endpoint `POST /cshub/api/patient-match-proxy/` (session-auth, calls Sandbox).
|
||
- "Find in HRS" button on patient chart.
|
||
- Match diff modal showing per-field comparison.
|
||
|
||
### Phase 5 — Sandbox UI: "Find in Rimidi" (2–3 days)
|
||
|
||
- Proxy handler (session-required, calls Provider).
|
||
- "Find in Rimidi" button on dashboard.
|
||
- Diff panel with field-level comparison.
|
||
|
||
---
|
||
|
||
## Future Considerations
|
||
|
||
1. **Deterministic hash columns** for scale (SHA-256 of lowercased fields) to
|
||
avoid full-table decrypt at Provider panels > 5k patients.
|
||
2. **Multi-instance Rimidi** — HRS fans out to multiple Rimidi deployments.
|
||
`org_key` + per-instance API key handles this.
|
||
3. **Patient linking** — a future `PatientCrossReference` model to explicitly
|
||
link records after a match is confirmed by a human.
|