← all branches

feat/energy-api

needs attentionviewing older commit
d219403 · incrementalPR #207reviewed 2026-07-05 05:37 UTC1H · 3M · 4L · 3I
The branch
Purpose
Port the legacy electricity-data-api (billing + savings + metrics + webhooks + PDFs; Pulumi/DDB/Athena in batu-monorepo) into the platform. Domain logic and contracts port; AWS plumbing dies.
Goal
Deliver Phases 0–8 of the electricity-data-api port end-to-end: metrics adapter → tariff/TOU reference data → CFE bill calculators → calculated-bill persistence → PDF. Phases 4/5/6/7 delivered as Inc 1 pure cores; Inc 2 infra deferred.
Sub-goals
  • SG-0: Port spec + decisions D1–D5 (decisions-port.md)
  • SG-1: Tinybird → SiteMetrics adapter (derived-at-read, integral transform, DQ) — KK matched to 1e-6
  • SG-2: tariff_rate_values storage (mig 0046, 5,801 legacy rates seeded)
  • SG-3: CFE TOU classifier + tariff_tou_schedules (mig 0045, conservation 1e-9)
  • SG-4: CFE bill calculators + THOR FAC + computeCfeBillShell + persist/dedup
  • SG-5: Run orchestration Inc 1 — pure planner + runScheduledBillingShell
  • SG-6: Webhooks Inc 1 — utility.bill.finalized event + WebhookDeliveryFCIS
  • SG-7: Savings Inc 1 — SavingsFCIS.calculateSavingsReport
  • SG-8: Public /v1 API Inc 1 — GET /v1/sites/:id/energy-summary (CINEMEX validated)
  • SG-9: PDFs — @batu/bill-pdf (react-pdf), rich header + S3/bill_files persist + tz-local charts
