Dentolize · DHS (NPHIES) Integration Walkthrough
On this pageThe first question, alwaysTriage decision treeEscalate to engineering whenUseful facts to have to hand

For Support

Practical triage for DHS (NPHIES) tickets. Written against the code on this branch — every error string below is quoted from source.


The first question, always

"Which branch is the user working in, and does it have an NPHIES code?"

Roughly every "the DHS buttons are missing" ticket is this. The visibility rule is feature flag AND branch has an NPHIES code AND user has the permission, checked on seven separate surfaces (PatientProfile.js:147, ChartTabs.js:130, ChartTableFooter.js:407, PatientCommonFields.js:737, DHSStatusPopover.js:47, ChartTable.js:319, InvoiceExpendedRow.js:274).

Fix: Settings → Integrations → DHS Integration → step 2, fill in the branch's code, Submit Changes. The controls appear immediately on reload. This was reproduced exactly on the branch sandbox — setting one branch code made Check Eligibility, Check Insurance, the Approvals tab and GET Approval all appear at once.


Triage decision tree

"I can't see the DHS Integration tab in Settings"

  1. Does the user have View DHS Integration? (Settings → Permission Groups → group

Settings tab → DHS Integration row.)

  1. Is FEATURE_DHS_INTEGRATION enabled for the company? The tab is wrapped in the flag at

SettingsIntegrations.js:23,47-53.

If both are true and it is still missing, escalate.

"Saving the client secret does nothing"

The connection test is failing. The error is a transient toast that is easy to miss.

Ask engineering for the saveDHSIntegration response. The messages you will see:

MessageMeaning
Authentication failed: client secret rejected by DHS (HTTP 204/401/403)Wrong or expired secret. DHS returned an empty body.
Authentication failed: <payer text>DHS gave a reason — relay it.
Validation-shaped text (comma-joined)DHS rejected the request format. Escalate.

Nothing is saved on failure — saveDHSIntegration validates against /api/Login before it persists. There is no half-saved state to clean up.

"Check Insurance / Check Eligibility does nothing"

Watch for the toast. The common ones:

ToastCauseFix
DHS Integration not configured or secret missingNo secret stored for the companyComplete step 1 of the wizard
Branch NPHIES code is not configuredBranch unmapped or the branch belongs to another companyComplete step 2
Feature not enabledFeature flag off for this companyEscalate — flag change
Date of birth cannot be in the futureBad DOB in the pre-flight modalCorrect it

Note the second one is deliberately ambiguous: checkInsurance.js:28-45 returns the same message for "branch not found in your company" and "branch has no code". That is a tenant-isolation measure, not a bug.

"The Check Insurance button is greyed out"

DHSCheckInsurance.tsx:63-68 disables it unless all of these hold: the patient has a non-blank national ID, an identifier type, a branch, and the identifier type maps to a system type. The tooltip says "fill identifier and national ID". In the patient form it is additionally disabled while the National ID field has a validation error (PatientCommonFields.js:751).

"GET Approval is greyed out even though I selected treatments"

This is the highest-friction one. ChartTable.js:264-280 excludes an operation when any of these is true:

ExclusionWhy
It already has an invoiceApprovals are pre-treatment; invoiced work is past that
Its approval is PENDING or APPROVEDAlready with the payer
insuranceDiscount === false ("not subject to insurance")Matches the server guard

The button label shows the count of eligible operations, so GET Approval (0) means everything selected was excluded. On the branch sandbox every seeded operation was already invoiced, so the button was dead until a fresh procedure was charted — expect the same confusion in demos.

"The approval wizard won't let me continue"

Four pre-flight refusals happen before the wizard opens, each with a notification listing the offending operations (useDHSApprovalSubmission.tsx:56-134):

RefusalFix
Grouped teeth (one operation, several teeth)Split into one operation per tooth
Quantity greater than 1Split into separate operations
Mixed branchesSubmit one branch at a time
No branch on the operationsData problem — escalate

Inside the wizard, Next validates the whole form and only logs failures to the console (DHSApprovalSubmissionModal.tsx:73-80). If Next appears dead, a required field on an earlier step is invalid and its error is off-screen. Walk the user back through each step. Fields most often missed: Membership Number (step 1), SCFHS License (step 3), and the five Medical Details boxes plus at least one diagnosis (step 4).

If the ICD search finds nothing, the dropdown offers "Use this code: <what you typed>" — that is intentional (DiagnosisStep.tsx:77-94), not a broken search.

"An approval is stuck on PENDING"

  1. Hover the status tag → Check Status. That forces a poll now.
  2. If it flips to ERROR, the payer returned an error or an unrecognised status. Open the

