On this page
What this isWhy it mattersScope of the changeRisk assessmentTest coverageRecommended gates before widening the betaWhat success looks likeFor 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
| Metric | Value |
|---|---|
| Files changed | ~260 (excluding three large seed-data JSON files) |
| New GraphQL operations | 12 mutations, 7 queries |
| New database models | 3 (DHSIntegration, DHSEligibilityCheck, DHSApproval) |
| Migrations | 8 DHS-related |
| New permissions | 9 |
| New front-end feature modules | 6 (all in clinic-web-canary) |
| New background job | 1, on a one-minute schedule |
| Languages translated | 11 |
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:
| File | Coverage |
|---|---|
dhsStatusMapping.test.js | 18 cases including nullish, unknown and type-coercion |
dhsErrorUtils.test.js | 10 cases, exact expected strings, incl. the 204/401 empty-body path |
dhsFieldGuards.test.js | 8 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.
Recommended gates before widening the beta
- Fix the
Logs → Approvalsroute (build it or hide the nav entry). - Add
S3_BUCKET_AWS/S3_REGION_AWSto.env.example. - Add timeouts to the four un-timed DHS calls.
- Correct the PR description's permission list.
- Agree an operational runbook for the submission-rollback reconciliation case.
- 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.