Files
accounted/lib/core
Mattsson eea410c42b fix(bookkeeping): correctEntry moves bank anchors to the correction; deleting the correction returns them (#2406)
* fix(bookkeeping): correctEntry moves the original entry's voucher links to the correction

Why the problem occurred: a bank row has two anchors, the pointer column
(transactions.journal_entry_id) and the transaction_voucher_links junction
(bulk-book writes a bank_line row beside the pointer for N=1 and as the only
anchor for a samlingsverifikat with N>1). correctEntry re-pointed only the
pointer, so the reversed original kept its junction rows and every junction
reader (is_transaction_booked, fetchJunctionLinkedTxIds, the bulk_book RPC,
the reconciliation bridge) went on treating the row as anchored there. A
later storno of the correction released the pointer while the stale link kept
the row out of Att bokfora: the split #2061 fixed on the storno path,
reproduced on the correction path. Prod holds 7 such links in 3 companies.

What was removed or simplified: nothing new is added to the data model. The
relink helper now moves both anchors with the same predicate (company and
source entry), so the junction follows the pointer and one rule covers the
N=1, samlingsverifikat, 1:N slice and residual shapes. A relink failure is
surfaced on the result (transactionRelinkError) beside documentRelinkError
instead of being logged and forgotten.

Why this solution: the issue proposed deleting the original's junction rows.
For a samlingsverifikat the junction is the row's only anchor, so deleting it
would push rows the corrected verifikat still explains back into the
worklist; re-pointing keeps them booked against the live entry. A
relink_entry_anchors RPC moving pointer and junction atomically was
considered and left for later: it costs a migration plus pg test on a path
that is already best-effort across five other statements, and the surfaced
warning now makes a partial failure visible if one ever happens.

Tests: unit cases on correctEntry for the junction update, its scoping and
the surfaced warning; a pg-real suite that runs the two UPDATE statements as
the correcting user against real Postgres for the N=1, samlingsverifikat,
split-plus-residual and cross-tenant shapes (RLS, the writer-role gate and
the immutability triggers do not block the move; role and allocated_amount
survive it).

Fixes #2364

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Efj3hm54wP53zfmoz4RXxE

* fix(bookkeeping): deleting a correction returns its bank anchors to the original

Found by the skeptic on the previous commit. Once the junction follows the
correction, the two-step undo of a rattelse (delete the correction, last in
series, then delete the storno, which restores the original to posted)
cascaded the links away with the correction (FK ON DELETE CASCADE; the
pointer FK is ON DELETE SET NULL). The restored original then explained bank
rows nobody pointed at: is_business stayed true, the rows surfaced as
bookable in Att bokfora and in bank reconciliation, and a second booking of
the same movement was one click away. Before, the links had stayed on the
original by accident and the undo happened to be clean.

delete_last_voucher (migration 20260908095907) now moves both anchors back to
correction_of_id before deleting a correction, the inverse of the move
correctEntry makes. Releasing the rows instead would leave the same trap
(the restored original still explains them), and a TS pre-step in the
DELETE route is not atomic with the RPC's own guards. A link the original
already holds (a correction made before the junction followed it) is
dropped rather than duplicated. Everything else in the function is
byte-for-byte 20260528120600.

Applied to staging and recorded under the file's version. pg-real suite
covers the N=1, samlingsverifikat, pre-existing-duplicate and plain-voucher
shapes; the existing delete_last_voucher and document-immutability suites
still pass.

Refs #2364

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Efj3hm54wP53zfmoz4RXxE

* docs(decisions): record the #2364 prod repair as planned, not done

CodeRabbit on PR #2406: the entry read as if the seven-link repair had already
run. It runs after merge on the founder's go and gets its own dated entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Efj3hm54wP53zfmoz4RXxE

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 10:34:15 +02:00
..