Dentolize · DHS (NPHIES) Integration Walkthrough
On this pageAbout the sandbox's limits — read this first1. Setup: the DHS Integration wizard2. Before and after: what a branch NPHIES code unlocks3. Eligibility: the "Fill Missing Fields" pre-flight4. The Approvals tab5. Submitting a pre-authorization6. Configuration surfaces7. One route that isn't finished

Feature Tour (screenshots)

Every screenshot below was captured from the branch sandbox built from this PR (maysara-dhs-integration.sandbox.anastawfik.com, commit 5226604), signed in as the owner roster user on the seeded Sandbox Dental clinic.

About the sandbox's limits — read this first

The sandbox has no valid DHS client secret, because DHS credentials are issued per real provider and cannot be minted for a test environment. That has two consequences for this tour:

  1. Anything that requires the exchange to answer — a real coverage list, a real

eligibility verdict, a live approval number — could not be produced. Those flows are documented from the code in the In depth pages.

  1. Everything up to the outbound call was exercised, including the real error paths.

Those are shown here, because they are exactly what a clinic sees on a bad credential.

To reach the patient-side screens at all, one setup step was completed through the app's own updateBranchesNphiesCodes mutation: the Riyadh Clinic branch was given the NPHIES code SANDBOX-PR-001. Without it every patient-side DHS control stays hidden — see below.


1. Setup: the DHS Integration wizard

Settings → Integrations → DHS Integration. The tab only exists when the feature flag is on for the company and the user holds View DHS Integration.

Step 1 of the DHS setup wizard: a three-step progress bar reading Add Client Secret, Load Branches, Confirm & Save, with a masked Client Secret field and a Save & Test Connection button
Step 1 of the DHS setup wizard: a three-step progress bar reading Add Client Secret, Load Branches, Confirm & Save, with a masked Client Secret field and a Save & Test Connection button

Steps 2 and 3 are greyed out — they stay locked until a secret has been saved (DHSIntegrationSettings.tsx:54-58,69-70).

The bad-credential path

Entering an invalid secret and pressing Save & Test Connection leaves the wizard exactly where it was:

The same wizard after a failed connection test, still on step 1, with no visible error message on the page
The same wizard after a failed connection test, still on step 1, with no visible error message on the page

The server did respond, and its message is precise. Captured from the GraphQL response:

{
  "saveDHSIntegration": {
    "success": false,
    "message": "Authentication failed: client secret rejected by DHS (HTTP 204)"
  }
}

That wording comes from dhsErrorUtils.js:37-42, which special-cases an empty upstream body with HTTP 204/401/403 into a "client secret rejected" message rather than a bare fallback. Nothing was written to the database — saveDHSIntegration validates before it persists.

Support note: the message is delivered as a transient antd toast. It is easy to miss, and it had already dismissed itself by the time this frame was captured. If a clinic reports "nothing happens when I save the secret", the connection test is almost certainly failing.


2. Before and after: what a branch NPHIES code unlocks

This is the single most useful thing to understand about the feature's visibility rules.

Before — the branch has no NPHIES code. The patient profile shows the new National ID field, but no DHS controls at all:

Patient profile for an insured patient, showing action buttons New Appointment, New Patient Reminder, Add Balance, Quick Invoice, Send SMS, Search and Edit Patient — with no DHS buttons
Patient profile for an insured patient, showing action buttons New Appointment, New Patient Reminder, Add Balance, Quick Invoice, Send SMS, Search and Edit Patient — with no DHS buttons

After — the same screen once the branch has an NPHIES code. Two new buttons appear on their own row:

The same patient profile now showing Check Eligibility and Check Insurance buttons above the patient detail table
The same patient profile now showing Check Eligibility and Check Insurance buttons above the patient detail table

The gate is hasDhsFeature && branchData?.branchDetails?.nphiesCode (PatientProfile.js:147), repeated on six other surfaces. Check Insurance is greyed here because the patient's identifier type was not yet set — the tooltip explains it (DHSCheckInsurance.tsx:63-68).


3. Eligibility: the "Fill Missing Fields" pre-flight

Pressing Check Eligibility on a patient with gaps in their record opens this before it calls anything:

A modal titled Fill Missing Fields, with an editable Insurance company nphies code field at the top, then First Name, Last Name, Identifier Type, National ID, Gender, Marital Status, Occupation, Policy Number, Coverage Type and Date of Birth — most greyed out and pre-filled
A modal titled Fill Missing Fields, with an editable Insurance company nphies code field at the top, then First Name, Last Name, Identifier Type, National ID, Gender, Marital Status, Occupation, Policy Number, Coverage Type and Date of Birth — most greyed out and pre-filled

