tech-debt batch 5a: decompose projectSeason + API consistency (#158/#159)#168
Merged
Conversation
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'.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 localmediannow useslib/stats.ts(batch 3). Byte-identical: existingseasonProjection.test.ts(12 tests) passes unchanged; +10 new hand-calc tests for the helpers. (Dropped an unusedratesparam the issue sketched —noUnusedParameterswould reject it; doesn't change any value.)#159 — API consistency
withAccount(lib/route.ts) now wrapsempty()+handler()inerrorResponse; removed the now-redundant inner try/catch from dashboard/layout + notifications; added try/catch to non-withAccountread routes (runs/accounts/refresh[id]).errorResponseserializeserr.messageonly — ng-logins routes untouched, no secret leak.export+export/pdfsmigrated towithAccount(header-only CSV / 404 empty callbacks; byte-identical).bills/[date]/pdfnow honors?accountId=(default when absent — single-account unchanged); multi-account installs get the correct premise's PDF.runtime='nodejs'+dynamic='force-dynamic'.Verification
/api/verifygreen on staging; API consistency: uniform error envelope + export routes via withAccount + bills PDF accountId #159 routes smoke-tested on staging (overview/bills?accountId/export).