# Prior-year actuals and deterministic forecast releases

Use this pattern when a financial dashboard adds manually maintained historical annual actuals and compares them with a deterministic current-year forecast.

## Storage contract

1. Inspect existing tables before adding a migration. Reuse a table only when it already represents comparison baselines—not transactions, plans, recurring contracts, or forecast snapshots—and already provides year, stable category FK, amount, timestamps/source, and uniqueness.
2. Represent **not recorded** as absence/`NULL`; represent an explicitly confirmed zero as a persisted `0.00` row. Never normalize empty UI cells to zero.
3. Persist annual totals as annual rows. Derive the read-only monthly average as `annual_actual / 12`; do not materialize twelve synthetic transaction rows.
4. Save one year's edited category grid as one fingerprint/source-version-bound Preview → Confirm transaction and one batch audit event. Validate duplicate category IDs, active expense-category membership, past-only years, non-negative finite amounts, and bounded numeric magnitude. Malformed extreme inputs must return 422 rather than leaking Decimal/int exceptions as 500s.

## Comparison arithmetic

For each category:

- `deviation_chf = forecast_year - comparison_actual`
- `deviation_percent = deviation_chf / comparison_actual * 100` only when comparison actual is present and greater than zero.

For totals, sum money fields independently and calculate total percent from total sums—never sum or average category percentages. If any comparison category is unrecorded, show the recorded comparison total and missing count, but suppress total deviation and total percent; comparing a complete forecast with a partial historical total is misleading.

When rows are limited to today's active canonical categories, compute current-year total actuals from **all confirmed canonical expense effects through the cutoff**, not merely the displayed rows. Detect confirmed effects with no active displayed category (uncategorized or tied to an inactive category), expose their amount/count as a material gap, and show that warning in the primary comparison UI. Do not silently drop them or invent a pseudo-category/second taxonomy. Suppress aggregate forecast and deviations until the gap is resolved, even when each displayed row is individually forecastable.

## Deterministic forecast contract

1. Start from confirmed canonical financial effects, preserving the existing refund semantics.
2. Exclude transfers, credit-card settlements/counterposts, recurring candidates, subscription candidates, manually maintained fixed-cost plans, and any plan/budget amount from the normal forecast.
3. Require at least three **provably complete** completed months. A month containing one or more confirmed transactions is not proof of coverage. Build coverage from authoritative import/session intervals (or an explicit month-closure contract), merge only overlapping or adjacent intervals, and count a month only when the merged interval covers its first through last calendar day. Also exclude months with unresolved candidates. Never bridge gaps between disjoint partial imports or infer completeness from `MIN/MAX(transaction_date)` alone.
4. Exclude the current partial month from the averaging window. Compute:
   - `average = completed-month actuals / completed-month count`
   - `remaining_parts = remaining full months + remaining fraction of current month`
   - `forecast_remaining = average * remaining_parts`
   - `forecast_year = actual through data cutoff + forecast_remaining`
5. Quantize every displayed operand at the point it becomes part of the calculation contract. In particular, round the monthly average to currency precision before multiplying it by displayed remaining-month parts. Then verify `displayed actual + displayed forecast remaining == displayed annual forecast` exactly.
6. If material confirmed spending is uncategorized, or the coverage threshold is not met, return an unreliable/missing forecast rather than treating actual-to-date as a reliable annual forecast or displaying zero.

## API and frontend contract review

- Compare real backend response keys and nullability with TypeScript types for read, preview, and confirm payloads. Include purpose/version literals, totals/review key names, saved-count fields, and every nullable forecast/deviation field.
- Protect every year/filter load with a monotonically increasing request generation so stale responses cannot overwrite a newer selection. Put the guard at the state owner: if pages share a Pinia/Redux-style store, guarding only one page component is insufficient. Commit result, error, and `loading=false` only when the response generation is still current; add a deferred-promise test where the newer response resolves first. Disable year switching while editing or saving a batch.
- Keep missing historical values and unreliable forecasts rendered as an em dash or explicit unavailable text; render persisted zero as `CHF 0.00`.
- Category drill-downs must carry current year, selected comparison year, category ID, and confirmed-expense scope into the existing explorer.

## Verification probes

- Default year and arbitrary additional past year; future and pathological years rejected.
- Empty versus explicit zero through preview, confirm, readback, total, and audit.
- Monthly average and total average.
- Category CHF/% deviations and total percent from total sums; incomplete comparison suppresses total deviation/%.
- At least-three-month threshold with explicit full-month import coverage; adversarial disjoint/partial intervals must not qualify. Also verify current-month fraction, transfer/card-settlement neutrality, and no recurring/provider contribution.
- Confirmed uncategorized and inactive-category expenses remain in current-year actual totals, produce a visible material gap, and suppress aggregate forecast/deviation without creating a second category structure.
- A stale-response inversion test at the shared state owner: request B resolves before request A, and B remains rendered with loading/error state owned by B.
- An uneven monthly average (for example `100 / 3`) proving the displayed forecast explanation reconciles exactly.
- Browser UAT at desktop/tablet/mobile with synthetic data, no horizontal overflow, 44 px targets, no console errors, and no external render requests.
- Run final full suites only on the frozen candidate. If later review fixes change code or tests, rerun affected focused/static gates and let mandatory final CI provide the one complete frozen-candidate suite unless the release contract explicitly requires another local full run.