On this page
The first question, alwaysTriage decision treeEscalate to engineering whenUseful facts to have to handFor 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"
- Does the user have View DHS Integration? (Settings → Permission Groups → group →
Settings tab → DHS Integration row.)
- Is
FEATURE_DHS_INTEGRATIONenabled 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:
| Message | Meaning |
|---|---|
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:
| Toast | Cause | Fix |
|---|---|---|
DHS Integration not configured or secret missing | No secret stored for the company | Complete step 1 of the wizard |
Branch NPHIES code is not configured | Branch unmapped or the branch belongs to another company | Complete step 2 |
Feature not enabled | Feature flag off for this company | Escalate — flag change |
Date of birth cannot be in the future | Bad DOB in the pre-flight modal | Correct 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:
| Exclusion | Why |
|---|---|
| It already has an invoice | Approvals are pre-treatment; invoiced work is past that |
| Its approval is PENDING or APPROVED | Already 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):
| Refusal | Fix |
|---|---|
| Grouped teeth (one operation, several teeth) | Split into one operation per tooth |
| Quantity greater than 1 | Split into separate operations |
| Mixed branches | Submit one branch at a time |
| No branch on the operations | Data 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"
- Hover the status tag → Check Status. That forces a poll now.
- 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.
- 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
| Fact | Value |
|---|---|
| Poll interval | Every minute |
| Approvals per poll | Up to 100, oldest-updated first |
| Auth token cache | 55 minutes, per company |
| Attachment size limit | 10 MiB per file |
| Attachment formats | JPEG, PNG, PDF only |
| Attachment source | Must be the clinic's S3 bucket over HTTPS |
| VAT on services | Fixed 15%, computed, not editable |
| Approved amounts | Rounded to whole units |
| Secret display | Only ever **** + last 4 characters |