feat/tax-regime
needs attentionviewing older commit5088e00 · incrementalPR #302reviewed 2026-07-13 02:47 UTC3H · 4M · 4L · 2I- Purpose
- Fix over-taxation of northern-border sites in the calculated-bill path, which hardcoded IVA at 16% instead of respecting the legal 8% IVA Frontera Norte rate for ~20km border-strip sites.
- Goal
- Add per-contract IVA regime (iva=16% / iva_frontera=8%) and wire it through the calculated-bill computation path, replacing the hardcoded MX_IVA constant.
- Sub-goals
- SG-1: Schema — tax_regime column on utility_contracts (additive, default iva, migration 0058)
- SG-2: Domain — TaxRegime type, TAX_REGIME_RATES map, CreateContractCommand wiring, mapper, type assertions
- SG-3: Billing — resolveSiteTariffContext returns taxRegime; computeBillShell and persistBillForContract consume it
- SG-4 (this commit): Allow updating taxRegime via decideUpdateContract + UpdateContractCommand; add NaN guard in shell
- What
- Added taxRegime to UpdateContractCommand (enabling programmatic updates), added the update branch in decideUpdateContract, added the ?? 0.16 NaN guard in computeBillShell, and excluded taxRegime from the UtilityContractResponse wire shape.
- Why
- SG-4 closes the loop on the feature: taxRegime can be created (SG-2/3) and is now also updatable. The NaN guard protects against raw-SQL bypass of the TS enum. The wire-response omission keeps the field billing-internal.
- Areas
- domains/cross-domain+25−4domains/utility+37−2packages/api/src/types+3−1packages/database+13069−0
- Blast
- 15 files, +13146/−7 (bulk is drizzle/meta snapshot). Substantive code changes across 8 files touching billing calculation, contract domain, and API types. No UI changes. Scraped CFE bills unaffected.
Findings · 12
correctness1
Zod update schema missing taxRegime — silent strip if field is ever sent over wire
packages/api/src/schemas/utility-contract.schemas.ts
UpdateContractCommand now includes taxRegime. UpdateUtilityContractRequest = UpdateContractCommand & { version: number }, so taxRegime is now part of the accepted wire type. However, the UpdateUtilityContractSchema Zod object does not include taxRegime: z.enum(['iva','iva_frontera']).optional(). No satisfies failure occurs because the field is optional — ZodObject silently strips unknown keys on parse, so any caller that sends taxRegime in a future PATCH body will have it silently dropped. The Zod schema must be updated alongside the command type to close this gap before the endpoint goes live.
security3
No DB-level CHECK constraint on tax_regime column
packages/database/src/schema/utility-contracts.ts
Drizzle's text('tax_regime', { enum: TAX_REGIMES }) does NOT emit a SQL CHECK constraint — enforcement is TypeScript-only. A direct SQL write, migration script, or ORM bug can insert an arbitrary string. The ?? 0.16 NaN guard mitigates the arithmetic consequence but silently applies 16% to frontera contracts with a corrupt value (over-billing by 2×). Add a $additionalConfig CHECK constraint: CHECK (tax_regime IN ('iva', 'iva_frontera')).
Silent 16% fallback for corrupt regime — no logging or error surfacing
domains/cross-domain/src/bill-compute.shells.ts:186
The ?? 0.16 fallback prevents NaN but silently over-taxes frontera contracts with a corrupt regime (applies 16% instead of the legal 8%). For a financial calculation this should at minimum emit a structured warning (tagged with contractId and the bad regime value). Silent fallbacks in billing code are a compliance risk.
taxRegime changes tracked in outbox events but no test pins the audit trail
domains/utility/src/utility-contract/utility-contract.decisions.ts:283
changes.taxRegime correctly captures old/new values in the decision output and will appear in outbox event data — good for audit. Consider adding an integration test that specifically asserts a taxRegime change produces an outbox event with the expected old/new pair, since this is a high-sensitivity financial configuration field.
conventions2
CreateContractCommand declares taxRegime manually instead of picking from UtilityContract
domains/utility/src/utility-contract/utility-contract.decisions.ts:59
UpdateContractCommand correctly uses Partial<Pick<UtilityContract, ...'taxRegime'>>. CreateContractCommand re-declares taxRegime as a manual readonly field with a comment, instead of including 'taxRegime' in the Pick set. If the TaxRegime type changes, the manual declaration won't be caught by the AssertEqual type-check. Move taxRegime into the Pick.
NaN guard is a business rule — belongs in domain layer, not cross-domain shell
domains/cross-domain/src/bill-compute.shells.ts:186
Per FCIS/ADR-016, 'what rate applies when the regime is invalid?' is a decision, not shell orchestration. Extract a resolveTaxRate(regime: TaxRegime | undefined): number helper in the utility domain (bill-calculator or utility-contract decisions) that encapsulates both fallbacks. Minor layering concern — the guard is correct as-is.
tests4
No unit tests for decideUpdateContract with taxRegime
domains/utility/src/utility-contract/__tests__/utility-contract.decisions.test.ts
decideUpdateContract now handles taxRegime (line 281–284) but has no tests for: (1) taxRegime changes iva→iva_frontera → patch and changes populated, (2) taxRegime passed but equal to existing → no-op (idempotency), (3) taxRegime undefined → no patch. Three test cases needed, all follow the established pattern for the other fields.
No test for ?? 0.16 NaN guard in computeBillShell
domains/cross-domain/src/bill-compute.shells.ts:186
The ?? 0.16 fallback is the only defence against an unknown/corrupted taxRegime yielding NaN in the bill total. The comment in the diff explicitly calls this a safety guard — it should be pinned by a unit test that passes an unrecognised regime string and asserts the returned taxRate is 0.16, not NaN.
Integration test does not verify taxRegime is persisted by updateContractShell
domains/utility/src/utility-contract/__tests__/utility-contract.integration.test.ts
The integration test suite calls updateContractShell but has no case that passes taxRegime and asserts the persisted row reflects the new value. Without this, a dropped field in the Drizzle update or mapper layer would be invisible.
Wire response omission of taxRegime not covered by an API-level test
packages/api/src/types/utility-contract.api-types.ts:31
There is no test asserting that a GET /utility-contracts response body does not contain a taxRegime key. Type-level omission is correct but a handler-level test would guard against accidental re-inclusion if a mapper spreads the domain object instead of field-mapping.
improvement2
UpdateContractCommand and UpdateContractPatch are parallel types maintained in two places
domains/utility/src/utility-contract/utility-contract.decisions.ts:124
Both types enumerate the same optional fields. The diff correctly adds taxRegime to both, but the parallel maintenance will bite again on the next field. UpdateContractPatch could be derived from UpdateContractCommand (e.g. as a mapped Required<UpdateContractCommand>) to make the single-source invariant structural.
taxRegime has no read surface — intentional but undocumented
packages/api/src/types/utility-contract.api-types.ts:31
taxRegime is billing-internal and excluded from the wire response. It's also not exposed by any read endpoint. This is a reasonable short-term stance but a future maintainer building a billing-settings UI may not know the domain layer is already wired. A brief comment on updateContractShell or UpdateContractCommand noting 'not exposed via HTTP yet' would prevent a parallel implementation.
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:38
- 5088e00needs attentionincremental3H · 4M · 4L2026-07-13 02:47current
- 1bda877blockedfull12H · 12M · 9L2026-07-13 02:30