Dentolize · ZATCA E-Invoice Rounding Fix Walkthrough
On this pageTermsKey code locationsData model notesThe worked example, once more

Glossary & Data Model

Terms

ZATCA — Saudi Arabia's Zakat, Tax and Customs Authority. Runs the mandatory e‑invoicing platform ("Fatoora") that Saudi businesses report invoices to.

E‑invoice — the structured XML document Dentolize builds from a clinic invoice and reports to ZATCA. Not a copy of the stored total; it is rebuilt from line items and its totals are recomputed.

Simplified invoice (B2C) — a patient‑facing sale reported to ZATCA, typically cleared with a QR code on the receipt. Built by reportSimplifiedInvoice.js.

Standard invoice (B2B) — an insurance/business invoice with buyer details, reported through the same platform. Built by reportStandardInvoice.js.

Taxable base / taxable amount — the amount VAT is charged on: (price − discount) × quantity, before tax.

VAT category — the tax bucket a line falls in: standard 15%, standard 5%, or zero‑rated / exempt (codes O/Z/E). Totals are aggregated per category.

BR‑CO‑17 — the EN16931 / ZATCA business rule that a VAT category's tax amount is the category's taxable base times the rate, rounded once. The fix aligns Dentolize's e‑invoice with this rule.

Aggregate‑then‑round vs round‑then‑aggregate — the crux of this fix. round(rate × Σ base) (correct, one rounding) vs Σ round(rate × baseᵢ) (old, can drift a cent on multi‑line invoices).

EGS unit — the "E‑invoice Generation Solution" identity that signs and submits e‑invoices to ZATCA, carrying the clinic's certificates and invoice counter.

Invoice counter / previous‑invoice hash — each e‑invoice is chained to the one before it via a counter number and the prior document's hash. Configured on the ZATCA Integration screen (invoiceCounterNumber, lastSubmittedInvoiceHash).

acceptWarning — a flag threaded through the calculation; it does not affect decimal precision. Rounding always uses Decimal(...).toFixed(2).

halalah — 1/100 of a Saudi Riyal (SAR); "a cent" throughout this documentation.

Key code locations

WhatWhere
The fix — document tax totalspackages/zatca/src/zatca/calc.tsconstructTaxTotal (:203), category VAT (:344‑364), Calc (:514)
Compiled output the server runspackages/zatca/lib/zatca/calc.js
Per‑line tax (unchanged)calc.tsconstructLineItemTotals (:126‑131)
App‑side invoice total (source of truth)packages/server/.../invoices/invoiceUtils.jscalculateInvoiceValues (:96, VAT :163, total :205)
Line‑item builder for ZATCApackages/server/src/services/eInvoices/invoiceCalculator.js (:300‑364; per‑unit discount :313)
Simplified reporterpackages/server/src/services/eInvoices/reportSimplifiedInvoice.js (:129)
Standard reporterpackages/server/src/services/eInvoices/reportStandardInvoice.js (:145)
Line‑item typespackages/zatca/src/zatca/templates/simplified_tax_invoice_template.ts (:180‑202)

Data model notes

Invoice (packages/prisma/schema.prisma, model at :2620)

Monetary fields relevant here are stored as Float (not fixed‑point Decimal):

FieldLineType
total2622Float
tax2623Float
taxPercent2624Float
discount2625Float
subtotal2629Float
insurance2633Float

A few settlement fields are Decimal(15,2)paid (:2632), balance (:2642), paidDiagnosticFee (:2628). The mix of Float and Decimal is the root reason sub‑cent artifacts exist; converting the monetary fields to Decimal is a noted long‑term follow‑up, not part of this PR.

Operation (model at ~:2233)

Line‑level monetary fields (price, total, amount, discount, tax, insurance, …) are likewise Float; only paid is Decimal(15,2).

Per‑company ZATCA configuration

  • EInvoiceSettings (:6993‑7000) — autoSubmit (auto‑submission toggle), 1:1 with Company.
  • ZatcaCredentials (:7173‑7188) — isConfigured (onboarded flag), csr, privateKey, compliance (sandbox) and production certificate/secret pairs, lastSubmittedInvoiceHash, invoiceCounterNumber.
  • Company.eInvoiceProvider (:6942) — defaults to "ZATCA".
  • Sandbox vs production is chosen by environment (process.env.XLZ_ENV), not per company.

The worked example, once more

Two lines, 15% VAT, taxable base 100.10 each:

MethodTotalVAT
App (aggregate VAT)230.2330.03
Old ZATCA (per‑line VAT)230.2230.02
New ZATCA (category VAT)230.2330.03

After the fix, the ZATCA column equals the app column — by construction.