← all branches

fix/gdmth-cap-q

needs attentionviewing older commit
4a519dd · fullPR #312reviewed 2026-07-15 00:11 UTC1H · 5M · 5L · 3I
The branch
Purpose
Fix systematic under-billing of GDMTH capacity charges in no-punta winter seasons (Tijuana). CFE mandates the Q-method (demanda facturable) when no punta demand exists; the calculator was billing $0 instead.
Goal
Port the legacy method_q_demand_mandatory branch into the new pure billing calculator, data-gated on the chargeFactor rate field so existing tariffs without it are unaffected.
Sub-goals
  • SG-1: Add qFallbackFactor to Charge interface and chargeFactor to RateValues/RateComponents
  • SG-2: Wire chargeFactor → qFallbackFactor on TOU capacity charges in tariff adapter
  • SG-3: Apply Q formula in demandQuantity when measured=0/null and qFallbackFactor is set
  • SG-4: Validate against legacy KK figure (170.8107 kW) and A/B vs mgmt bills across 33 sites × 3 periods
The changes (whole branch)
What
Added qFallbackFactor optional field to Charge; chargeFactor optional field to RateValues and RateComponents; demandQuantity fallback logic in decisions.ts; adapter wiring; 6 new test cases for Q-fallback and 3 for adapter wiring.
Why
GDMTH winter (Tijuana, no punta band) billed capacity=0 instead of the CFE mandatory Q-method demanda facturable. Fix closes 12/12 winter mismatches, zero summer regressions.
Areas
domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts+1060domains/utility/src/bill-calculator/__tests__/tariff.adapter.test.ts+150domains/utility/src/bill-calculator/bill-calculator.decisions.ts+141domains/utility/src/bill-calculator/bill-calculator.types.ts+150domains/utility/src/bill-calculator/tariff.adapter.ts+111domains/utility/src/tariff-rate/tariff-rate.type.ts+90
Blast
6 files, +170/-2 lines. Pure domain layer (domains/utility/src/bill-calculator + tariff-rate). No API, shell, or DB schema changes. Data-gated on chargeFactor presence.
billing-correctness cfe-regulatory gdmth-winter no-migration
CI checks· gh pr checks returned GraphQL access error — status unknowntypecheck· Not run in this review pass; PR description states typecheck clean across @batu/utility-domain + @batu/cross-domainunit tests· PR states 26 unit tests pass; not re-run in this review passA/B validation· PR documents 0/12 → 12/12 winter capacity matches; 0 regressions on 66 summer site-periods

Findings · 14

correctness2

medium

Q fires on genuine shutdown (bands absent, peak_15min=0) — no test pins this as intentional

domains/utility/src/bill-calculator/bill-calculator.decisions.ts:106

When by_tou_band is absent AND peak_15min=0 (genuine shutdown, no punta), measuredDemandForCharge returns undefined??0=0, triggering Q. On derived_from_energy the same scenario returns min(0,Q)=0. This mirrors legacy !tip_kw behavior and is intentional — but there is no test documenting this specific input (bands absent + peak_15min=0) to prevent an accidental regression normalizing both strategies to 0 in a refactor.

info

Q formula always uses total_consumption regardless of charge.schedule — correct today, implicit assumption

domains/utility/src/bill-calculator/bill-calculator.decisions.ts:108

derived_from_energy selects kWh by schedule type (tou_band vs always). The new fallback always uses total_consumption. This is correct because the capacity charge has schedule: {type: 'always'}, but if qFallbackFactor were set on a tou_band charge in future, it would silently use the wrong basis. No guard or comment captures this assumption.

security1

info

num() already guards NaN/Infinity — parseFloat safety concern is addressed

domains/utility/src/bill-calculator/tariff.adapter.ts

num() returns Number.isFinite(n) ? n : undefined, correctly rejecting Infinity, -Infinity, and NaN. The security surface for qFallbackFactor corruption from JSONB is appropriately mitigated at the parse layer.

conventions1

low

chargeFactor naming breaks the {type}Cost / {type}Adjustment convention in RateComponents

domains/utility/src/tariff-rate/tariff-rate.type.ts:46

All other RateComponents fields follow {concept}Cost (supplyCost, capacityCost, etc.) or {concept}Adjustment (energyAdjustment). chargeFactor has no suffix. More consistent: capacityChargeFactor or qDemandFactor. Low impact since this is an internal adapter field.