The changes (whole branch)
What
Two housekeeping commits: (1) admin-required justification comments added to all public-v1 handlers (energy-summary, savings ×4, tariff-jobs ×2) documenting why service-role DB reads bypass RLS; (2) premature seed-dependent e2e specs (SitesPage + sites.spec) deleted; (3) ESLint devDeps added to @batu/utility-tariffs-cfe.
Why
BAT-124 audit trail: public-v1 endpoints use service-role DB (no auth.uid() → RLS can't apply) and need explicit per-call justifications so security reviewers can verify the auth boundary. E2e cleanup removes tests that were testing a not-yet-launched feature and causing flaky seed-dependent failures.
Areas
apps/platform+165444387domains/utility+664028domains/cross-domain+843513packages/database+25398packages/api+114233domains/metrics+118014services/utility+12890packages/bill-pdf+3660infra/tinybird+4521e2e (deleted specs)+050
Blast
480 files, +49,180/−4,729 across 10+ areas; core energy port touching metrics, utility, cross-domain, platform API, database migrations, and new bill-pdf package. Incremental window: 7 files, comment-only + e2e deletion + devDeps.
Phase 9 legacy peso-to-peso A/B not yet complete (−5.9% on KK 2026-05 cycle, day-count + FAC discrepancy open) Inc-2 infra deferred: cron→SFN→worker estate, webhook-delivery worker, savings entities/cascade Niko /v1 host cutover + api-key scope enforcement should be resolved before DNS flip
ci· No CI check results available via gh pr checkscoderabbit· No .coderabbit.yaml in repo

Findings · 11

security4

high

tariff-jobs: new admin-required comment claims `withPublicApiAuth` gate but handler uses `withAuth`

apps/platform/src/api/handlers/tariff-jobs.handler.ts

The new comment reads 'The scrape is a service-role write gated by withPublicApiAuth — matches the cfe-jobs posture (BAT-247)' but the handler imports and calls `withAuth` (Supabase session JWT), not `withPublicApiAuth` (machine actor key). These are materially different: `withAuth` accepts any active session with no role gate; `withPublicApiAuth` accepts Batu machine-actor JWTs. The factual error misrepresents the security posture in an audit-trail comment and misleads future reviewers. Fix: either correct the comment to reference `withAuth`, or if machine actors should be able to trigger tariff scrapes (BAT-247 intent), swap the middleware.

medium

API key scopes declared but silently dropped — per-key access restrictions are never enforced

apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts

API keys carry a `scopes` array; `validateBatuPublicApiAuth` verifies the claim but then maps to `AuthContext` which has no `scopes` field — they are dropped. No handler checks scopes. A narrowly-scoped key (`['bills:read']`) has identical reach to a wildcard key. Pre-existing issue surfaced by the admin-required audit pass; should be resolved before Niko host cutover.

medium

Post-gate shells receive raw `params.sitePublicId` rather than the gate-confirmed `site.sitePublicId`

apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts

`resolveSiteTariffContext` and `resolveSiteEnergyMetricsShell` are called with the raw path param `params.sitePublicId`, not the gate-confirmed value returned by `findSiteInOrg`. Currently safe (the gate already 404s for cross-org sites), but if a future refactor reorders or conditionalizes the gate these calls would silently expose cross-org data. Passing `site.publicId` instead costs nothing and makes the defense explicit.

low

Multi-org human JWTs silently collapse to memberships[0] in public-v1 handlers

apps/platform/src/api/handlers/public-v1/savings.handler.ts

All public-v1 handlers resolve the caller's org as `memberships[0]?.orgId`. A human user belonging to multiple orgs always uses their first-claim org; requests for sites in a second org return 404 with no indication why. Pre-existing; document the single-org machine-actor design intent explicitly or reject multi-membership tokens at `withPublicApiAuth`.

conventions2

medium

admin-required comment in tariff-jobs incorrectly names auth middleware

apps/platform/src/api/handlers/tariff-jobs.handler.ts:58

The admin-required comment pattern is the audit trail for service-role DB reads. Naming the wrong middleware (`withPublicApiAuth` vs actual `withAuth`) in this canonical justification comment undermines the audit-trail purpose — a reviewer cannot trust the comment accurately describes the security boundary. The correctness lens also flagged this: the comment has a factual error.

low

admin-required comment tags `dispatchTariffJobPipeline` as a 'DB read' — it is an SFN dispatch

apps/platform/src/api/handlers/tariff-jobs.handler.ts:80

`dispatchTariffJobPipeline` is an AWS Step Functions `StartExecution` call (with a best-effort ARN write), not a DB read of the tariff job. The admin-required pattern is for service-role DB reads that bypass RLS; tagging an SFN dispatch with it adds noise and misrepresents what bypass is happening at that point.

tests2

low

No tracking issue for restoring sites e2e coverage when the feature ships

e2e/platform/sites.spec.ts

The commit removes the spec as 'premature WIP UI'. No corresponding Linear issue or TODO comment marks that coverage needs to be restored when the sidebar link is enabled. Consider filing a tracking issue or a `// TODO(sites-launch)` comment in the e2e directory.

info

E2E spec deletions are safe — both files tested a disabled/pre-launch UI route

e2e/platform/sites.spec.ts

Sites sidebar link is `disabled (coming-soon)`; the spec navigated directly to the route. No shipped user-reachable behavior is uncovered by the deletion. SitesPage has zero remaining imports after removal.

improvement3

low

Identical org-gate comment duplicated 4× in savings.handler.ts

apps/platform/src/api/handlers/public-v1/savings.handler.ts

The same 2-line admin-required org-gate justification appears verbatim before every `findSiteInOrg` call across all four handlers. The file-level module JSDoc already documents the public-v1 auth posture. A single module-level comment plus a one-line reference at each call site would reduce drift risk.

info

3 no-console lint warnings remain after ESLint devDeps fix

services/utility/tariffs/cfe

ESLint now runs (exit 0) but leaves 3 `no-console` warnings: `console.log` in `src/db.ts` lines ~48 and ~61, `console.info` in `src/browser-clearance.ts`. The fix commit was the natural moment to resolve these — they're not errors, but structured logging (stderr/warn) is the convention for Lambda handlers.

info

Deleted e2e page objects had reusable patterns worth documenting before they're lost

e2e/pages/SitesPage.ts

The deleted files demonstrated: (1) `domcontentloaded` + explicit readiness assertion (not `networkidle` — PostHog keeps pages non-idle); (2) `gotoFromList()` seed-independent navigation helper; (3) `escapeRegExp` multi-value text-match for status pills. A brief note in `e2e/CLAUDE.md` or a fixture would prevent reinventing these patterns.

History · 8 commits

  1. d6b4a35needs attentionincremental4H · 4M · 5L2026-07-05 20:29
  2. 678cb93safeincremental0H · 0M · 1L2026-07-05 17:40
  3. d219403needs attentionincremental1H · 3M · 4L2026-07-05 05:37current
  4. 5ecd80cneeds attentionincremental1H · 2M · 1L2026-07-05 04:59
  5. 3fd1d03needs attentionincremental0H · 3M · 5L2026-07-05 04:36
  6. 1446f7eneeds attentionincremental2H · 4M · 7L2026-07-05 04:08
  7. ca73a1bneeds attentionfull4H · 10M · 14L2026-07-05 02:46
  8. c24738eneeds attentionincremental3H · 10M · 9L2026-07-04 05:12