Numeric Input Limits
Requirement (2026-06): every numeric value sent from a client (FE/POS/BO) to a backend API is bounded. Out-of-bounds input is rejected with a clean HTTP 422 validation error — it must never reach the database (no
numeric overflow500s).
1. The Limits
| Kind | Applies to | Max | Decimals | DB equivalent |
|---|---|---|---|---|
| Monetary / value | amount, price, total, fee, discount value, balance, revenue, cost | 99,999,999,999.9999 | 4 | numeric(15,4) |
| Quantity / volume | quantity, qty, volume, stock count, uom conversion | 9,999.9999 | 4 | numeric(8,4) |
Rules:
| Rule | Behavior |
|---|---|
| Above max | 422 with field-level error message |
| More than 4 decimal places | 422 (input is rejected, not silently rounded) |
| Negative | 422 — all monetary/quantity inputs are non-negative unless the field explicitly documents otherwise |
NaN / Infinity / non-numeric | 422 |
| Exchange-rate fields | Keep their dedicated precision (numeric(12,6) / numeric(19,6)) but are bounded at the validation layer the same way |
2. Scope — where bounds are enforced
Bounds live in the Zod validation layer, via shared schemas in @nx/core (single source of truth — never re-declare per package). DB column precision is unchanged by this requirement.
Two input surfaces are covered — both must be swept per package:
- Explicit request schemas —
z.number()fields inmodels/requests/and controllerdefinitions.ts. - CRUD-generated schemas — entities with
decimal/numericcolumns (and numeric fields insidejsonb) exposed through generic CRUD controllers; the generated insert/update schema accepts unbounded values unless overridden.
3. Rollout by package
| Package | Surfaces | Status |
|---|---|---|
core | shared schemas + jsonb numeric constants (sale-item discount, ledger book schemas) | WK25 |
commerce | 18 CRUD controllers, lowStockThreshold, uom conversion ratios | WK25 |
sale | shift cash fields, order quantity, 14 CRUD controllers | WK25 |
inventory | purchase-order/voucher quantity & unit price (15 CRUD controllers) | WK25 |
pricing | fare/cost/tax/promotion-method amounts & quantities (9 CRUD controllers) | WK25 |
finance | voucher amount/exchangeRate, 5 CRUD controllers | WK25 |
invoice | invoice line quantity/price/amount/taxAmount | WK25 |
ledger | tax-tier revenueMin/Max, declaredAnnualRevenue | WK25 |
taxation | tax-group-item value (5 CRUD controllers) | WK25 |
helpdesk | compensation.amount | Pending — package build is broken; item created when repaired |
identity / signal / search / outreach / payment / asset / licensing | priority/pagination/count only — no monetary or quantity FE input found (audited 2026-06) | No action |
4. Boundary test matrix (for QA)
Run per endpoint that accepts a monetary or quantity field:
| # | Input | Expected |
|---|---|---|
| 1 | exactly 99999999999.9999 (money) / 9999.9999 (quantity) | 202/200 accepted |
| 2 | max + 0.0001 | 422, field named in error |
| 3 | value with 4 decimal places (e.g. 12.3456) | accepted |
| 4 | value with 5 decimal places (e.g. 12.34567) | 422 |
| 5 | -1 | 422 (unless field documents negative support) |
| 6 | 0 | accepted (unless field requires positive, e.g. voucher amount) |
| 7 | string "abc", null on required, 1e30 | 422 |
A DB error / HTTP 500 on any of these = bug — the bound is missing at the validation layer.