feat/kwh-price
needs attentionviewing older commitb968509 · fullPR #344reviewed 2026-07-23 15:48 UTC0H · 4M · 3L · 2I- Purpose
- Incremental $-layer for the energy findings system: demand detectors now compute estimatedImpact in MXN using real CFE tariff rates.
- Goal
- Price D2b overnight_load_factor and finalize D1 punta_management with real genBCost/capacityCost from tariff_rates — so findings can be prioritized by dollar magnitude alongside billing errors.
- Sub-goals
- New consumption-series module: pure FCIS, calendar-allocated dedup kWh series
- D1 punta_management: real capacityCostMxnPerKw from tariff_rates
- D2b overnight_load_factor: estimatedImpact = (util-median) x avgBaseKw x 12 x genBCost
- Unified ContractRate type, single fetchContractRatesForOrg query for both detectors
- What
- Added consumption-series FCIS module (type, decisions, errors, queries, tests) and extended finding domain with real-rate $-layer for D1+D2b demand detectors via new fetchContractRatesForOrg query. Detector versions bumped to @2.
- Why
- Detectors previously ranked findings by severity only; with real rates they can be sorted by estimated annual MXN impact — a key prioritization signal for the Batu for Enterprise product.
- Areas
- domains/utility/src/consumption-series+862−0domains/utility/src/finding+1153−371docs/energia+291−0
- Blast
- 3 areas, 14 files, +2306/-371 lines. No schema changes, no API surface changes, no new Lambda or CDK resources. Pure domain logic + one new SQL query.
Findings · 9
correctness4
LATERAL filter gates genBCost on capacityCost presence — stale rates if fields diverge
domains/utility/src/finding/finding.queries.ts:813
In PostgreSQL NULL <> '' evaluates to NULL, so rate rows lacking capacityCost are excluded. D2b silently reads genBCost from a potentially older row if CFE publishes a partial update.
D1 rateByContract lacks > 0 guard — asymmetric with D2b
domains/utility/src/finding/finding.decisions.ts:1069
D2b filters baseCostMxnPerKwh > 0 before inserting into its map; D1 does not guard capacityCostMxnPerKw. A zero value suppresses the documented DEFAULT_PUNTA_CAPACITY_RATE_MXN_PER_KW fallback.
4x `as number` casts suppress TypeScript narrowing
domains/utility/src/finding/finding.decisions.ts:1079
Casts at lines 1079, 1170, 1174, 1239 are semantically safe but fragile under refactoring. Use type-predicate filters instead.
D2b avoidableBaseKwhPerYear positivity guaranteed implicitly, not explicitly
domains/utility/src/finding/finding.decisions.ts:1214
Positivity follows from ratio > D2B_WARN_RATIO, but is not asserted. Low risk; named avoidable implies positive.
conventions1
ContractRate type defined in finding.decisions.ts, imported by finding.queries.ts
domains/utility/src/finding/finding.decisions.ts:1046
Per canonical form, domain types belong in finding.type.ts. Both decisions and queries import from the same source to avoid lateral coupling.
tests4
Missing test: D2b estimatedImpact null when baseCostMxnPerKwh = 0 but D1 resolves
domains/utility/src/finding/__tests__/finding.detectors.test.ts
The > 0 guard asymmetry between D1 and D2b is not exercised by any test. A ContractRate with positive capacityCost but zero baseCost should produce null for D2b while D1 still computes a figure.
No integration test for fetchContractRatesForOrg LATERAL join semantics
domains/utility/src/finding/finding.queries.ts:789
The LATERAL filter, pricing_zone_id IS NOT NULL gate, and baseCostMxnPerKwh fallback are not covered at the DB level. The shell integration test does not seed tariff_rates rows.
Missing test for perPeriod absent on mixed-coverage bimonthly month
domains/utility/src/consumption-series/__tests__/consumption-series.decisions.test.ts
The daysWithPerPeriod === daysCovered guard's negative branch is untested. A bimonthly bill with mixed per-period coverage should emit perPeriod: undefined.
decideUpsertFinding null→number estimatedImpact transition untested
domains/utility/src/finding/__tests__/finding.detectors.test.ts
The null→number path in decideUpsertFinding is not covered. Logic is correct but the transition case is implicit.