Everything the patient record already holds is disabled and shown for confirmation only (DHSCheckEligibility.utils.ts:125-127). Here only three fields are editable: the insurer's NPHIES code, Marital Status and Occupation.

Filled in:

The same modal with Married selected for Marital Status and Doctor for Occupation
The same modal with Married selected for Marital Status and Doctor for Occupation

The important consequence

Pressing OK runs updatePatientDetails first and the eligibility check second. The eligibility call failed here (no client secret), but the patient record was still updated — Marital Status now reads Married and Job reads Doctor:

The patient profile after the modal was submitted, with Marital Status showing Married and Job showing Doctor, and no eligibility status tag
The patient profile after the modal was submitted, with Marital Status showing Married and Job showing Doctor, and no eligibility status tag

No eligibility tag appeared, because no DHSEligibilityCheck row was created. This is the documented sequence in useDHSCheckEligibility.ts:281-307 — the clinic keeps the data-quality improvement even when the exchange is unreachable. Trainers and support agents should state this explicitly; it surprises people.

The error itself does surface correctly. Captured from the live DOM while the toast was on screen:

DHS Integration not configured or secret missing

That string originates at dhsAuthentication.js:31-33.


4. The Approvals tab

Patient → Chart → Approvals. Empty on the sandbox, but the structure is the real one:

The patient chart with the dental chart above and an Approvals sub-tab selected below, showing a search box, a date range picker, and columns Approval Number, Approval Status, Response Date, Manual Updated, Created, Last Updated, with a No data placeholder
The patient chart with the dental chart above and an Approvals sub-tab selected below, showing a search box, a date range picker, and columns Approval Number, Approval Status, Response Date, Manual Updated, Created, Last Updated, with a No data placeholder

Six columns, exactly as defined in Approvals.js:133-181. The table is read-only by construction — hasEdit={false}, newButton={null}, DrawerForm={null} (:188-203). The only controls are the search box (which searches approval numbers) and the date range.

Row actions live in the hover popover on the status tag, not in the table.


5. Submitting a pre-authorization

The entry point is in the chart's operations footer. Note the count in the button label:

A chart operations row for a Composite filling procedure at 500, with the footer showing a green Invoice (1) button and a blue GET Approval (1) button
A chart operations row for a Composite filling procedure at 500, with the footer showing a green Invoice (1) button and a blue GET Approval (1) button
Tester's trap: every operation seeded into the sandbox already had an invoice, and selectedForDHS requires !op.invoice (ChartTable.js:264-280). GET Approval was disabled on every patient until a fresh, un-invoiced procedure was added to the chart.

Step 1 — Patient Info

Step 1 of the DHS Approval Submission wizard showing a six-step bar (Patient Info, Request Info, Encounter Info, Diagnosis, Services, Review) and mostly disabled pre-filled fields, with Membership Number the only empty required field
Step 1 of the DHS Approval Submission wizard showing a six-step bar (Patient Info, Request Info, Encounter Info, Diagnosis, Services, Review) and mostly disabled pre-filled fields, with Membership Number the only empty required field

Six steps, not seven — the Attachments step only appears when the selected operations carry files (DHSApprovalSubmissionModal.tsx:168-173). Note how much is pre-filled and locked; only Membership Number needed input.

Step 2 — Request Info

Step 2 showing Visit Reason, Transaction Type, Claim Preauth Request Type and Priority dropdowns
Step 2 showing Visit Reason, Transaction Type, Claim Preauth Request Type and Priority dropdowns

Several of these lists are deliberately trimmed to a single option, with the rest commented out in DHSApprovalSubmission.utils.ts:4-25 "until their flows are implemented" — Transaction Type offers only New Approval, Request Type only Dental.

Step 3 — Encounter Info and Practitioner

Step 3 showing Encounter Class, Encounter Status, Service Event Type, Admission Date, Discharge Date, and a Practitioner block with Name, SCFHS License, Speciality Code 08.26 and Speciality Name General
Step 3 showing Encounter Class, Encounter Status, Service Event Type, Admission Date, Discharge Date, and a Practitioner block with Name, SCFHS License, Speciality Code 08.26 and Speciality Name General

The practitioner picker is a server-searched select filtered to doctors, which auto-fills the SCFHS licence and speciality on selection (EncounterInfoStep.tsx:161-171). Speciality defaults to 08.26 / General. Admission and discharge dates cross-validate against the service dates (:86-116).

Step 4 — Diagnosis

Step 4 showing an Add Diagnosis button and a Medical Details section with Chief Complaint, Investigation Result, Treatment Plan, Patient History, Physical Examination and History of Present Illness
Step 4 showing an Add Diagnosis button and a Medical Details section with Chief Complaint, Investigation Result, Treatment Plan, Patient History, Physical Examination and History of Present Illness

