A whole-krona payment (Bankgiro/Swish/kort) of an öre-bearing invoice
left the invoice hanging: the match_batch_allocate RPC (samlingsbetalning
dialog + MCP path) had no öre handling, so a sub-krona overshoot was
rejected as BATCH_OVERSHOOT and a sub-krona shortfall parked the invoice
in partially_paid forever. Invoices already stuck that way had no exit:
the mark-paid dialog proposed clearing the full total (rejected with
MATCH_AMOUNT_EXCEEDS_REMAINING) and the route refused partially_paid
outright.
Fix, both halves:
1. New migration 20260824120000 replaces match_batch_allocate with the
same öresavrundning band every single-payment path already uses
(ORE_ROUNDING_SETTLEMENT_MAX = 1.00 kr, lib/money.ts): overshoot
rejected only at >= 1 kr; a 0 < |remaining - allocation| < 1 kr diff
clears the FULL remaining off 1510/2440, books the residual to 3740
with correct polarity per side, records the full remaining as paid
and flips the status to paid. >= 1 kr diffs keep today's behaviour.
2. proposePaymentLines is remaining-aware: a partially_paid SEK accrual
invoice gets a proposal clearing the actual remaining, and a
sub-krona remaining gets a bank-less Dr 3740 / Cr 1510 write-off so
one click closes a stuck invoice. The mark-paid route and the
invoice-detail button now accept partially_paid (the settle layer's
CAS guard always did).
Forward-only: already-stuck invoices are not auto-repaired; they are
closed via the new dialog proposal.
Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>