feat/energy-api
needs attentionviewing older commitd219403 · incrementalPR #207reviewed 2026-07-05 05:37 UTC1H · 3M · 4L · 3I- 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
- 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+16544−4387domains/utility+6640−28domains/cross-domain+8435−13packages/database+2539−8packages/api+1142−33domains/metrics+1180−14services/utility+1289−0packages/bill-pdf+366−0infra/tinybird+45−21e2e (deleted specs)+0−50
- 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.
Findings · 11
security4
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.
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.
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.
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
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.
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
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.
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
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.
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.
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
- d6b4a35needs attentionincremental4H · 4M · 5L2026-07-05 20:29
- 678cb93safeincremental0H · 0M · 1L2026-07-05 17:40
- d219403needs attentionincremental1H · 3M · 4L2026-07-05 05:37current
- 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