fix(payments): refuse to book a bank row that unlinked vouchers already explain (#2300)
* fix(payments): refuse to book a bank row that unlinked vouchers already explain A bank feed can deliver several affarshandelser as one row (a Bankgirot daily aggregate: two customers' invoices, one "BGGIRERING" row with no payer). When each invoice was already marked paid by hand, nothing on the account equals the row, the 1:1 duplicate check passes, and "Dela betalning" books the money a second time against whatever open invoices the user picks (the next period's identical ones, in the reported case). - lib/reconciliation/covering-set.ts: exact ore subset sum over a capped candidate list, smallest set first, closest in date second. - detectExplainingVoucherSet(+ForTransaction): the vouchers whose bank legs on the row's settlement account, in the row's direction, within 7 days, add up exactly to the row; linked through any of the three anchors drops a voucher, a payment row without a bank transaction keeps it. - POST match-batch refuses with BATCH_TX_POSSIBLE_DUPLICATE and returns the set; force=true must echo expected_journal_entry_ids (same binding as the single door). Fails open on a detection error. - GET duplicate-payment-check returns candidate_set next to candidate. - MatchAllocationDialog: pre-flight panel with the vouchers, one click links the row to them through the existing 1:1 or 1:N bank link (no new voucher), "Bokfor anda" acknowledges the set; confirm is disabled until then. Invoices dated after the bank row get a hint badge. - Mark-paid guard: aggregate sweep (row = this invoice + an exact subset of other open invoices, 7 days, kronor) when the name sweeps found nothing; PaymentBookingDialog shows the covered invoice numbers and points to the split under Transaktioner. Follow-ups: #2293 (1:N proposals in the auto-matcher), #2294 (MCP staging guard), #2299 (supplier-side text guard). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyjeEi1U8vnuPT4QXgayXu * test(invoices): account for the aggregate sweep in the mark-paid route queue The sweep issues one more transactions query whenever the name probes come back empty, so every queued-mock sequence that reaches it gains a slot. The sweep itself now fails open on odd client shapes (a single object for a list query) and on errors: an advisory guard must never block "Markera som betald". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyjeEi1U8vnuPT4QXgayXu * fix(payments): fail open on resolved query errors; aggregate sweep without a payer name Review follow-ups on #2300. A PostgREST failure resolves with { data: null, error } instead of throwing, so the set detector read a failed link lookup as "no links" and a failed cash-account lookup as "scan every 19xx account"; both now return null (the booking RPC keeps the last word). The aggregate sweep never needed a customer name (a Bankgirot row names nobody), so a nameless invoice goes straight to it instead of skipping the guard. The already-booked panel is announced as a live region, and the "also covers" string is plural-aware. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyjeEi1U8vnuPT4QXgayXu --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
9418de585f
commit
287828a850
+16
-1
@@ -3500,7 +3500,19 @@
|
||||
"success_title": "Payment allocated",
|
||||
"success_description": "{count, plural, one {The invoice has} other {The invoices have}} been booked against the transaction.",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm allocation"
|
||||
"confirm": "Confirm allocation",
|
||||
"already_booked_title": "Already looks booked",
|
||||
"already_booked_body_in": "The incoming payment of {amount} is covered exactly by {count, plural, =1 {one posted voucher} other {# posted vouchers}} with no link to any bank transaction. Most often the invoice was already marked paid in the invoice module. Link the transaction there instead of booking the payment a second time.",
|
||||
"already_booked_body_out": "The outgoing payment of {amount} is covered exactly by {count, plural, =1 {one posted voucher} other {# posted vouchers}} with no link to any bank transaction. Most often the supplier invoice was already marked paid. Link the transaction there instead of booking the payment a second time.",
|
||||
"already_booked_link": "Link to {labels}",
|
||||
"already_booked_book_anyway": "Book anyway",
|
||||
"already_booked_acknowledged_title": "Booking anyway",
|
||||
"already_booked_acknowledged_note": "You chose to book the payment despite {labels}. The voucher is created when you confirm the allocation.",
|
||||
"already_booked_foreign_hint": "The transaction is in {currency}. Link it under Reconciliation, Match manually.",
|
||||
"already_booked_link_success_title": "Transaction linked",
|
||||
"already_booked_link_success_description": "Linked to {labels}. No new booking created.",
|
||||
"already_booked_link_failed": "Could not link",
|
||||
"invoiced_after_payment_badge": "Invoiced {date}, after the payment"
|
||||
},
|
||||
"tx_skattekonto_card": {
|
||||
"skv_badge": "Skatteverket",
|
||||
@@ -4469,6 +4481,9 @@
|
||||
"match_reason_ocr_exact": "Exact OCR match",
|
||||
"match_reason_name_amount_fuzzy": "Likely match",
|
||||
"match_reason_amount_only": "Possible match",
|
||||
"match_reason_aggregate_exact": "Aggregated payment",
|
||||
"aggregate_covers": "The amount also covers {count, plural, =1 {invoice {numbers}} other {invoices {numbers}}} exactly. Split the payment under Transactions so it is booked once.",
|
||||
"allocate_transaction": "Split under Transactions",
|
||||
"duplicate_title": "Possible duplicate payment",
|
||||
"duplicate_one": "An incoming bank transaction looks like it could be this payment. Link it instead of creating a new verifikat, or post anyway if you're sure.",
|
||||
"duplicate_many": "{count} incoming bank transactions look like they could be this payment. Link the right one instead of creating a new verifikat, or post anyway if you're sure.",
|
||||
|
||||
+16
-1
@@ -3500,7 +3500,19 @@
|
||||
"success_title": "Betalning fördelad",
|
||||
"success_description": "{count, plural, one {Fakturan} other {Fakturorna}} har bokförts mot transaktionen.",
|
||||
"cancel": "Avbryt",
|
||||
"confirm": "Bekräfta fördelning"
|
||||
"confirm": "Bekräfta fördelning",
|
||||
"already_booked_title": "Ser redan ut att vara bokförd",
|
||||
"already_booked_body_in": "Inbetalningen på {amount} täcks exakt av {count, plural, =1 {en bokförd verifikation} other {# bokförda verifikationer}} som saknar koppling till någon banktransaktion. Vanligast är att fakturan redan markerats som betald i fakturamodulen. Koppla transaktionen dit i stället för att bokföra betalningen en gång till.",
|
||||
"already_booked_body_out": "Utbetalningen på {amount} täcks exakt av {count, plural, =1 {en bokförd verifikation} other {# bokförda verifikationer}} som saknar koppling till någon banktransaktion. Vanligast är att leverantörsfakturan redan markerats som betald. Koppla transaktionen dit i stället för att bokföra betalningen en gång till.",
|
||||
"already_booked_link": "Koppla till {labels}",
|
||||
"already_booked_book_anyway": "Bokför ändå",
|
||||
"already_booked_acknowledged_title": "Bokförs ändå",
|
||||
"already_booked_acknowledged_note": "Du har valt att bokföra betalningen trots {labels}. Verifikationen skapas när du bekräftar fördelningen.",
|
||||
"already_booked_foreign_hint": "Transaktionen är i {currency}. Koppla den under Avstämning, Matcha manuellt.",
|
||||
"already_booked_link_success_title": "Transaktionen kopplad",
|
||||
"already_booked_link_success_description": "Kopplad till {labels}. Ingen ny bokföring skapad.",
|
||||
"already_booked_link_failed": "Kunde inte koppla",
|
||||
"invoiced_after_payment_badge": "Fakturerad {date}, efter betalningen"
|
||||
},
|
||||
"tx_skattekonto_card": {
|
||||
"skv_badge": "Skatteverket",
|
||||
@@ -4469,6 +4481,9 @@
|
||||
"match_reason_ocr_exact": "Exakt OCR-träff",
|
||||
"match_reason_name_amount_fuzzy": "Sannolik träff",
|
||||
"match_reason_amount_only": "Möjlig träff",
|
||||
"match_reason_aggregate_exact": "Samlad inbetalning",
|
||||
"aggregate_covers": "Beloppet täcker exakt även {count, plural, =1 {faktura {numbers}} other {fakturorna {numbers}}}. Fördela inbetalningen under Transaktioner så bokförs den en gång.",
|
||||
"allocate_transaction": "Fördela under Transaktioner",
|
||||
"duplicate_title": "Möjlig dubblettbetalning",
|
||||
"duplicate_one": "En inkommande banktransaktion ser ut att vara denna betalning. Länka den istället för att skapa en ny verifikation, eller bokför ändå om du är säker.",
|
||||
"duplicate_many": "{count} inkommande banktransaktioner ser ut att kunna vara denna betalning. Länka rätt transaktion istället för att skapa en ny verifikation, eller bokför ändå om du är säker.",
|
||||
|
||||
Reference in New Issue
Block a user