# Financial data-truth release review

Use this checklist for independent exact-commit reviews of financial import and read-model changes. Green suites are supporting evidence, not a substitute for tracing the real writer into the canonical reader.

## Scope and evidence

1. Verify exact `HEAD`, branch, clean worktree, requested base..target diff, and `git diff --check`.
2. If full suites are already disclosed green, do not rerun them; inspect changed tests and use focused synthetic/read-only probes only.
3. Never inspect or report production merchant, account, booking, report, database, or secret content. Report synthetic structures and aggregate outcomes.
4. Return one verdict (`APPROVE` or `CHANGES_REQUIRED`) with severity/file/line/exact remediation.

## Canonical review semantics

Define one shared actionable predicate and reuse it across global counts, filtered counts, page queries, item rendering, batch selection, confirmation guards, overview badges, and compatibility aliases:

- actionable: `requires_review=1` plus canonical open statuses such as `needs_review`, `pending`, and `transfer_candidate`;
- excluded: confirmed rows and source-covered/detail-only rows such as `covered_by_source`;
- proposal-ready: proposed category exists, is active, and matches the amount-derived income/expense type;
- special cases: one shared classification set for totals, filters, rendering, and write guards.

Counterexample-test inactive/deleted/wrong-type proposed categories. SQL totals must not call a row `proposal_ready` when item construction calls it `decision_needed`.

Do not cap confirmation lookup below the navigable backlog. A row reachable on page N must remain actionable; beware helpers with an implicit `LIMIT` used to build token maps.

## Cursor, filter, and count consistency

1. Use the exact sort tuple in both `ORDER BY` and cursor boundary, with a deterministic ID tie-breaker.
2. Bind cursor scope to every filter and a dataset/data-version token.
3. Reject filter or snapshot mismatch (normally 409) rather than appending across datasets.
4. Return global total, filtered total, page count, `has_more`, cursor, as-of, and data version separately.
5. Test a mutation between pages. The UI must restart pagination on a stale cursor rather than combine old rows with new totals.
6. Build filter facets from canonical server options/facets, not current-page rows; otherwise values present only on later pages are unreachable.

## Frontend state matrix

Verify independently:

- request failure: error visible; no empty/success claim;
- global empty: success wording only when canonical global total is zero;
- filtered empty: reset affordance and truthful global count;
- filter failure after prior success: clear results or explicitly mark retained rows stale and tied to the old filter;
- load-more failure: preserve prior rows without replacing them with contradictory counts;
- append: only across matching cursor scope/data version.

## Unified financial effects: writer to reader

Model effects once and consume them consistently across overview, monthly/yearly charts, category totals, details, forecasts, and cockpit APIs:

- income: positive income effect;
- expense/fee: positive expense effect;
- linked refund/reversal: negative expense effect inherited into the origin category;
- unlinked refund: excluded from income/expense and counted explicitly for review;
- transfer and credit-card settlement legs: zero income/expense/net effect, with volume/count separate if displayed;
- receipt details (for example Migros): non-monetary when covered by a linked money movement;
- duplicates: no productive monetary effect.

The highest-value probe exercises the real importer and then the canonical read model. A common false-green pattern is matching code that stores an origin ID only in JSON notes while the reader joins a dedicated `reversal_of_transaction_id` column. A read-model unit test that directly inserts the column passes, but imported refunds remain unlinked. Assert both the persisted linkage field and reconciled summary after end-to-end import.

For cross-period refunds, ensure a filtered refund can still join an origin outside the selected period. Verify inherited category and net arithmetic. Validate that the linked origin is an eligible expense/fee and that over-refunds or arbitrary links cannot manufacture negative expense; a foreign key to any confirmed transaction is not sufficient financial validation.

For every CHF-denominated effect, distinguish `amount_chf` from `amount_original`. Never fall back to a non-CHF original amount when CHF conversion is missing: a confirmed `EUR 100`, `amount_chf=NULL`, `fx_status='missing'` row must remain excluded/unknown or explicitly incomplete, not become CHF 100. Probe income, expense/refund, and neutral-transfer volume because the same fallback often contaminates all three.

If the API exposes neutral effect and transfer volume, name and render both explicitly. Do not replace a historical transfer-volume KPI with hard-coded zero under the same field; use separate `neutral_effect_chf` and `neutral_transfer_volume_chf` contracts.

Treat transfer volume as movement-level, not leg-level, unless the public contract explicitly and visibly says otherwise. A canonical internal-transfer writer commonly persists two neutral legs; summing `abs(amount_chf)` over transaction rows therefore doubles user-visible volume. Counterprobe through the real writer: one CHF 900 linked pair must render the contractually expected CHF 900 movement volume, while one unmatched CHF 900 transfer remains CHF 900. Independently probe zero volume, positive/negative stored signs, and missing-FX legs. Preserve `null`/unavailable volume when any included movement lacks a trustworthy CHF value, and expose the unavailable movement count separately. A frontend that correctly renders a backend-provided field does not close the finding when the provider double-counts paired legs.

