DHS (NPHIES) Integration — Overview
Pull request: #119 · maysara/DHS-integration → main Status: Unreleased. Gated behind the FEATURE_DHS_INTEGRATION flag, which is seeded with a beta-only rule. Nothing in here is live for general customers.
Business view
Clinics in Saudi Arabia and the wider Gulf cannot simply invoice an insurer and hope for the best. Before treating an insured patient, the clinic has to ask the payer three questions, in order:
- Who actually insures this person? A patient walks in with a national ID or an
Iqama. The clinic needs to know which insurer, which policy and which class of cover they belong to — information the patient often does not know or gets wrong.
- Is their cover live today? Policies lapse, employers switch insurers, and
dependants get removed. An eligibility check answers "is this person covered right now, and for how much?"
- Will you pay for this specific treatment? For anything beyond routine work the
payer wants to approve the treatment plan up front. That is a pre-authorization, or pre-auth: the clinic sends the proposed procedures, diagnoses and supporting files, and the payer replies approved / partially approved / rejected — sometimes minutes later, sometimes days.
Today clinics do all three on the payer's own web portal, retyping patient details and procedure codes by hand, then retyping the answer back into Dentolize. It is slow, it is error-prone, and the clinic frequently discovers a rejection only after the treatment has been done and the invoice raised.
This feature moves all three steps into Dentolize. DHS (the Motalabate Health platform, which fronts the Saudi NPHIES national exchange) becomes a service the app talks to on the clinic's behalf. The receptionist checks insurance from the patient screen; the dentist submits a pre-auth straight from the treatment chart; and the app polls the payer in the background so the approval number and the approved amounts land on the operations automatically.
The other half of the feature is protection. Once a treatment is sitting with a payer awaiting approval, Dentolize stops staff from quietly changing the price, the tooth or the diagnosis underneath it, and stops the treatment being invoiced before the payer has answered. That keeps what the clinic billed and what the payer approved in sync.
What a clinic actually gets
| Capability | Where it lives in the app |
|---|---|
| Store and test the clinic's DHS credentials | Settings → Integrations → DHS Integration |
| Map each branch to its NPHIES provider code | Same wizard, step 2 |
| Discover a patient's insurer / policy / class from their ID | Patient profile → Check Insurance |
| Create the matching insurer records locally, in one flow | Same wizard |
| Confirm cover is active | Patient profile → Check Eligibility |
| Submit a pre-auth for selected treatments | Patient chart → GET Approval |
| Watch approvals and their status | Patient chart → Approvals tab |
| Chase, cancel, or hand-reconcile an approval | Status popover on the Approvals tab |
| Automatic status polling and amount reconciliation | Background job, every minute |
Technical view
The integration spans four packages. There is no mobile UI — clinic-mobile only carries the shared GraphQL documents, the permission map, translations, and guards that disable existing actions.
| Package | Role |
|---|---|
packages/server | 12 mutations, 7 queries, a cron job, encryption + validation utilities, permission rules |
packages/clinic-web-canary | All six DHS UI feature modules (React + TypeScript) |
packages/clinic-web | Host app — mounts the canary modules, adds the Approvals tab, status popover, routes, permission-group tab |
packages/prisma | Three new models, DHS fields on six existing models, eight migrations |
The three outbound APIs
Configured by environment variable, resolved at module load, and fail-fast — a missing variable throws before the process starts (packages/server/src/utils/dhsConfig.js:1-11).
| Variable | Used for |
|---|---|
DHS_AUTH_URL | POST /api/Login — exchange client secret for a bearer token |
DHS_ELIGIBILITY_URL | POST /api/v1/Eligibility/CheckInsurance, POST /api/v2/Eligibility/Checkeligibility |
DHS_PREAUTH_URL | POST /api/v1/Preauth/{SubmitApprovalRequest,GetApprovalResponse,CancelApproval} |
ENCRYPTION_MASTER_KEY (64 hex chars) is also required, and is validated at import time (packages/server/src/utils/encryption.js:6-14).
The data flow, end to end
Clinic saves client secret
└─ validated against /api/Login, then AES-256-GCM encrypted into DHSIntegration
Receptionist: Check Insurance (national ID)
└─ checkInsuranceEligibility → DHS /api/v1/Eligibility/CheckInsurance
└─ resolveInsuranceCoverages → "do we already have this insurer/policy/class?"
└─ wizard creates the missing InsuranceCompany / InsurancePolicy / PolicyClass
Receptionist: Check Eligibility
└─ dhsCheckEligibility → DHS /api/v2/Eligibility/Checkeligibility
└─ writes DHSEligibilityCheck, points Patient.currentEligibilityId at it
Dentist: select operations → GET Approval
└─ dhsApprovalSubmission → DHS /api/v1/Preauth/SubmitApprovalRequest
└─ writes DHSApproval (status PENDING), links Operations, stamps Operation.preAuth
Every minute: dhsApprovalsCron
└─ DHS /api/v1/Preauth/GetApprovalResponse for up to 100 PENDING approvals
└─ maps payer status → DHSApprovalStatus
└─ writes Operation.approved and Operation.insuranceValue
Every write is scoped by companyId, and every DHS GraphQL field is gated by both a permission and the feature flag — see Permissions, Tenancy & Rollout.
Where to start reading
- The wizard the clinic sets up first: Setup & Secret Handling
- The receptionist's daily flow: Insurance Discovery then
- The core of the feature: Pre-Auth Approvals Lifecycle
- What it does to the rest of the app: Operations & Invoicing Guards
- Real screens from the branch sandbox: Feature Tour
- Things that are genuinely not finished: Known Gaps
A note on honesty
These pages describe what the code on this branch does, verified by reading /work/repo and by driving the branch sandbox. Where the PR description and the code disagree, the code wins and the discrepancy is called out. The most notable one:
The PR description lists permissionsMANAGE_DHS_INTEGRATION,MANAGE_DHS_APPROVALSandCHECK_INSURANCE. Those names exist only as orphaned values in the Postgres enum from migration20260615152553_dhs_permissions. The permissions the code actually enforces are nine finer-grained ones — a repo-wide search forMANAGE_DHSreturns zero hits. See Permissions, Tenancy & Rollout.