← all branches

feat/tax-regime

needs attentionviewing older commit
92e5afa · incrementalPR #302reviewed 2026-07-13 16:47 UTC1H · 1M · 8L · 7I
The branch
Purpose
Fix systematic over-taxation of northern-border sites in calculated bills — computeBillShell hardcoded IVA at 16% but Frontera Norte sites pay 8%
Goal
Add per-contract tax regime (iva/iva_frontera) threaded into the calculated-bill path; extract resolveTaxRate as a testable pure decision with a safe fallback chain
Sub-goals
  • SG-1: Schema — tax_regime column on utility_contracts (additive, safe migration)
  • SG-2: Domain — TAX_REGIMES/TaxRegime/TAX_REGIME_RATES; type, mapper, CreateContractCommand, insert
  • SG-3: Billing — resolveSiteTariffContext returns taxRegime; computeBillShell applies it; persistBillForContract resolves from pinned contract
  • SG-4: Data-driven — tax_rates table replaces code constants; seed with iva→0.16, iva_frontera→0.08
  • SG-5: Test+fix — extract resolveTaxRate as pure decision, add unit tests, close review gaps (this commit)
The changes (whole branch)
What
Final commit extracts the three-tier fallback (table → constant → 0.16 default) from bill-compute.shells into a pure resolveTaxRate decision function; adds unit tests for all three branches; adds taxRegime to CreateUtilityContractSchema/UpdateUtilityContractSchema; adds decideUpdateContract test cases for taxRegime patch/no-patch/omit; adds compliance warn log for unrecognized regime codes
Why
Addresses reviewer feedback from the previous commit: the fallback logic was inline in the shell (not testable) and the Zod schemas were missing the taxRegime field (API clients couldn't set it)
Areas
domains/cross-domain+485domains/utility+1343packages/api+72packages/database+971
Blast
27 files changed across 4 areas (+26509/−8 cumulative branch churn, dominated by Drizzle snapshots); core impact is billing calculation path for calculated bills only — scraped CFE recibos unaffected; additive schema with DEFAULT 'iva' is safe on populated staging/prod
billing-critical migration-additive-safe test-coverage-gaps
typecheck· typecheck green across @batu/database, @batu/utility-domain, @batu/cross-domain, @batu/platform per PR descriptiondb:check-rls· db:check-rls green per PR descriptioncoderabbit· no .coderabbit.yaml in repoci· PR CI checks not queryable from runner context

Findings · 17

correctness2

low

resolveTaxRate does not guard against NaN from Number() coercion in the query layer

domains/utility/src/tax-rate/tax-rate.decisions.ts:22

The query layer converts the postgres `numeric` column via `Number(row.rate)`. For a malformed row, `Number()` returns `NaN`, which satisfies `!== null`. `resolveTaxRate` would return `{ rate: NaN, source: 'table' }`, and `NaN × subtotal = NaN` on the bill — the exact failure mode the whole chain aims to prevent. A `Number.isFinite(tableRate)` guard instead of `!== null` would close the gap. No test covers `resolveTaxRate('iva', NaN)`.

info

'constant' source not logged — no observability when table is unseeded

domains/cross-domain/src/bill-compute.shells.ts:194

The warn fires only on `source === 'default'`. When tax_rates is empty (unseeded preview/local), billing silently uses the TAX_REGIME_RATES constants. The constants are correct, so no billing bug, but there is no observability signal distinguishing 'running off the table' from 'running off a fallback constant'. Intentional per the design; noted for ops visibility.

security2

low

Customer site identifiers logged to console.warn without structured-log routing

domains/cross-domain/src/bill-compute.shells.ts:184

`sitePublicId` and `yearMonth` appear in the warn JSON. Values are non-secret (opaque ULIDs, non-PII period strings) but routed through `console.warn` which lands in CloudWatch as plaintext without field-level filtering. No injection risk; minor hygiene concern for log governance if field-level access controls are ever needed.

info

resolveTaxRate parameter typed as `string` rather than `TaxRegime` enum

domains/utility/src/tax-rate/tax-rate.decisions.ts:22

Any string can be passed; unrecognized codes silently apply 16% IVA. The API boundary is closed (TaxRegimeSchema z.enum validates incoming strings), so external risk is nil. Internal misuse (direct domain calls bypassing schema) could produce silent financial defaults. Narrowing to `TaxRegime` type at compile time would make this a type-system invariant.

conventions4

low

Decision function not prefixed `decide{Operation}` per canonical naming

domains/utility/src/tax-rate/tax-rate.decisions.ts:14

Canonical-form.md requires decision functions to be named `decide{Operation}` (e.g. `decideResolveTaxRate`). The exported function is `resolveTaxRate`, matching query/utility naming instead. The function is infallible so not returning `Result<T,E>` is acceptable, but the naming convention is independent of return type.

low

index.ts barrel missing TaxRateFCIS namespace re-export

domains/utility/src/tax-rate/index.ts:1

Per canonical-form.md, `index.ts` should export a `{Entity}FCIS` namespace grouping the entity's public surface. The shell already calls `TaxRateFCIS.resolveTaxRate` and `TaxRateFCIS.findRateByCode`, so the namespace object is expected but not exported from the barrel (only named exports exist).

info

resolveTaxRate not returning Result<T,E> — intentional and acceptable

domains/utility/src/tax-rate/tax-rate.decisions.ts:14

ADR-016 requires Result<T,E> for fallible operations. resolveTaxRate is total (every path returns ResolvedTaxRate), so omitting Result is correct. Flagged for reviewer awareness only.

info

console.warn in shell — consider structured logger if one exists

domains/cross-domain/src/bill-compute.shells.ts:195

Project conventions favor a structured logger over raw console.* in shells. Consistent with the file's existing style (other warn calls also use console.warn + JSON.stringify). Standardise when a logger utility lands.

tests4

high

tableRate = 0 (zero-rate) is untested — falsy coercion regression risk

domains/utility/src/tax-rate/__tests__/tax-rate.decisions.test.ts:4

The guard is `tableRate !== null`, which correctly handles 0, but no test passes tableRate = 0. A future refactor tightening the guard to `!tableRate` or `tableRate ?? ...` would silently break zero-rate and this suite would not catch it. Add: `expect(resolveTaxRate('iva', 0)).toEqual({ rate: 0, source: 'table' })`.

medium

tableRate = undefined not exercised — null vs undefined assumption undocumented

domains/utility/src/tax-rate/__tests__/tax-rate.decisions.test.ts:10

The function accepts `number | null` but Drizzle nullable columns sometimes surface as `undefined` after a left-join. `undefined !== null` is true, so `resolveTaxRate('iva', undefined as any)` would fall through the null-check and reach TAX_REGIME_RATES lookup — coincidentally correct but for the wrong reason. A test with undefined documents the assumption and guards against looser callers.

low

NaN as tableRate untested — original bug vector resurfaces through table branch

domains/utility/src/tax-rate/__tests__/tax-rate.decisions.test.ts:14

The test verifies NaN protection for unknown *regime codes* but not for `NaN` as the *tableRate* itself. `NaN !== null` is true so `resolveTaxRate('iva', NaN)` returns `{ rate: NaN, source: 'table' }`. Add a case: `expect(resolveTaxRate('iva', NaN)).toEqual({ rate: 0.16, source: 'constant' })` (once the guard is fixed).

low

mockContract.taxRegime fixture assumption is implicit in decideUpdateContract tests

domains/utility/src/utility-contract/__tests__/utility-contract.decisions.test.ts:290

The idempotency test (`taxRegime: 'iva'` unchanged) relies on `mockContract.taxRegime` being `'iva'`. The diff shows `taxRegime: 'iva'` added to the mock, but if it were missing, the test would pass vacuously (`undefined === undefined`). Confirmed present in the diff, so this is just a fragility note.

improvement5

low

findRateByCode fires per-bill; multi-bill callers should prefer findAllRates

domains/cross-domain/src/bill-compute.shells.ts:191

computeBillShell issues one SELECT per call. savings-report paths call persistBillForContract 2-3× per site-period (baseline + PPA + CFE_GRID), each hitting tax_rates independently for the same regime code. findAllRates (already exported) loads the full table in one round-trip. A memoised per-request cache would collapse N queries to 1 for multi-bill callers. Low urgency — table is tiny — but worth noting as a hot-loop pattern.

low

0.16 default literal duplicates TAX_REGIME_RATES.iva

domains/utility/src/tax-rate/tax-rate.decisions.ts:26

The default `{ rate: 0.16, source: 'default' }` hard-codes a value already defined as `TAX_REGIME_RATES.iva`. If the standard IVA rate ever changed, both sites need updating. `{ rate: TAX_REGIME_RATES.iva, source: 'default' }` makes the intent explicit and keeps constants DRY.

info

console.warn could adopt a structured-log abstraction when one lands

domains/cross-domain/src/bill-compute.shells.ts:195

The unknown_tax_regime warning is the most compliance-sensitive log line in the billing path. It is a natural early adopter for any structured-logger utility. No action before merge.

info

TaxRegimeSchema not re-exported from api package schemas barrel

packages/api/src/schemas/utility-contract.schemas.ts:63

TaxRegimeSchema is defined and used internally. A future handler or MCP tool validating taxRegime standalone would need to re-derive it or import from an internal path. Consider whether exporting it from the package's exports entry is warranted.

info

'constant' fallback unreachable in seeded environments — worth documenting

domains/utility/src/tax-rate/tax-rate.decisions.ts:24

The 'constant' fallback can only be reached in tests or a freshly-provisioned environment before the seed runs. The code is correct and the safety is valuable, but a comment noting 'seeding-gap fallback, not a normal operating mode' would help on-call readability.

History · 6 commits

  1. a1f0898needs attentionincremental1H · 1M · 4L2026-07-13 20:37
  2. a72b534safeincremental0H · 0M · 0L2026-07-13 19:28
  3. 92e5afaneeds attentionincremental1H · 1M · 8L2026-07-13 16:47current
  4. 7f40d56needs attentionincremental1H · 4M · 5L2026-07-13 16:38
  5. 5088e00needs attentionincremental3H · 4M · 4L2026-07-13 02:47
  6. 1bda877blockedfull12H · 12M · 9L2026-07-13 02:30