Adding a diagnosis reveals a Code / Type / Description row:

The Diagnosis step with one diagnosis row added, showing Code, Type and Description fields, and the Medical Details fields filled in
The Diagnosis step with one diagnosis row added, showing Code, Type and Description fields, and the Medical Details fields filled in

The code field searches the global ICD-10 conditions list. When nothing matches, it offers a deliberate escape hatch rather than dead-ending:

The diagnosis code dropdown showing a single option reading Use this code: K02.9
The diagnosis code dropdown showing a single option reading Use this code: K02.9

That is DiagnosisStep.tsx:77-94 — the typed string is injected as both code and name. At least one diagnosis is required to leave the step (:42-50).

Step 5 — Services

Step 5 showing a services table with columns Service Code, Description, Tooth #, Quantity, Unit Price, Discount Amount, VAT (15%) and Service Type, containing one row: r1, Composite, tooth 12, quantity 1, unit price 500, discount 0, VAT 75, service type SFDA
Step 5 showing a services table with columns Service Code, Description, Tooth #, Quantity, Unit Price, Discount Amount, VAT (15%) and Service Type, containing one row: r1, Composite, tooth 12, quantity 1, unit price 500, discount 0, VAT 75, service type SFDA

Everything here is derived from the chart operation and read-only except Service Type. VAT is computed automatically — 500 × 15% = 75 — and recalculates whenever price, quantity or discount changes (ServicesStep.tsx:110-144).

Step 6 — Review

The Review & Submit step showing collapsible sections for Patient Information, Claim Preauth Request with an Immediate priority tag, and Encounter Details including the Practitioner block
The Review & Submit step showing collapsible sections for Patient Information, Claim Preauth Request with an Immediate priority tag, and Encounter Details including the Practitioner block

Everything is restated with the coded values translated back to labels, with all panels open by default (ReviewStep.tsx:210). Priority renders as a red tag when Immediate.

This tour stops here. Submitting would have fired a request at the DHS staging exchange with credentials that do not exist; the run was deliberately not completed.


6. Configuration surfaces

Insurance company NPHIES code

**Associates → Insurance Companies → company.** A new read-only row:

An insurance company detail page showing a NPHIES Code row with a dash value, next to a Disabled row
An insurance company detail page showing a NPHIES Code row with a dash value, next to a Disabled row

Editable on the company form (InsuranceCompanyForm.js:372-376). Note the asymmetry: addNewInsuranceCompany accepts both nphiesInsuranceCompanyId and nphiesCode, but editInsuranceCompany accepts only nphiesCode — the NPHIES insurer id is not editable after creation (schema.graphql:703-704).

The DHS permission tab

**Settings → Permission Groups → group → DHS Settings.** Shown here on the seeded Reception group, with everything off:

A permission group editor for the Reception group with a DHS Settings tab selected, listing seven permissions — View DHS Approvals, Check DHS Insurance, Check DHS Eligibility, Create DHS Approval, Create Manual DHS Approval, Cancel DHS Approval, Get DHS Approval — each with an unchecked Enable box
A permission group editor for the Reception group with a DHS Settings tab selected, listing seven permissions — View DHS Approvals, Check DHS Insurance, Check DHS Eligibility, Create DHS Approval, Create Manual DHS Approval, Cancel DHS Approval, Get DHS Approval — each with an unchecked Enable box

Seven granular permissions, one Enable box each (tableData.js:471-479). The remaining two — View and Edit DHS Integration — sit on the Settings tab under a DHS Integration row.

Unlike the sidebar and the integrations tab, this permission tab is not wrapped in a feature-flag check (Group.js:251), so it is visible to every tenant regardless of whether DHS is enabled for them.

7. One route that isn't finished

The sidebar registers Logs → Approvals at /logs/dhsApprovals (Sidebar.js:567-569, DashboardRouter.js:810-815). Navigating there on the sandbox gives:

A near-empty dashboard page showing only the text
A near-empty dashboard page showing only the text "DHS Approvals (stub — component not built on this branch)"

The route lazy-loads packages/clinic-web/src/components/dashboard/logs/dhsApprovals/DHSApprovals at DashboardRouter.js:399, and that directory does not exist in the repository — verified by listing packages/clinic-web/src/components/dashboard/logs/. The sandbox build substitutes the stub shown above so the bundle still compiles.

The per-patient Approvals tab (section 4) is the working surface on this branch. The detail route, /logs/dhsApproval/:approvalId, is implemented — it is the canary DHSApprovalDetail payload inspector, reachable by clicking an approval number in the Approvals tab. See Known Gaps.