Do not accept a row-level `LEFT JOIN` from transactions to transfer relations merely because canonical writers currently emit one relation per leg. Inspect schema uniqueness as well as writer guards. If membership is unconstrained, insert a second synthetic relation for the same leg: the reader must not duplicate confirmed counts, income/expense/refund effects, category counts, or movement volume. Prefer a pre-aggregated one-row-per-transaction membership CTE plus fail-closed conflict detection, or enforce the invariant with a migrated database constraint. Also inspect indexability: an `OR` join across unindexed `from_transaction_id`/`to_transaction_id` columns can turn every financial summary into a repeated transfer-table scan.

For paired missing-FX coverage, distinguish all-missing from mixed-known/missing legs. Verify global movement volume stays `null`, the unavailable count remains one logical unit, and account-filtered scopes follow the explicitly documented scope contract rather than accidentally inheriting or dropping the counterpart leg's quality state.

## Frontend request-order truth

A correct empty-state matrix is insufficient without request-order control. Rapid filter changes, reset watchers, and load-more can overlap. Require an abort controller or monotonically increasing request generation so an old response cannot overwrite the current filter or append an old page into a new scope. On a failed filter request, either clear rows/counts or label retained data with its previous filter/as-of; never leave old decisions looking current beneath newly selected controls.

Treat component unmount/navigation as another request generation. A response that resolves after unmount must not update state or call `history.replaceState`/router synchronization using the destination page's current pathname. Counterprobe by delaying the first-page response, navigating away, then releasing it; the destination URL and state must remain untouched. Increment the generation or abort on unmount, and prefer route-aware replacement over reconstructing a URL from global `window.location.pathname`.

## Performance and test-quality traps

- Flag repeated full-table effect scans, such as two CTE scans per month inside a 12-month loop; prefer one grouped year query or a shared/materialized result when material. Count transitive scans too: a chart doing 12 summaries and then calling a status helper that does another 12 can execute 48 scans, not merely 24.
- Date wrappers such as `substr(transaction_date,...)` commonly prevent ordinary date-index use; include query shape/indexability in the performance review.
- Direct SQL fixtures are valid for pure reader tests but cannot substitute for production writer→reader reconciliation.
- A deterministic synthetic counterexample outweighs aggregate green-suite status. Include only the sanitized outcome in the verdict.

## Final remediation counterprobes

When a remediation adds partial/incomplete financial semantics, test a **dimension × public-surface matrix**, not only the canonical helper that computes the values. Independently inject an unlinked refund and a missing-FX row, then inspect every changed public summary, overview, chart, cockpit, category, and transaction contract. A surface that emits zero totals with `current` status—or omits incompleteness metadata after switching to the canonical effect reader—still presents false completeness. Include missing-FX neutral transfers: a known transfer count with unknown CHF conversion must not become a trustworthy CHF `0.00` volume.

Exercise refund linkage through the real public writer, not direct SQL alone. Attempt a refund linked to an income or transfer and an over-refund beyond the eligible expense/fee amount. Require writer rejection or safe unlinked/partial treatment, and independently verify the reader does not infer validity merely because the referenced row exists and is confirmed. Report the observed `(income, expense, net, data_status)` tuple; negative expense or inflated net is blocking.

For every newly paginated mutable read model—not just the transaction ledger—challenge the dataset token with a non-max row mutation that does not advance `updated_at`, plus mutations in joined lookup tables that affect filtering or rendering. Review-inbox cursors commonly depend on candidate amount/date/classification/proposed category and active category metadata while hashing only `(COUNT, MAX(updated_at))`. The token must cover all ordering, membership, count, and rendered-state dependencies, and the endpoint should verify it again after assembling the page.

Do not stop at tables visibly joined by the page query. If item construction calls a classifier, rule resolver, learned-history helper, recurring detector, or request-local lookup cache, inventory every table that helper reads. Mutate one indirect dependency between pages—for example a merchant default, alias, review rule, confirmed-history row, or recurring definition—and compare both `data_version` and the old-cursor result. An accepted old cursor that changes category, explanation, evidence count, actionability, or any rendered field proves a mixed snapshot even when candidate and category mutation probes pass.

Frontend pagination must use the same server scope as the visible tab/filter. Never fetch a mixed-state page and hide rows locally: the first page can appear empty while matching rows remain on later pages. For compound tabs, add a canonical server-side union state or issue separately snapshot-bound requests. On a cursor-specific `409`, discard accumulated rows and restart page one; generic load-more failure preservation is not sufficient for a known stale snapshot.

Review action capability must also be end-to-end authoritative. For each rendered item, counterprobe special-case rows and opposite-type categories. The UI must honor `can_confirm`/supported actions and show only categories compatible with the immutable amount-derived type; selecting a value must not enable a normal confirmation path that the preview endpoint deterministically rejects.

## Post-fix verdict discipline

State whether the previously reported blocker is closed independently of the final verdict. A prior P1 can be fully closed end-to-end while a separate P1 still requires `CHANGES_REQUIRED`. When the requester demands a strict concise format, output only the gate token followed by complete severity/file/line findings and the explicit prior-finding closure statement—no praise, plan, or retrospective.
