invoices.journal_entry_id means "the registration verifikat that booked
this invoice at issuance" — payment flows route on it (set → clear 1510,
NULL → kontantmetoden cash entry). Two bugs in v1 mark-paid broke that:
- The pre-flight select omitted journal_entry_id, so invoiceAlreadyBooked
always read false — a kontantmetoden company paying an already-registered
invoice would re-recognise revenue + VAT (double-booking) and orphan the
1510 receivable. Fixed by fetching the column for routing only; the
response contract and invoice.paid event payload are unchanged.
- The update wrote the just-created PAYMENT/cash entry id into the column
(wrong semantic) — once routing reads the column, a cash partial payment
#1 would make payment #2 clear a 1510 that was never debited. Removed;
the payment entry id still returns in the response body.
New backfill migration links the earliest posted invoice_created entry to
historical invoices (353 registered-but-unlinked rows in hosted prod),
repairs any payment-type links, and links credit_note reversal entries to
credit-note rows. Idempotent; rows with no registration entry stay NULL
(correct for kontantmetoden/unsent invoices).
Tests: 3 new unit tests lock the select projection, the already-booked→
clearing routing, and the no-write-back semantics (the supabase mock now
records call args). New pg-real suite (11 tests) runs the actual migration
SQL: earliest-wins, reversed/draft exclusion, no-overwrite, cash stays
NULL, payment-link repair, credit notes, cross-company isolation,
idempotency. insertDraftJournalEntry fixture gains optional sourceType/
sourceId/createdAt (defaults unchanged).
Hosted prod requires manual migration apply after merge (Supabase MCP).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>