feat/energy-api
needs attentionviewing older commitca73a1b · fullPR #207reviewed 2026-07-05 02:46 UTC4H · 10M · 14L · 5I- Purpose
- Port the legacy electricity-data-api (batu-monorepo, Pulumi/DDB/Athena) into the platform monorepo's canonical Drizzle/Tinybird stack
- Goal
- Enable CFE billing, savings reports, and energy metrics for all Batu customers from a single codebase, with the SiteMetrics seam as the stable metrics↔billing contract
- Sub-goals
- Phase 0: port spec + decisions + SiteMetrics type contract
- Phase 1: Tinybird→SiteMetrics adapter (resolveMetricStreamWindow)
- Phase 2: site monitoring status derivation + asset fleet
- Phase 3: pure CFE bill calculator (GDMTH/PDBT, Q-method, FAC)
- Phase 4: asset fleet UI + portfolio worklist
- Phase 5: savings report computation + configs
- Phase 6: savings schema (savings_configs, savings_reports)
- Phase 7: PDF handler + tariff jobs
- Phase 8: public-v1 energy-summary + savings endpoints
- What
- 477 files changed across the full stack: new domains/metrics/site-metrics and domains/cross-domain coordinator shells, new domains/utility bill-calculator + savings + tariff-job entities, new DB tables (tariff_jobs, tariff_tou_schedules, savings_configs, savings_reports, site_exception_states), operationStartDate moved from site_locations → sites, 22 new API endpoints, fleet/portfolio UI, and critical bug fixes (parseTinybirdUtc UTC parse, GDMTH pass-through double-count).
- Why
- The legacy stack (Pulumi/DDB/Athena) is being decommissioned; the port brings billing-grade energy calculations under the canonical FCIS/Drizzle/Tinybird architecture with full test coverage of the pure billing core.
- Areas
- apps/platform+16519−4386domains/cross-domain+8435−13domains/utility+6582−28docs+5512−0packages/ui+3529−0packages/database+2539−8domains/metrics+1180−13services/utility+1286−0packages/api+1142−33infra/cdk+675−0
- Blast
- 477 files, +49.3k/−4.7k across billing-critical, public-API, schema-migration, and UI areas
Findings · 23
correctness4
Q-method demand cap skips when measured demand is exactly 0 kW
domains/utility/src/bill-calculator/bill-calculator.decisions.ts
Guard `measured > 0 ? Math.min(measured, q) : q` treats a genuine 0 kW reading as 'no meter', returning uncapped Q. A shutdown or fully-offset net-metering site would be over-billed. Fix: key on value presence (undefined/null) not 0.
Pass-through charges emitted standalone when genCost=0
domains/utility/src/bill-calculator/tariff.adapter.ts
genNet lambda skips pass-through subtraction when genCost=0. If seed data pairs genCost=0 with non-zero CENACE/SCNMEM/transmission, customers would be over-billed.
memberships[0]?.orgId silently picks first org for multi-org machine tokens
apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts
Not a security issue; machine tokens carry one synthetic membership. Human JWT with multiple memberships would silently 404 for non-first-org sites.
String date comparison in site-savings handler — no format validation guard upstream
apps/platform/src/api/handlers/site-savings.handler.ts
Lexicographic YYYY-MM-DD comparison is correct but contract schema uses z.string() without datetime refinement — malformed input reaches the shell.
security5
Multi-membership token: first-org selection implicit and undocumented
apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts
findSiteInOrg gates correctly so no IDOR, but cross-tenant API key callers silently get 404 for sites in orgs[1..n]. Enforce single-membership invariant at token-creation time.
Post-guard resolveSiteEnergyMetricsShell uses raw service-role db
apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts
Safe today IF the shell never re-fetches the site record unscoped. Consider passing the resolved site object instead of raw sitePublicId to make the org-scoping structural.
SITE_ID_PATTERN /i flag accepts lowercase ULID segments
apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts
Lowercase variants pass the regex but return no rows from case-sensitive PG, leaking format/not-found distinction. Drop the /i flag.
tariff_jobs authenticated select policy has no tenant scope
packages/database/src/schema/tariff-jobs.ts
Acceptable for global reference data, but needs an inline comment confirming the rationale to prevent future tenant-column additions from exposing cross-tenant data.
snoozedUntil NOT NULL for status='snoozed' not enforced at DB level
packages/database/src/schema/site-exception-state.ts
Only enforced in the TS decision layer. Add CHECK (status <> 'snoozed' OR snoozed_until IS NOT NULL) to prevent service-role direct inserts from creating corrupt rows.
conventions4
throw in exported decisions function violates ADR-016 Result<T,E> contract
domains/utility/src/savings/net-metering.decisions.ts
parseIsoDate throws new Error() on invalid input. Return Result<Date, NetMeteringError> and propagate via early-return.
Pure mapping function siteMetricsToConsumption placed in .shells.ts
domains/cross-domain/src/bill-compute.shells.ts
Pure synchronous mapper should be in .mapper.ts per canonical form.
Pure mapping function toPpaBillCommand placed in .shells.ts
domains/cross-domain/src/bill-for-contract.shells.ts
Same issue — move to .mapper.ts.
Dead import: siteLocations still imported after JOIN migration
domains/metrics/src/metric-source/metric-source.queries.ts
siteLocations removed from query but still imported.
tests8
resolveSiteEnergyMetricsShell has zero integration test coverage
domains/cross-domain/src/site-energy-metrics.shells.ts
The central energy coordinator shell is only tested at the pure-function level. A DB-round-trip integration test covering TOU breakdown, ambiguousVariables, and RLS is missing.
Public v1 endpoints have no handler-level or E2E tests
apps/platform/src/api/handlers/public-v1/
/v1/energy-summary and /v1/savings have only a mapper unit test. Auth enforcement, error mapping, and wire shape are untested.
computeSavingsReportShell shell orchestration untested
domains/cross-domain/src/savings-report-compute.shells.ts
Helper functions have unit tests but the fetch→compute→SavingsReport shell path has no integration test.
bill-pdf handler has zero test coverage
apps/platform/src/api/handlers/bill-pdf.handler.ts
No test references bill-pdf. Auth enforcement and S3 presign error paths are dark.
resolveSiteMonitoringStatusShell untested at shell level
domains/cross-domain/src/site-monitoring-status.shells.ts
Decision logic is well covered; the shell's DB fetch and timezone-sensitive yesterday-bounds are untested.
Tariff-job shell and handler untested
Shell idempotency and handler error mapping are dark.
Asset-fleet and portfolio-rollup shells untested
Shell-level DB integration (org scoping, RLS, cross-site aggregation) untested for both coordinators.
site-exception-state shell and handler untested
Decisions thoroughly tested (6 snoozedUntil cases); shell DB upsert + optimistic lock are dark.
improvement2
Duplicate lazy TinybirdClient singleton across 4 handler files
apps/platform/src/api/handlers/energy-metrics.handler.ts
Extract to apps/platform/src/api/utils/tinybird-client.ts.
SITE_ID_PATTERN regex duplicated across public-v1 handlers
apps/platform/src/api/handlers/public-v1/energy-summary.handler.ts
Move to public-v1-validation.ts alongside other ID 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: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:46current
- c24738eneeds attentionincremental3H · 10M · 9L2026-07-04 05:12