← all branches

feat/energy-api

needs attentionviewing older commit
ca73a1b · fullPR #207reviewed 2026-07-05 02:46 UTC4H · 10M · 14L · 5I
The branch
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
The changes (whole branch)
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+165194386domains/cross-domain+843513domains/utility+658228docs+55120packages/ui+35290packages/database+25398domains/metrics+118013services/utility+12860packages/api+114233infra/cdk+6750
Blast
477 files, +49.3k/−4.7k across billing-critical, public-API, schema-migration, and UI areas
billing-critical public-api-surface schema-migration large-pr
CI· GitHub checks not accessible (PAT scope)rls-coverage· all 5 new tables have enableRLS() + full CRUD policiesmigration-sequence· 0053–0055 staged correctly; backfill for operationStartDate movelegacy-equivalence· KK: 49.878019 kWh = 598536.226656/12000; MC1 $93,865 regression anchorcoderabbit· no .coderabbit.yaml present

Findings · 23

correctness4

high

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.

medium

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.

low

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.

low

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

medium

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.

medium

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.

low

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.

low

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.

low

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

medium

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.

low

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.

low

Pure mapping function toPpaBillCommand placed in .shells.ts

domains/cross-domain/src/bill-for-contract.shells.ts

Same issue — move to .mapper.ts.

low

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

high

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.

high

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.

high

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.

medium

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.

medium

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.

medium

Tariff-job shell and handler untested

Shell idempotency and handler error mapping are dark.

medium

Asset-fleet and portfolio-rollup shells untested

Shell-level DB integration (org scoping, RLS, cross-site aggregation) untested for both coordinators.

medium

site-exception-state shell and handler untested

Decisions thoroughly tested (6 snoozedUntil cases); shell DB upsert + optimistic lock are dark.

improvement2

low

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.

low

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

  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:37
  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:46current
  8. c24738eneeds attentionincremental3H · 10M · 9L2026-07-04 05:12