Dentolize · DHS (NPHIES) Integration Walkthrough
On this pageWhat this isWhy it mattersScope of the changeRisk assessmentTest coverageRecommended gates before widening the betaWhat success looks like

For Stakeholders

What this is

PR #119 adds a NPHIES integration to the clinic product, via the DHS (Motalabate Health) platform. It lets a clinic discover a patient's insurance, verify their cover, and obtain a pre-authorization for planned treatment — all inside Dentolize, instead of a payer portal.

It is unreleased, gated behind FEATURE_DHS_INTEGRATION with a beta-only rule.

Why it matters

Insured patients are the majority of revenue for Gulf dental clinics, and the insurance workflow is currently manual, duplicated, and error-prone. The measurable outcomes:

  • Time. Discovery, eligibility and pre-auth stop requiring a second system and manual

re-keying.

  • Revenue leakage. The system now refuses to invoice a treatment whose pre-auth is still

pending, and freezes the treatment's price, tooth and diagnosis while the payer decides. This closes a class of loss that clinics currently absorb.

  • Data quality. Insurer, policy and class records are created from the payer's own data

rather than typed by a receptionist.

  • Market access. NPHIES connectivity is becoming table stakes for Saudi clinic software.

Scope of the change

MetricValue
Files changed~260 (excluding three large seed-data JSON files)
New GraphQL operations12 mutations, 7 queries
New database models3 (DHSIntegration, DHSEligibilityCheck, DHSApproval)
Migrations8 DHS-related
New permissions9
New front-end feature modules6 (all in clinic-web-canary)
New background job1, on a one-minute schedule
Languages translated11

The change reaches beyond new screens: existing operation, invoice and quotation flows gained guards, and the patient form gained identity fields NPHIES requires.

Risk assessment

Handled well

Credential security. The DHS client secret is validated against the exchange before it is stored, encrypted with AES-256-GCM using a per-encryption random IV, and never returned over the API — only a ****#### mask. Rotation is a distinct mutation that only replaces the old secret once the new one is proven. This is materially better than the industry norm of plaintext payer credentials.

Tenant isolation. Every DHS query and mutation carries a permission plus the feature flag; four mutations carry an additional ownership rule. Writes are companyId-scoped, and the submission path asserts a row count and rolls back rather than trusting client-supplied ids. companyId was denormalised onto the DHS models specifically for tenant indexing.

Migration safety. The companyId denormalisation was rewritten as add-nullable → backfill-from-patient → set-NOT-NULL, with an explicit guard that raises rather than silently coercing if any orphan rows remain (20260507183115_add_companyid_to_dhs_models, lines 25–38). The flag-removal migration backfills status = 'CANCELED' before dropping the boolean columns. Both are safe against environments that already hold data.

SSRF hardening. Attachment URLs are restricted to a single derived S3 host over HTTPS, with private-IP rejection performed after DNS resolution and maxRedirects: 0 — which is what makes the host check sound rather than decorative.

Blast radius when off. With the flag disabled a company is behaviourally identical to pre-PR: the guards check the flag before blocking anything, so invoicing is never impeded.

Reversibility. Rollout is per-company via a flag rule. Disabling is a data change, not a deploy.

Genuine risks

1. Reconciliation hazard on submission rollback — medium. If the payer accepts a submission but the local transaction rolls back (the operation-count assertion at dhsApprovalSubmission.js:305-312), the approval exists upstream and not locally. The user sees a failure and will likely resubmit, potentially duplicating the claim at the payer. Support has been told to escalate this specific error string. Mitigation to consider: a reconciliation query against the payer before allowing resubmission.

2. No HTTP timeouts on the DHS API calls — medium. Only the attachment download and the cron's poll carry a timeout. /api/Login, CheckInsurance, Checkeligibility and SubmitApprovalRequest have none. A hung upstream holds a request thread until the platform's own limits intervene. Low probability, but the fix is one line per call site.

3. Encryption key rotation is unsupported — medium, latent. ENCRYPTION_MASTER_KEY is used directly with no key-version field and no re-encryption job. Rotating it silently invalidates every stored client secret; the symptom would present as "no secret configured" rather than an obvious error. This is documented in code and in .env.example, but it is an operational landmine that should be scheduled before the key is ever rotated in anger.

4. One route ships broken — low severity, high visibility. The sidebar entry Logs → Approvals points at a component that does not exist in the repository (DashboardRouter.js:399). On the sandbox this renders a stub. It is cosmetic — the working per-patient Approvals tab covers the need — but it is on the main navigation and will generate tickets. Either build it or hide the sidebar entry before the beta widens.

5. Undocumented environment prerequisites — low, but a deploy-blocker. S3_BUCKET_AWS and S3_REGION_AWS are absent from .env.example, yet validateAttachmentUrl.js throws at import time without them. A deploy missing them fails at boot rather than gracefully. One-line fix.

6. Dependence on a third party. Availability and correctness now partly depend on the DHS platform. The design degrades reasonably — approvals stay pending and retry, and a manual path exists — but expectation-setting with beta clinics matters.

7. Cron lock is not value-checked — low. The Redis lock is released with an unconditional DEL. A tick overrunning the 600-second TTL would delete a successor's lock. With a 100-row cap and 5-way concurrency, overrun is unlikely.

Documentation debt

The PR description does not match the code in two places, and should be corrected before it becomes the historical record:

  • It names permissions MANAGE_DHS_INTEGRATION, MANAGE_DHS_APPROVALS, CHECK_INSURANCE.

The code implements nine finer-grained permissions instead; a repo-wide search for MANAGE_DHS returns zero hits. The coarse names survive only as orphaned Postgres enum values.

  • It describes auth-token caching in Redis as a cron improvement. That is accurate **for the

cron**, but the five request-path resolvers each perform a fresh /api/Login per call — redisClient is threaded into dhsAuthentication and never used. This is a latency and rate-limit consideration, not a correctness one.

Test coverage

Unit tests cover the two pure mapping utilities well:

FileCoverage
dhsStatusMapping.test.js18 cases including nullish, unknown and type-coercion
dhsErrorUtils.test.js10 cases, exact expected strings, incl. the 204/401 empty-body path
dhsFieldGuards.test.js8 cases incl. CANCELED unlock and order-insensitive diagnosis

There is no automated coverage of the resolvers, the cron, or the front-end wizards. Given that the cron writes financial amounts onto operations, that is the most valuable gap to close. See For Quality for the manual test matrix.

  1. Fix the Logs → Approvals route (build it or hide the nav entry).
  2. Add S3_BUCKET_AWS / S3_REGION_AWS to .env.example.
  3. Add timeouts to the four un-timed DHS calls.
  4. Correct the PR description's permission list.
  5. Agree an operational runbook for the submission-rollback reconciliation case.
  6. Write down the encryption-key rotation procedure — even if the answer is "don't".

None of these are architectural. The design is sound; these are finishing items.

What success looks like

Instrument the beta cohort for these, ideally against a pre-rollout baseline:

  • Minutes per pre-auth, portal versus Dentolize.
  • Count of invoices raised against un-approved treatments (should go to zero).
  • Pre-auth rejection rate attributable to data-entry error.
  • Share of approvals resolved by the background poll versus manual update — a high manual

share means the integration is not carrying its weight.

  • Time from submission to recorded payer answer.