feat/tax-regime
needs attentionviewing older commit7f40d56 · incrementalPR #302reviewed 2026-07-13 16:38 UTC1H · 4M · 5L · 1I- Purpose
- Fix over-taxation of IVA Frontera Norte sites in calculated bills — computeBillShell hardcoded IVA at 16%, incorrectly taxing northern-border sites that owe only 8%.
- Goal
- Wire per-contract tax_regime (iva/iva_frontera) into the calculated-bill path so THOR, CFE baseline, and grid bills use the correct IVA rate, validated to the cent against WE LOVE BURGERS (16%) and PETCO (8%) management-account bills.
- Sub-goals
- SG-1: tax_regime column on utility_contracts + TAX_REGIMES / TaxRegime domain types + mapper/decisions
- SG-2: resolveSiteTariffContext returns taxRegime; computeBillShell applies it (dropping MX_IVA constant); bill-for-contract shell resolves it automatically
- SG-3: Move IVA rates from TAX_REGIME_RATES code constant to data-driven tax_rates catalog table (this commit)
- What
- This commit (SG-3) introduces the tax_rates catalog table (migration 0059), a TaxRateFCIS query module (findRateByCode / findAllRates), a seed script, and updates computeBillShell to look up the rate from the DB first with fallback to the code constant then to 0.16. Prior commits wired the tax_regime field through the utility-contract type, mapper, decisions, and billing shells.
- Why
- Moving rates to the DB makes new regimes (and future period-keyed rates) data-driven — seed rows, not code edits. Also adds the defensive triple-fallback so a missing/unseeded DB row never propagates NaN into a bill.
- Areas
- domains/cross-domain/src+36−4domains/utility/src+66−2packages/api/src+3−1packages/database+107−0
- Blast
- 23 files (excluding drizzle meta snapshots), +212/−7 net. Additive schema change (new table + existing-table column added with default in 0058). Billing path change touches computeBillShell and bill-for-contract shell — all calculated bills, not scraped CFE recibos.
Findings · 10
correctness2
NaN from Number() silently bypasses ?? fallback chain
domains/utility/src/tax-rate/tax-rate.queries.ts:17
If `rate` ever holds a non-numeric string (empty string, bad seed, direct SQL), `Number(row.rate)` returns `NaN`. `NaN ?? TAX_REGIME_RATES[code]` evaluates to `NaN` — not null/undefined — so both the code-constant fallback and the 0.16 hard-stop are silently skipped, producing `NaN × subtotal = NaN` in the bill. Fix: `const n = Number(row.rate); return Number.isFinite(n) ? n : null;` so a corrupt row falls through correctly. The `numeric(6,4) NOT NULL` column constraint makes this unlikely, but the stated invariant ('must never yield NaN') is not upheld.
Per-bill DB lookup when savings-report prices 3 contracts sequentially
domains/cross-domain/src/bill-compute.shells.ts
`computeBillShell` calls `TaxRateFCIS.findRateByCode` once per bill. The savings-report shell calls `persistBillForContract` (→ `computeBillShell`) for baseline + PPA + grid — 3 round-trips per report just for the tax rate, all fetching the same 2-row table with the same code. `findAllRates` was exported specifically 'for callers that price many bills' but is not consumed anywhere. Either prefetch once at the savings-report caller and pass the resolved rate via the existing `overrides` mechanism, or remove `findAllRates` until it's needed (YAGNI).
security1
Silent rate substitution masks future missing catalog entries
domains/cross-domain/src/bill-compute.shells.ts:190
If a future `tax_regime` value is added to the enum but its seed row is omitted from `tax_rates`, bills silently compute at 16% with no logged warning or Result error. Consider emitting a warning log when the DB lookup returns null and the code-constant fallback is used, so unseeded environments surface the gap rather than going unnoticed until a customer dispute.
conventions3
Multi-line JSDoc block on index.ts violates one-short-line-max comment rule
domains/utility/src/tax-rate/index.ts:1
The 3-line `/** Tax-rate FCIS module — the tax_rates catalog (regime code → IVA rate). */` block is a multi-line docstring. Project rule: 'Do NOT add multi-line docstrings or multi-line comment blocks — one short line max.' Collapse to a single `//` line or remove entirely.
File-level and per-function JSDoc in tax-rate.queries.ts explains WHAT, not WHY
domains/utility/src/tax-rate/tax-rate.queries.ts:1
The 4-line file-level block and the two per-function JSDoc comments (`/** The IVA rate… */`, `/** All regime→rate rows… */`) both explain what the code does — already communicated by function names and return types. Rule: 'Never explain WHAT the code does.' The only non-obvious fact worth keeping is the `numeric`→string→`Number()` boundary conversion, as a one-line inline comment at line 17.
Missing tax-rate.type.ts breaks canonical FCIS type-flow for a named FCIS module
domains/utility/src/tax-rate/index.ts
The module is exported as `TaxRateFCIS` — a first-class FCIS entity — but has no `tax-rate.type.ts` for the domain type. Canonical form: Drizzle schema → {entity}.type.ts → queries (the SSOT). Peer entity `tariff-rate/` has `tariff-rate.type.ts`. For a catalog-only entity, the type file is minimal (just `TaxRate { code: string; rate: number; description: string | null; … }`) but it closes the type-flow gap.
tests2
No integration test for findRateByCode — the primary lookup in the fallback chain
domains/utility/src/tax-rate/tax-rate.queries.ts
`tax_rates` is now the authoritative rate source. The fallback chain is correct only if `findRateByCode` returns `null` (not NaN, not 0) for an unknown code. This contract is not exercised by any test. A `tax-rate.integration.test.ts` should assert: (1) seeded codes return the correct numeric rate; (2) an unknown code returns `null`; (3) `findAllRates` returns a complete map. See `pricing-zone.integration.test.ts` for the established pattern.
Triple-fallback legs 2 and 3 have no test coverage
domains/cross-domain/src/bill-compute.shells.ts:187
The DB→constant→0.16 chain's second leg (unseeded-env fallback to `TAX_REGIME_RATES[code]`) is the designed path for preview/CI environments and is exercised by no test. A unit test with a mocked `findRateByCode` returning null would pin the contract and catch a future refactor that reorders or drops a leg.
improvement2
findAllRates is dead code — no caller in this or prior commits
domains/utility/src/tax-rate/tax-rate.queries.ts:20
`findAllRates` is exported and re-exported from the barrel but nothing calls it. Its docstring anticipates a looping caller that doesn't exist yet. Per CLAUDE.md: don't add features beyond what the task requires. Remove it and add it in the PR that adds the looping caller (savings-report or batch-billing path).
updatedAt column has no auto-refresh trigger — benign for seed-only catalog
packages/database/src/schema/tax-rates.ts
No `BEFORE UPDATE` trigger exists to auto-refresh `updated_at` on row mutation (none in this codebase). The seed script covers its own `ON CONFLICT DO UPDATE` correctly. For a catalog table only written by seeds this is fine, but a bare `UPDATE tax_rates SET rate = …` would leave `updated_at` stale. Consistent with other catalog tables in the repo; note the invariant in a migration comment if rates ever become admin-editable.
History · 6 commits
- a1f0898needs attentionincremental1H · 1M · 4L2026-07-13 20:37
- a72b534safeincremental0H · 0M · 0L2026-07-13 19:28
- 92e5afaneeds attentionincremental1H · 1M · 8L2026-07-13 16:47
- 7f40d56needs attentionincremental1H · 4M · 5L2026-07-13 16:38current
- 5088e00needs attentionincremental3H · 4M · 4L2026-07-13 02:47
- 1bda877blockedfull12H · 12M · 9L2026-07-13 02:30