Skip to content

tech-debt batch 5a: decompose projectSeason + API consistency (#158/#159)#168

Merged
delabrcd merged 3 commits into
mainfrom
tech-debt/batch-5a
Jun 22, 2026
Merged

tech-debt batch 5a: decompose projectSeason + API consistency (#158/#159)#168
delabrcd merged 3 commits into
mainfrom
tech-debt/batch-5a

Conversation

@delabrcd

Copy link
Copy Markdown
Owner

Tech-debt batch 5a — non-UI decompositions (epic #161). Behavior-preserving.

Closes #158, closes #159.

#158 — decompose projectSeason (prediction.ts)

Split the 131-line dual-mode function into pure, hand-testable helpers: resolvePricingMode(rows), priceSeasonMonth(...), combineAnnualBand(months). Kalman filter left as-is. The local median now uses lib/stats.ts (batch 3). Byte-identical: existing seasonProjection.test.ts (12 tests) passes unchanged; +10 new hand-calc tests for the helpers. (Dropped an unused rates param the issue sketched — noUnusedParameters would reject it; doesn't change any value.)

#159 — API consistency

  • Uniform error envelope: withAccount (lib/route.ts) now wraps empty()+handler() in errorResponse; removed the now-redundant inner try/catch from dashboard/layout + notifications; added try/catch to non-withAccount read routes (runs/accounts/refresh[id]). errorResponse serializes err.message only — ng-logins routes untouched, no secret leak.
  • export + export/pdfs migrated to withAccount (header-only CSV / 404 empty callbacks; byte-identical).
  • bills/[date]/pdf now honors ?accountId= (default when absent — single-account unchanged); multi-account installs get the correct premise's PDF.
  • All routes keep runtime='nodejs' + dynamic='force-dynamic'.

Verification

delabrcd added 3 commits June 22, 2026 19:29
Extract three behavior-preserving pure helpers from projectSeason in
lib/prediction.ts, each independently hand-testable:

- resolvePricingMode(rows): the #72 component-vs-flat pricing decision
  (Kalman component rates, useComponents, per-fuel variable rates, median
  period days). Pure function of rows alone — the flat `rates` only matter
  in per-month pricing, so it takes no `rates` param (a dead one would
  fail the strict build).
- priceSeasonMonth(...): the 12-month loop body — per-month usage
  projection, component/flat cost, sqrt(h) horizon band in quadrature,
  plus the anyFit/usedFallback signals the caller folds.
- combineAnnualBand(months): the annual quadrature band + sum point, with
  the +-15% floor.

projectSeason now composes these; output is byte-identical (same ops,
order, rounding, null handling). The Kalman filter is left as-is. The
local median was already re-pointed to lib/stats by batch 3.

Tests: seasonProjection.test.ts passes UNCHANGED (value-preservation
proof); add seasonProjectionHelpers.test.ts with hand-calculated expected
values for each new helper. Docker test stage 983 green; builder green.
…s PDF accountId (#159)

- withAccount now wraps resolution + callbacks in errorResponse, giving the
  read routes the same { error } + 500 envelope as the write/management
  routes. Dropped the now-redundant inner try/catch from the dashboard/layout
  and notifications handlers (kept notifications' deliberate best-effort sync
  swallow). Added try/catch -> errorResponse to the non-withAccount read
  routes (runs, accounts, refresh/[id]).
- export/route.ts and export/pdfs/route.ts migrated off the hand-rolled
  resolveRequestAccount -> unknownAccount dance to withAccount with their own
  empty callback (header-only CSV / 404), behavior-preserving.
- bills/[date]/pdf now resolves via withAccount so ?accountId= selects the
  correct premise's PDF; default account when absent (single-account
  unchanged), historical 404 preserved on no account.
- All routes keep runtime='nodejs' + dynamic='force-dynamic'.
@delabrcd delabrcd merged commit fdf07bb into main Jun 22, 2026
6 checks passed
@delabrcd delabrcd deleted the tech-debt/batch-5a branch June 22, 2026 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant