Dentolize · Quotation Tax Fix & New Design Walkthrough
On this pageWhat to testEdges likely to be under-tested

For Quality

What to test

Tax calculation

  • Patient with no insurance: tax should equal the old flat-rate calculation, (subtotal - discount) * patientTaxPercent / 100.
  • Patient with insurance where insurance company tax rate == patient tax rate: total should match the flat-rate calculation exactly — this is the "equal rates" collapse path (quotationUtils.js per-op branch, QuotationDrawer.js:379).
  • Patient with insurance where insurance company tax rate != patient tax rate: tax should be the sum of insurance-covered part × insurance rate and patient-paid part × patient rate, computed separately — verify against manual arithmetic, not just "the number changed."
  • Mixed quotation: some procedures insured, some not, within the same quotation — confirm only the insured lines get the insurance-rate treatment.
  • A procedure with taxApplied: false should contribute 0 tax regardless of insurance status.
  • Manually typing into the quotation-level Tax or Tax% field should force patientTaxPercent === insuranceTaxPercent going forward for that quotation (QuotationDrawer.js:621-622, 653-654) — confirm the per-line split visibly collapses and stays collapsed after save/reload.
  • Editing a procedure's discount via the Discount modal should recompute that line's insurance amount, tax, and total, and the quotation-level total should update via updateValuesFromLineItems.

Recalculation triggers (server-side)

handleUpdateQuotations in quotationUtils.js runs on several indirect triggers — each is worth its own test (confirmed call sites):

  • Converting a quotation to an invoice — packages/server/src/resolvers/mutations/actions/invoices/createNewInvoice.js.
  • Deleting an invoice that was converted from a quotation — packages/server/src/resolvers/mutations/actions/invoices/deleteInvoice.js.
  • Adding/removing operations on an existing invoice tied to a quotation — addOperationsToInvoice.js, removeOperationFromInvoice.js.
  • Editing or deleting chart operations referenced by a quotation — patient/saveOperations.js, patient/deleteOperations.js (e.g. changing a procedure's price or taxApplied flag after it's already on a quotation).
  • Confirm old quotations (pre-migration, patientTaxPercent/insuranceTaxPercent = 0) recalculate using the legacy flat taxPercent rather than treating 0% as a real insurance tax rate — this is the patientTaxPercent || topTaxPercent / insuranceTaxPercent || topTaxPercent fallback in quotationUtils.js (lines 33-34). A real insurance tax rate of exactly 0% is indistinguishable from "not set" in this logic — worth a dedicated edge-case test.

Insurance limit display — verified regression, not a pre-existing gap

This is new, not carried over from before: the diff shows the old code computed insuranceLeftToUse as patientDetails.insuranceLimit - totalInsuranceUsed where totalInsuranceUsed = patientDetails?.dailyInsurancePaid ?? 0. The new code drops that subtraction entirely — totalInsuranceUsed is now hardcoded to 0 (QuotationDrawer.js:1319), and the mobile insuranceLeftToUse (NewQuotationScreen.js:144) is likewise just Math.max(insuranceDetails.insuranceLimit || 0, 0) with no usage subtracted.

  • Test: create a patient, use some of their daily insurance limit via an invoice, then open a new quotation for them — the quotation's "insurance remaining" should reflect the reduced amount but currently will not.
  • File this as a bug for a product/engineering decision rather than assuming it's intentional — see For Stakeholders.
  • Confirm the Invoice screen's equivalent figure is unaffected (out of scope for this PR) — if it also regressed, that's a more urgent escalation.
  • Test the Saudi-only max-limit checkbox: toggling it per procedure should not, by itself, recompute totals — verify this matches intended behavior rather than being an oversight.
  • The dropdown's only real option (value newDesign) is labeled "New Customer" in the UI, not "New Design" — it reuses the generic app.new translation key (en.json: "app": { "new": "New Customer" }), which is presumably meant for a customer/lead-registration flow elsewhere in the app. Confirmed live in the sandbox. This isn't a functional bug, but it's confusing enough to flag for a copy fix — file it rather than assume it's intentional.
  • With "Quotation Design" unset: printed quotation uses the legacy layout, unchanged.
  • With "Quotation Design" = New: verify the Insurance column only appears when at least one procedure has an insurance amount (hasInsurance check in NewQuotationDesign.js) — a fully non-insured quotation shouldn't show an empty Insurance column.
  • Verify per-procedure notes print correctly under their row, and rows respect the manually-set Order value from the editor.
  • Verify the Quotation Details free-text box only appears when text has been entered in branch settings, and respects the 2000-character limit.
  • Verify the printed tax line shows one combined figure even when the quotation has a patient/insurance split — confirm this is accepted as intentional scope for this PR (the split doesn't reach the printout) rather than a missed requirement.
  • Test RTL rendering (Arabic) — NewQuotationDesign.js has explicit .rtl styling branches for the table and signature blocks (lines 98-99, 165-166, 226, and the rtl class toggle based on lng === 'ar' at lines 358, 482, 497).
  • Multi-branch: confirm the setting is genuinely per-branch and doesn't leak to other branches.

Cross-platform parity

  • Mobile (NewQuotationScreen.js) reimplements the same tax-split logic independently rather than sharing code with web — run the same tax-split test matrix on mobile and confirm identical totals for the same input data.
  • Confirm a quotation created on mobile and edited on web (or vice versa) round-trips correctly. Note: mobile's internal field name for a line item's procedure ID is operationId while web's is id, but both serialize to the same id key in the persisted operationInsurance JSON (QuotationDrawer.js:1194-1208, NewQuotationScreen.js:960-975) — so this is an internal naming difference only, not expected to cause a cross-platform data bug, but still worth a round-trip test given how much of this logic was rewritten independently on each platform.

Unused plumbing to be aware of (not a bug, but don't assume it works)

pendingPaymentPercent is a declared GraphQL argument on both mutations and read server-side (patientMutations.js), but neither QuotationDrawer.js nor NewQuotationScreen.js sends it — confirm no test relies on setting a custom pending-payment percentage from either app, since there's currently no UI path to do so.

Edges likely to be under-tested

  • Insurance company taxPercent explicitly set to 0 vs. left unset — these should behave differently (explicit 0% insurance tax vs. "use patient rate") per the typeof ... === 'number' style check used when deriving insuranceTaxPercent, but it's an easy distinction to get wrong in manual testing.
  • A quotation edited enough times that its operationInsurance JSON mixes "legacy" entries (no priceDiscount) and "new" entries (priceDiscount present) — the legacy-fallback logic in the per-op build loop is a plausible source of subtle discount miscalculation if the two shapes are mixed within one quotation.
  • Floating-point rounding: the totals aggregator subtracts a small epsilon (~0.001) before rounding in places — worth a test with many low-value line items to confirm totals don't drift by a cent.