eea410c42b
* 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>