Glossary & Data Model
Terms
NPHIES — National Platform for Health Information Exchange Services. Saudi Arabia's national exchange through which providers and payers transact eligibility, pre-authorization and claims.
DHS — the Motalabate Health platform that Dentolize actually talks to. It fronts NPHIES. Throughout the code and UI, "DHS" is the integration; "NPHIES" is the identifier scheme.
NPHIES code / provider code — identifies a place of care. In Dentolize this lives on Branch.nphiesCode, not on the company. Every eligibility check and approval is filed under one branch's code, sent as the OrganizationCode header.
Payer — the insurer. Identified in requests by InsuranceCompany.nphiesCode.
NPHIES insurer id — InsuranceCompany.nphiesInsuranceCompanyId, distinct from nphiesCode. Part of the composite unique [companyId, nphiesInsuranceCompanyId] used to recognise an insurer returned by discovery. It cannot be edited after creation — editInsuranceCompany accepts only nphiesCode (schema.graphql:703-704).
Client secret — the clinic's DHS credential, exchanged at /api/Login for a bearer token. Stored encrypted; only a ****#### mask is ever returned.
Coverage — one insurance arrangement for one person: insurer + policy + class + limits + dates. Discovery may return several.
Eligibility check — a point-in-time answer to "is this person covered right now?" Recorded as a DHSEligibilityCheck; the latest is pointed at by Patient.currentEligibilityId.
Pre-authorization (pre-auth / approval) — asking the payer to commit, before treatment, to covering specific services. Recorded as a DHSApproval. Not the same as a claim.
Approval number — the payer's identifier for a pre-auth. Also stamped onto each linked Operation.preAuth.
Iqama — Saudi residency permit. Its number is a 10-digit identifier starting with 2 (national IDs start with 1). In the DHS mappings iqama and residency are treated as the same thing.
SCFHS — Saudi Commission for Health Specialties. The practitioner's licence number, required on every pre-auth.
Deductible rate — the share the patient pays. Dentolize's insurancePercentage is the inverse: a 20% deductible becomes 80% cover (DHSCheckInsurance.utils.ts:24-33). Returned by DHS as a string.
Operation — Dentolize's term for a single planned or performed treatment on the chart. What becomes a "service line" in a pre-auth.
insuranceDiscount: false — the "not subject to insurance" flag on an operation. Such operations are excluded from DHS submission, client- and server-side.
Enums
DHSApprovalStatus
schema.prisma:5602 · enums.graphql:1304 — identical in both.
| Value | Meaning |
|---|---|
DRAFT | Created locally, never sent (the Prisma default) |
PENDING | With the payer |
APPROVED | Payer will cover it |
PARTIALLY_APPROVED | Some services approved — not cancellable |
DENIED | Declined (distinct payer code) |
REJECTED | Declined |
CANCELED | Withdrawn by the clinic |
ERROR | Upstream error, or an unrecognised payer status |
EligibilityStatus
schema.prisma:5613 — ELIGIBLE, NON_ELIGIBLE. There is no "unknown" state; anything not explicitly eligible is stored as NON_ELIGIBLE.
Permission — DHS values
schema.prisma:6348-6356 · enums.graphql:622-630:
VIEW_DHS_INTEGRATION EDIT_DHS_INTEGRATION VIEW_DHS_APPROVALS
CHECK_DHS_INSURANCE CHECK_DHS_ELIGIBILITY CREATE_DHS_APPROVAL
CANCEL_DHS_APPROVAL CREATE_MANUAL_DHS_APPROVAL GET_DHS_APPROVAL
Orphaned values. Migration20260615152553_dhs_permissionsaddedMANAGE_DHS_INTEGRATION,MANAGE_DHS_APPROVALSandCHECK_INSURANCEto the Postgres enum. They were superseded a day later by the nine above. Postgres cannot drop enum values, so they persist in the database type while being absent fromschema.prismaandenums.graphql. A repo-wide grep forMANAGE_DHSreturns zero hits. The PR description still names them.
Data model
DHSIntegration — schema.prisma:5541
One per company (companyId @unique). Holds the encrypted credential.
| Field | Type | Notes |
|---|---|---|
id | String | uuid |
companyId | String | @unique, cascade delete |
clientSecretCiphertext | Bytes? | AES-256-GCM |
clientSecretIv | Bytes? | 12 random bytes per encryption |
clientSecretAuthTag | Bytes? | |
secretUpdatedAt | DateTime | @default(now()) |
No plaintext column exists. The GraphQL type exposes only hasSecret, clientSecretMask and secretUpdatedAt (types.graphql:5468-5476).
DHSEligibilityCheck — schema.prisma:5555
| Field | Type | Notes |
|---|---|---|
patientId | String | cascade |
companyId | String | added by the 20260507 migration |
status | EligibilityStatus | @default(NON_ELIGIBLE) |
checkedAt | DateTime | @default(now()) |
responseDate | DateTime? | from the payer when supplied |
responsePayload | Json | required in Prisma, nullable in GraphQL |
insuranceCompanyId | String? | cascade |
currentFor | Patient? | back-relation of Patient.currentEligibility |
Indexes: [patientId], [companyId, patientId].
DHSApproval — schema.prisma:5575
| Field | Type | Notes |
|---|---|---|
operations | Operation[] | the treatments this covers |
patientId | String | cascade |
companyId | String | denormalised for tenant indexing |
approvalNumber | String? | the payer's identifier |
status | DHSApprovalStatus | @default(DRAFT) |
requestPayload | Json? | attachments already '[redacted]' |
responsePayload | Json? | verbatim payer response |
canceledAt / canceledById | DateTime? / String? | audit only |
responseDate | DateTime? | |
manualUpdateById | String? | source of truth for "manually updated" |
Indexes: [patientId], [companyId, status], [status, updatedAt] — the last one serves the cron's PENDING + orderBy: updatedAt scan.
Two load-bearing comments in the schema:
:5586—status === 'CANCELED'is the source of truth for "canceled?";canceledAt/By
carry audit information the status does not.
:5591—manualUpdateById != nullis the source of truth for "manually updated?"
DHS fields on existing models
| Model | Fields | Line |
|---|---|---|
Company | dhsIntegration, dhsApprovals, dhsEligibilityChecks | :263-265 |
User | canceledDHSApprovals, manualUpdatedDHSApprovals | :675-676 |
Patient | currentEligibilityId @unique (SET NULL), currentEligibility, dhsEligibilityChecks, dhsApprovals | :965-968 |
Branch | nphiesCode String? | :1104 |
InsuranceCompany | nphiesCode, nphiesInsuranceCompanyId, eligibilityChecks, @@unique([companyId, nphiesInsuranceCompanyId]) | :1281, :1301-1310 |
Operation | dhsApproval, dhsApprovalId String? (SET NULL) | :2334-2335 |
InsurancePolicy and PolicyClass have no DHS fields at all. They connect to DHS only indirectly, through InsuranceCompany.nphiesInsuranceCompanyId and the ResolveInsuranceCoverage response type. Their identity keys matter for resolution: InsurancePolicy.@@unique([insuranceCompanyId, policyNumber]) (:1349) and PolicyClass.@@unique([insurancePolicyId, name]) (:1383).
Migrations
Eight touch DHS.
| Migration | What it does |
|---|---|
20260506145429_add_unique_constraints_to_insurance_and_patient | Creates both enums, the three tables, and all the DHS fields on existing models. Original DHSApproval carried isCanceled and isManualUpdate booleans. |
20260507183115_add_companyid_to_dhs_models | add-nullable → backfill from patient.companyId → SET NOT NULL, with an orphan guard that RAISE EXCEPTIONs rather than coercing (lines 25–38). Adds the three tenant indexes. |
20260508120000_drop_dhs_redundant_status_flags | Backfills status='CANCELED' where isCanceled, then drops both boolean columns. No down-migration. |
20260609102732_add_insurance_company_to_eligibility_check | Adds DHSEligibilityCheck.insuranceCompanyId, FK ON DELETE SET NULL. |
20260614090102_add_insurance_company_to_eligibility_check | Same directory name, later timestamp. Recreates the FK as ON DELETE CASCADE. |
20260615152553_dhs_permissions | Adds the three coarse permissions later abandoned. |
20260616100525_add_dhs_permissions | Adds the nine that are actually used. |
20260616145425_refactor_dhs_permissions | Intentional no-op — comment-only. Its original SQL re-added an existing column and broke migrate deploy on fresh databases; kept to preserve ordering. |
GraphQL surface
Queries — schema.graphql:597-604
getDHSClientSecret: GetDHSClientSecretResponse!
dhsBranches: DHSBranchesResponse!
getInsuranceCompanyDhsInsuranceHierarchy(input: GetDhsInsuranceHierarchyInput!): GetDhsInsuranceHierarchyResponse!
resolveInsuranceCoverages(input: ResolveInsuranceCoverageInput!): ResolveInsuranceCoverageResponse!
getDHSApprovals(input: GetDHSApprovalsInput!): GetDHSApprovalsResponse!
totalDHSApprovals(input: TotalDHSApprovalsInput!): Int!
dhsApprovalDetails(dhsApproval: ID!): DHSApproval!
Mutations — schema.graphql:1378-1390
dhsAuthentication: DHSAuthenticationResponse!
saveDHSIntegration(input: SaveDHSIntegrationInput!): SaveDHSIntegrationResponse!
testDHSConnection: TestDHSConnectionResponse!
rotateDHSClientSecret(newSecret: String!): RotateDHSClientSecretResponse!
updateBranchesNphiesCodes(input: UpdateBranchesNphiesCodesInput!): UpdateBranchesNphiesCodesResponse!
checkInsuranceEligibility(input: CheckInsuranceEligibilityInput!): CheckInsuranceEligibilityResponse!
dhsCheckEligibility(input: DHSCheckEligibilityInput!): DHSCheckEligibilityResponse!
updatePatientDetails(input: UpdatePatientDetailsInput!): updatePatientDetailsResponse!
dhsApprovalSubmission(input: DHSApprovalSubmissionInput!): DHSApprovalSubmissionResponse!
dhsGetApproval(input: DHSGetApprovalInput!): DHSGetApprovalResponse!
dhsCancelApproval(input: DHSCancelApprovalInput!): DHSCancelApprovalResponse!
dhsManualUpdateOperations(input: DHSManualUpdateOperationsInput!): DHSManualUpdateOperationsResponse!
Note rotateDHSClientSecret and dhsApprovalDetails take bare scalars rather than input objects, unlike everything else.
Key response types
| Type | Line | Shape |
|---|---|---|
DHSApproval | types.graphql:5186 | Full row minus companyId, which is deliberately not exposed |
DHSIntegration | :5468 | hasSecret, clientSecretMask, secretUpdatedAt — never the bytes |
DHSInsuranceCoverage | :5508 | 22 nullable String fields; maxLimit and deductibleRate are strings, not numbers |
InsuranceEntityResolution | :5540 | { exists: Boolean!, id: String, nphiesCode: String } |
ResolveInsuranceCoverage | :5546 | The three InsuranceEntityResolutions — the only place policy/class meet DHS |
DHSEligibilityCheck | :5581 | No companyId; insuranceCompanyId as a raw id, not an object |
DHSCancelApprovalResponse | :5618 | Still exposes isCanceled, now derived from status |
Outbound DHS endpoints
| Purpose | Endpoint |
|---|---|
| Authenticate | POST ${DHS_AUTH_URL}/api/Login |
| Discover insurance | POST ${DHS_ELIGIBILITY_URL}/api/v1/Eligibility/CheckInsurance |
| Check eligibility | POST ${DHS_ELIGIBILITY_URL}/api/v2/Eligibility/Checkeligibility |
| Submit pre-auth | POST ${DHS_PREAUTH_URL}/api/v1/Preauth/SubmitApprovalRequest |
| Poll pre-auth | POST ${DHS_PREAUTH_URL}/api/v1/Preauth/GetApprovalResponse |
| Cancel pre-auth | POST ${DHS_PREAUTH_URL}/api/v1/Preauth/CancelApproval |
All carry Authorization: Bearer <token> and OrganizationCode: <branch.nphiesCode>.
Coding tables
Identifier type → systemTypeId (Check Insurance)
DHSIdentifierMappings.ts:76-96. There is no code 4; unknown → 1.
| Code | Types |
|---|---|
| 1 | national_id, iqama, residency |
| 2 | visa, visitor_id |
| 3 | passport |
| 5 | driver_license, border_number, displaced_person, other, OTHER |
Identifier type → eligibility / approval code
DHSIdentifierMappings.ts:22-43. Unknown → 7, logged, never thrown.
| Code | Types |
|---|---|
| 1 | iqama, residency |
| 2 | passport |
| 3 | national_id |
| 4 | visa, visitor_id |
| 5 | mrn |
| 6 | border_number |
| 7 | displaced_person, driver_license, other, OTHER |
DHS type → legacy patient type
DHSIdentifierMappings.ts:50-73, applied before updatePatientDetails. Pass-through: national_id, passport, driver_license, residency, OTHER. Mapped: other → OTHER, iqama → residency, visa | border_number | displaced_person → OTHER. Unmapped → OTHER, logged.
Other codings
DHSCheckEligibility.utils.ts (duplicated verbatim in DHSApprovalSubmission.utils.ts:167-198):
| Field | Mapping | Line |
|---|---|---|
| Gender | male=0, female=1, default 0 | :94-97 |
| Marital status | single=1, married=2, divorced=3, widowed=4, separated=5, default 1 | :101-110 |
| Occupation | healthcare=7, military=8, unemployed=9, retired=10, student=11, else 14 | :112-123 |
Hardcoded in the eligibility payload: subscriberRelated: 6 (Self), fallback NPHIES code 'INS-FHIRss', fallback DOB '2000-01-01', fallback names 'Unknown', fallback policy holder 'Holder'.
Hardcoded in the approval payload: vatPercentage: 15, deductible: 0, durationOfDrugSupply: 1, estimatedLengthOfStay: 1, specialityCode: '08.26', specialityName: 'General', exactly one visit dated now.
Protected fields
packages/server/src/utils/dhsFieldGuards.js:
| Set | Fields | Locked when |
|---|---|---|
PREAUTH_PROTECTED_FIELDS | preAuth | Always, every status |
GENERAL_PROTECTED_FIELDS | tooth, price, amount, doctorId, createdAt, diagnosis | Any status except CANCELED |
Arrays are compared order-insensitively as multisets; Dates by getTime(); only fields present in the incoming payload are checked.