Document Calculations Module
Overview
Shared calculation engine for all financial document types (Sale, Order, Quote, Bill). Provides a single source of truth for tax computation, line item recomputation, document total aggregation, validation, and currency conversion.
Location: apps/backend/src/document-calculations/
This module has no HTTP endpoints — it is a pure service module consumed by other document modules.
Architecture
document-calculations/
├── document-calculations.module.ts # NestJS module (provides + exports LineItemTaxService)
├── application/
│ ├── line-item-tax.service.ts # Core calculation service
│ └── __tests__/
│ └── document-totals-consistency.spec.ts
└── domain/
├── index.ts # Barrel export for domain types
├── tax.types.ts # TaxItem, TaxBreakdownItem, LineItemRecomputedResult
├── document-line-item.types.ts # DocumentLineItemShape, DocumentTotalsShape
└── document-calculation.errors.ts # DocumentTotalMismatchError
No infrastructure layer — the module performs pure calculations with no persistence.
Domain Concepts
TaxItem
A tax definition from product.taxes.items. Contains rate, type (percentage or fixed), and optional name/code.
TaxBreakdownItem
Computed tax breakdown for a single tax on a line item. Includes taxable amounts, tax amounts, and their base currency equivalents.
DocumentLineItemShape
Canonical line item contract used by all document types. Ensures the frontend ItemsTotals component works identically across Sale, Order, and Quote views.
DocumentTotalsShape
Header totals contract: totalAmount, totalBaseAmount, optional subtotal, taxAmount, cartDiscountTotal.
Key Service Methods
| Method | Purpose |
|---|---|
extractTaxItems(taxes) | Parse tax definitions from product JSON |
extractTaxItemsFromProduct(product) | Extract taxes from a product object |
calculateTaxAmountMajor(taxes, amount, qty) | Total tax in major units (tax-inclusive) |
recomputeItemAmountAndTaxes(taxItems, price, qty, rate, discount) | Full line item recomputation with tax breakdown |
validateHeaderTotalMatchesLineTotals(items, total, opts) | Invariant check before persistence |
computeDocumentTotals(items, rate, cartDiscount) | Aggregate line totals to document level |
recomputeTaxBreakdownFromGrossAmount(amount, origGross, origTax) | Derive taxes from stored snapshots |
computeGrossTotalFromOrderItems(items, minorUnit) | Sum order items (excludes voided/comped) |
toMinorUnit(amount, minorUnit) | Major to minor unit conversion (BigInt) |
toMinorUnitNumber(amount, minorUnit) | Major to minor unit conversion (number) |
Consumer Modules
| Module | Import | Usage |
|---|---|---|
| Sales | DocumentCalculationsModule | Tax enrichment, discount recomputation, total validation |
| Orders | DocumentCalculationsModule | Order pricing, bill splits, payment totals |
| Quotes | DocumentCalculationsModule | Quote item enrichment, totals, conversion to sale |
| App (root) | DocumentCalculationsModule | Global availability |
Tax-Inclusive Pricing Formula
lineGross = quantity × unitPriceFinal
taxableAmount = lineGross / (1 + taxRate)
taxAmount = lineGross - taxableAmount
grandTotal = sum(lineGross) // do NOT add tax on top
Header Total Invariant
Before persisting any document:
expectedTotal = sum(lineGrossAmounts) - cartDiscountTotal
headerTotal must equal expectedTotal (within rounding tolerance)
Call validateHeaderTotalMatchesLineTotals() before create/update when the client sends a header total.