approval detail (click the approval number) and read the response payload.

  1. If it stays PENDING, the background job may be skipping it. It only picks up approvals

with status = 'PENDING' and a non-null approval number, capped at 100 per minute, oldest first (dhsApprovalsCron.js:77-107). It also silently skips any approval whose branch or insurance company lacks an NPHIES code (:165-168) — no log, no error. Check both codes.

"The approval says ERROR"

ERROR means one of: the payer returned succeeded: false; the payer's status string wasn't recognised; or the request threw. The response payload is stored either way. Use Retry on the popover (needs Get DHS Approval).

Note ERROR is also the fallback for an unmapped status — see the mapping table in Approvals Lifecycle. A brand-new payer status string will land here.

"I can't cancel an approval"

Only PENDING and APPROVED can be cancelled (dhsCancelApproval.js:58-64). The error reads:

Cancellation is only allowed for PENDING or APPROVED statuses. Current status is <X>.

PARTIALLY_APPROVED is deliberately not cancellable. If a clinic needs to undo one, that is a product question, not a bug.

Cancelling zeroes insuranceValue and sets approved = false on every linked operation (:125-147), and unlocks the treatment fields for correction. The pre-auth number is kept forever as an audit trail.

"I can't edit the price / tooth on a treatment"

Expected. Once an approval exists and isn't cancelled, six fields are frozen: tooth, price, amount, doctorId, createdAt, diagnosis (dhsFieldGuards.js:3). The preAuth number is frozen in every status including cancelled (:2).

The error is app.cannotModifyDhsOperation. To unlock: cancel the approval.

"I can't invoice this treatment"

Expected while an approval is PENDING. The error is app.waitingApproval, raised by assertNoPendingDhsApproval from addOperationsToInvoice.js:16 and createNewInvoice.js:35. Quotation conversion is blocked the same way, with a tooltip.

Resolve the approval (or cancel it) and the block lifts.

"Manual Update is refusing"

This approval has already been finalized and cannot be manually updated.

Fires for APPROVED, REJECTED and CANCELED (dhsManualUpdateOperations.js:55-67). PENDING, PARTIALLY_APPROVED, DENIED, ERROR and DRAFT remain editable.

Also: duplicate operation ids in one submission always fail the count assertion (:46-51).

In the modal, the insurance value field is disabled unless the approved switch is on, and switching approved off force-resets the value to 0 (ManualUpdateModal.tsx:123-141). The discount column is displayed but permanently disabled (:149) — that is intentional; DHS never writes discount or tax.

"Logs → Approvals is blank / broken"

Known gap, not a bug to chase. The route exists in the sidebar and router but the page component was never built on this branch — DashboardRouter.js:399 imports components/dashboard/logs/dhsApprovals/DHSApprovals, which does not exist. Direct users to the per-patient Chart → Approvals tab instead. See Known Gaps.

"The eligibility badge disappeared"

Expected if the patient's insurance company was changed. The badge only renders when the stored eligibility's insuranceCompanyId matches the patient's current insurer (useDHSCheckEligibility.ts:67-83). A green Eligible from a previous insurer must not keep showing. Re-run the check.

"It updated the patient even though the check failed"

Expected, and worth explaining rather than apologising for. The Fill Missing Fields modal runs updatePatientDetails first, then the eligibility check (useDHSCheckEligibility.ts:281-307). If the exchange is unreachable the patient record still keeps the corrected marital status, occupation and so on.

Confirmed live on the branch sandbox: Marital Status and Job persisted while the eligibility call failed for want of a credential.


Escalate to engineering when

  • Any error you cannot map to the tables above.
  • An approval whose status disagrees with the payer portal.
  • Operation mismatch: Attempted to update N operations, but found M — this means a

submission was accepted by the payer but rolled back locally (dhsApprovalSubmission.js:305-312). The approval exists upstream and not in Dentolize. Always escalate; this needs manual reconciliation.

  • Any suspicion of cross-tenant data.
  • Update failed. Please try again. from the patient modal — this message deliberately hides

the real cause, which is only in the server log (updatePatientDetails.js:90-97).

Useful facts to have to hand

FactValue
Poll intervalEvery minute
Approvals per pollUp to 100, oldest-updated first
Auth token cache55 minutes, per company
Attachment size limit10 MiB per file
Attachment formatsJPEG, PNG, PDF only
Attachment sourceMust be the clinic's S3 bucket over HTTPS
VAT on servicesFixed 15%, computed, not editable
Approved amountsRounded to whole units
Secret displayOnly ever **** + last 4 characters