feat/energy-api
needs attentiond6b4a35 · incrementalPR #207reviewed 2026-07-05 20:29 UTC4H · 4M · 5L · 2I- Purpose
- Port the legacy electricity-data-api (Pulumi/DDB/Athena stack in batu-monorepo) into the platform — billing, savings, metrics, webhooks, and PDFs. Port rule: domain logic and contracts port; the AWS plumbing dies.
- Goal
- Deliver Phases 0–8 of the 10-phase port: Tinybird metrics seam → reference data (TOU + rates) → CFE bill calculators → Inc-1 orchestration cores (billing planner, webhooks, savings, public API) → PDF generator.
- Sub-goals
- SG-1: Phase-0 decisions D1–D5 (Niko API-compat, TOU app-code placement, rate-values table, legacy ID mapping)
- SG-2: Site→metric-stream resolver in cross-domain (RLS-wrapped, resolveSiteEnergyStreams)
- SG-3: SiteMetrics dictionary/transforms/DQ + numeric coordinator (SiteMetricsFCIS)
- SG-4: GET /sites/:id/energy-metrics endpoint + RLS cross-org guard integration test
- SG-5: Live KK validation — coordinator vs raw Tinybird, all metrics 1e-6
- SG-N: Durable learnings folded into .claude/rules/site-metrics-seam.md
- Ph2a: tariff_rate_values FCIS entity (migration 0046) + 5,801 legacy rate rows seeded
- Ph2b: CFE TOU classifier + tariff_tou_schedules (migration 0045) + coordinator wiring + live validation (conservation 1e-9)
- Ph3: CFE bill calculator core + THOR FAC (energy+demand) + rate→tariff adapter + computeCfeBillShell + calculated bill persistence
- Ph4 Inc-1: runScheduledBillingShell planner + TJ reconciliation validated (808,226 MXN)
- Ph5 Inc-1: utility.bill.finalized outbox event + WebhookDeliveryFCIS pure core
- Ph6 Inc-1: SavingsFCIS.calculateSavingsReport pure core (EBL leakage as positive deduction)
- Ph7 Inc-1: GET /v1/sites/:id/energy-summary (CINEMEX validated)
- Ph8: @batu/bill-pdf react-pdf generator + POST /sites/:id/bill-pdf + S3/bill_files persist + tz-local charts
- What
- Final housekeeping commit: deleted all .branch/ temporal port notes and .scratch/ working files (including customer data), added .scratch/ to .gitignore, and folded last uncovered durable knowledge (CFE TOU classifier gotchas, THOR/FAC overlay model, deduction contractType axis, legacy prod DynamoDB location for Phase-9) into .claude/rules/.
- Why
- Branch policy: .branch/ is temporal, .scratch/ is ephemeral — durable decisions belong in .claude/rules/. Closes the SG-N knowledge-folding sub-goal; prepares the branch for merge.
- Areas
- packages/database+47942−11apps/platform+18338−4427docs/development+0−9578domains/cross-domain+8435−13domains/utility+6643−32packages/ui+3529−0services/utility+1290−4packages/api+1153−44domains/metrics+1180−14infra/cdk+676−1.claude/rules+489−4packages/bill-pdf+366−0
- Blast
- 543 files, +91,120/-15,904 cumulative vs main. Heaviest areas: packages/database (47k lines — migrations 0045/0046 + seeded rate/schedule data), apps/platform (193 files — new handlers/mappers/contracts for energy-metrics, bill-pdf, public-v1), domains/cross-domain (38 files — energy-metrics + billing coordinators), domains/utility (91 files — TOU, rate-values, bill-calculator, savings, webhooks).
Findings · 15
correctness4
DynamoDB key schema '/oid/cid/sid' is wrong for prod-ddb-electricity-rates
.claude/rules/infrastructure.md
The rule claims all three DynamoDB tables are 'keyed by /oid/cid/sid'. This is factually incorrect for prod-ddb-electricity-rates, which is keyed by rate_division (hash) + year_month (range) — confirmed by packages/database/src/seed-tariff-rates-historical.ts. The /oid/cid/sid notation is the legacy siteKey path format, relevant only to prod-ddb-metrics-sites-devices. Applying it uniformly will mislead anyone trying to query or cross-account-access the rates or simulated-bills tables in Phase 9.
SIMULATED_CFE_TOTAL listed as a 'deduction type' but it is the savings baseline
.claude/rules/savings-billing.md
The new 'Deduction contract types' table lists SIMULATED_CFE_TOTAL as a deduction that prices 'the baseline no-solar counterfactual'. But savings-billing.md § The formula defines savings as 'baseline − Σdeductions', where SIMULATED_CFE_TOTAL IS the baseline (the minuend), not something subtracted. computeSavingsReportShell skips SIMULATED_CFE_TOTAL when it appears as a deduction type. Framing it as a deduction inverts its role in the savings formula and will cause confusion when adding contract types or debugging savings mismatches.
Phase-9 legacy FAC discrepancy not documented in the THOR/FAC section
.claude/rules/ontology.md
The PR body documents a known Phase-9 finding: 'legacy stores energy_adjustment=1.035 but energy_charges = kWh × energy_rate (FAC NOT applied to the charge); our calculator applies THOR FAC multiplicatively. Reconcile before host cutover.' The THOR section describes the new behavior as canonical but does not note this divergence from legacy. A developer running the Phase-9 A/B will observe a systematic ~3.5–8.5% charge delta on THOR sites and will have no rule document explaining whether it is a bug or an intentional improvement.
GDMTH THOR FAC '≈ 1.085' is period-specific, not a fixed system constant
.claude/rules/ontology.md
The rule states 'GDMTH THOR ≈ 1.085, PDBT THOR ≈ 1.035'. Seed data shows GDMTH THOR carried energyAdjustment=1.0 for Jan–Jun 2024 and several older months, with 1.085 only from Jul 2024 onwards. Without qualification, this reads as a fixed system constant. Developers back-testing historical THOR bills who see FAC=1.0 applied may incorrectly conclude the rate row is corrupt rather than period-appropriate.
security3
Customer financial data (powen-savings.json/html) persists in branch git history
.scratch/powen-savings.json
The files are now correctly untracked and .scratch/ is gitignored — future commits are safe. However, powen-savings.json (per-site monthly energy kWh + MXN billing figures for Powen customer sites) was committed in a prior commit and remains in git history. Merging the PR as-is carries these git objects into main permanently. Remediation before merge: rewrite branch history with `git filter-repo --path .scratch/ --invert-paths` (or BFG) then force-push. This is a carry-over finding from the previous review; only the working-tree half has been addressed.
Real customer site names and peso figures used as validation anchors in savings-billing.md
.claude/rules/savings-billing.md
The file references 'Powen MC1 2026-02: gross 109,681 vs net billTotal 53,194' and names 'MC Industrias 1', 'Teknia', 'Aceros Alcalde' with exact MXN amounts as algorithm validation examples. These are real production customer names and figures. While the data is used as algorithm anchors (not a bulk dump), these values will be in every clone's git history. Consider anonymising the site labels before merge, or obtain explicit approval that this level of detail in internal rules files is company policy.
.scratch/ and .branch/ are now correctly untracked and gitignored
.gitignore
git ls-files .scratch/ and git ls-files .branch/ both return empty — no tracked files remain. .scratch/ is now on line 153 of .gitignore; .branch/ was already gitignored. Future working files in these directories will not be staged by git add. The working-tree side of this cleanup is complete.
conventions3
Infrastructure note contains temporal/phase-specific task state in a permanent rule
.claude/rules/infrastructure.md
The added paragraph states 'still-open Phase-9 legacy peso-to-peso A/B (energy-api port) needs cross-account creds to read them.' CLAUDE.md is explicit: rules define how things should be built, not current task state. Once Phase-9 closes, this note becomes wrong. The durable fact (management account holds these DynamoDB tables, never deploy stacks there) is useful and should stay; the phase-reference and 'still-open' qualifier belong in the Linear issue or PR description, not a permanent rule file.
TOU and THOR sections appended after 'Deferred' section — breaks ontology.md hierarchy
.claude/rules/ontology.md
The new ## CFE TOU classification and ## THOR sections appear after '## Deferred: tariff-rate ontology rework'. The file organises content as top-level ## domain sections (Core / Utility / Metrics), then Deferred. Appending new durable sections after Deferred makes them visually appear deferred too. Both belong as ### subsections under '## Utility Domain' (both live in domains/utility/) or immediately before Metrics Domain — not trailing after the Deferred group.
Management account ID repeated — already in the account segmentation table
.claude/rules/infrastructure.md
The account ID 397762539248 already appears in the account segmentation table three lines above the new paragraph. Convention in infrastructure.md is to define the ID once in the table and use the human label ('management account') in prose. This is a minor DRY violation with trivial remediation.
tests3
Summer GDMTH 'base=0' case not tested in TOU classifier or breakdown
domains/utility/src/tou-schedule/__tests__/tou-schedule.decisions.test.ts
The rule documents a critical gotcha: summer GDMTH weekday has NO base period, so base=0 is CORRECT. No test exercises this path. All tou-schedule.decisions tests use GDMTH_BC_WINTER (which always has a base window). A regression here would silently misclassify all summer weekday slots — breaking the conservation invariant without a failing test. A test using a real summer GDMTH SIN schedule (weekday.base=[]) asserting decideTouPeriod returns non-null for all hours and base contribution sums to 0 is missing.
TOU conservation invariant tested only in cross-domain, not in the classifier's own tests
domains/utility/src/tou-schedule/__tests__/tou-schedule.decisions.test.ts
The Σ(base + intermedio + punta) == window total invariant is verified in domains/cross-domain/__tests__/site-energy-metrics-breakdown.test.ts (toBeCloseTo 1.5, 9) but not in the TOU scheduler's own unit suite. Coverage exists at the integration level; adding a unit-level invariant check inside tou-schedule.decisions.test.ts (iterating all 1440 minute-slots on a known schedule) would isolate classifier failures from transform failures.
SIMULATED_CFE_TOTAL deduction-skip guard not unit-tested
domains/cross-domain/src/__tests__/savings-cfe-grid.test.ts
savings-report-compute.shells.ts silently skips SIMULATED_CFE_TOTAL when it appears as a deduction type. No unit test asserts this skip behavior. If the guard were inadvertently removed, the baseline bill would be subtracted from itself (yielding near-zero savings) with no test failure to catch it.
improvement2
Infrastructure DynamoDB note gives no path to obtain cross-account credentials
.claude/rules/infrastructure.md
The note correctly identifies that Phase-9 needs cross-account creds to read the management account DynamoDB tables, but provides no guidance on how to obtain them: no IAM role name, no SSO permission set, no runbook reference. The standard 'batu-dev' SSO profile cannot access the management account per the same file's 'never deploy stacks here' rule. A developer attempting the A/B validation is left at a dead end. At minimum: note whether a cross-account read role exists, who to ask, or a Linear issue link.
bill-compute.shells.ts cited without path qualifier — lives in cross-domain, not utility
.claude/rules/ontology.md
The THOR section references 'bill-compute.shells.ts' without a path. The file lives at domains/cross-domain/src/bill-compute.shells.ts. In the context of ontology.md (a domain entity doc), readers may look in domains/utility/src/ first. Adding the full relative path removes the ambiguity.
History · 8 commits
- d6b4a35needs attentionincremental4H · 4M · 5L2026-07-05 20:29current
- 678cb93safeincremental0H · 0M · 1L2026-07-05 17:40
- d219403needs attentionincremental1H · 3M · 4L2026-07-05 05:37
- 5ecd80cneeds attentionincremental1H · 2M · 1L2026-07-05 04:59
- 3fd1d03needs attentionincremental0H · 3M · 5L2026-07-05 04:36
- 1446f7eneeds attentionincremental2H · 4M · 7L2026-07-05 04:08
- ca73a1bneeds attentionfull4H · 10M · 14L2026-07-05 02:46
- c24738eneeds attentionincremental3H · 10M · 9L2026-07-04 05:12