tests6

medium

Missing test: derived_from_energy vs measured_15min produce different results on identical winterInput(0)

domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts:195

No test runs both demand strategies on the same zero-punta input and asserts they diverge. This asymmetry is the behavioral core of the fix — a future refactor that accidentally normalizes both paths to 0 would pass all existing tests.

medium

Missing test: Q-fallback + FAC (applyEnergyAdjustment) interaction on measured path

domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts

THOR GDMTH sites (Krispy Kreme, BC = Tijuana) can combine FAC scaling with no-punta winter. The Q formula reads total_consumption after applyEnergyAdjustment — so Q scales with FAC. No test covers Q + FAC. Given KK is a live THOR GDMTH site in Tijuana, this is a real production scenario.

medium

Missing test: bimonthly period (60+ days) with Q fallback

domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts

GDMTH account prefixes 01-40 are bimonthly (~62 days). Q = totalKwh / (24 × daysInPeriod × CF) — a 62-day period roughly halves the result vs 31 days. No test covers this. Bimonthly billing regressions have historical precedent in this codebase.

low

winterInput helper casts `peakDemand as number` — hides undefined in typed field

domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts:184

by_tou_band.peak: peakDemand as number casts undefined into a number-typed field. The test behavior is correct (undefined?? falls back to peak_15min=150), but the type cast bypasses TS safety. Prefer omitting the peak key entirely when undefined: ...(peakDemand !== undefined && { peak: peakDemand }).

low

KK regression test precision at 3dp when fix was validated to the cent (4dp)

domains/utility/src/bill-calculator/__tests__/bill-calculator.decisions.test.ts:223

toBeCloseTo(170.8107, 3) has ±0.0005 tolerance. PR claims cent-level match; consider tightening to 4dp to actually pin that claim.

low

Adapter negative test vacuously passes if distribution charge is absent

domains/utility/src/bill-calculator/__tests__/tariff.adapter.test.ts:22

expect(t.charges.find(c => c.sourceConcept === 'distribution')?.qFallbackFactor).toBeUndefined() trivially passes when find() returns undefined. Add an assertion that the distribution charge exists before asserting its field.

improvement4

high

Asymmetric zero-treatment between demand strategies is undocumented — fragile for future maintainers

domains/utility/src/bill-calculator/bill-calculator.decisions.ts:106

derived_from_energy treats measured=0 as a solar-credit cap (min(0,Q)=0). The new qFallbackFactor branch treats measured=0 as 'no punta season' and TRIGGERS Q — the opposite. This is intentional (different CFE rules) but the comment only explains the new branch; it doesn't note the contrast with derived_from_energy. A developer reading both branches in sequence will flag the inconsistency as a bug. Add a cross-reference: 'note: derived_from_energy caps Q at measured (min(0,Q)=0 for solar offset); this path treats measured=0 as absent punta — different CFE rules, intentional.'

medium

qFallbackFactor is charge-level but semantically tariff-level — misconfiguration risk at adapter

domains/utility/src/bill-calculator/tariff.adapter.ts:91

chargeFactor (0.57) is a GDMTH tariff property, not a per-charge property. Attaching it at charge-level means any future author adding a second TOU demand charge in buildTariff must know to omit qFallbackFactor — the type system won't enforce this. A comment on the capacity-charge block noting 'only capacity charge for TOU tariffs gets this factor' reduces future extension blast radius.

low

Double guard `charge.qFallbackFactor && charge.qFallbackFactor > 0` is redundant

domains/utility/src/bill-calculator/bill-calculator.decisions.ts:106

The truthiness check is redundant with > 0. Simplify to (charge.qFallbackFactor ?? 0) > 0. Minor style nit.

info

No DB migration needed — worth noting in PR so reviewers don't hunt for one

domains/utility/src/bill-calculator/bill-calculator.types.ts

qFallbackFactor lives on in-memory Charge objects assembled by buildTariff at runtime. chargeFactor on RateComponents is an additive JSONB field (no schema change). No migration needed.

History · 2 commits

  1. 5ee36ddsafeincremental0H · 0M · 0L2026-07-15 00:29
  2. 4a519ddneeds attentionfull1H · 5M · 5L2026-07-15 00:11current