feat(reconciliation): match migrated bank history against imported SIE verifikat (#1598)

* feat(reconciliation): match migrated bank history against imported SIE verifikat

A first-class Fortnox/SIE migrator path: after SIE import plus bank connect
or bank CSV upload, historical bank rows are auto-matched (>= 0.9) or
suggestion-matched (0.75-0.89, persisted for review) against the imported
verifikat, with a guided review surface, instead of landing as anonymous
"Att bokfora" rows.

Phase 0: per-cash-account unattended sweep (fixes #1298 cross-account
pooling); widen payment_match_log action CHECK with
linked_to_existing_voucher (silently unlogged since March).
Phase 1: potential_journal_entry_id/method/confidence on transactions with
CHECK + invalidation triggers; persistSuggestions in runReconciliation;
sweep after bank CSV import with SIE overlap (suppressing
auto-categorization); sweep summaries stamped on bank_connections and
bank_file_imports; POST /api/reconciliation/bank/confirm-suggestions with
per-pair server-side revalidation (voucher consumption + bank-leg amount
and direction).
Phase 2: "Granska forslag" review tab on Transactions with chunked bulk
confirm, per-row fallbacks, "Kor matchning igen" (all_accounts sweep mode,
mutually exclusive with dry_run), attn line, pre-migration row marker.
Phase 3: ImportResultStep dual CTA (bank connect + CSV), migrator variant
of the account-picker #917 nudge, sweep outcome on the onboarding
checklist bank step.

Non-selection apply runs on /api/reconciliation/bank/run now floor at 0.9
and persist the review band instead of auto-committing fuzzy matches.
Migrations already applied to staging under the same versions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reconciliation): resolve PR review findings in one pass

Swedish accounting review (both previously-deferred holes closed):
- runReconciliation's >= 0.9 auto-apply now writes 'matched' to
  payment_match_log (behandlingshistorik, BFNAR 2013:2 kap 8); the bus
  event alone lands in the 30-day event_log and is not an audit record.
- The three match-route storno-conflict branches detach reconciliation
  links via unlinkReconciliation instead of storno-reversing the linked
  verifikat: a reconciliation link points at an independent verifikat
  that may evidence other affarshandelser, and a wholesale reversal is
  an over-broad rattelse (BFL 5 kap 5 §).
- Historical gap quantified on prod (read-only, recorded in DECISIONS):
  762 unlogged manual links across 52 companies since 2026-03-23.

CodeRabbit:
- confirm-suggestions route: maxDuration 300 for full 500-item batches.
- AccountPickerDialog: migrator-nudge buttons set lookbackTouched so the
  async gap-fill probe cannot override an explicit choice.
- enable-banking post-backfill sweep: persistSuggestions so the review
  band is not dropped.
- bank-file execute: sie_sweep stamp errors are logged, not swallowed.
- ImportResultStep: sandbox keeps the CSV CTA (file import works there).
- payment_match_log CHECK swap: NOT VALID + VALIDATE, no table scan
  under ACCESS EXCLUSIVE.
- logMatchEvent calls awaited (serverless can freeze unawaited work).
- DECISIONS.md stale version reference annotated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reconciliation): defer reconciliation-link detach until the match commits

Round-2 review findings:
- CodeRabbit: the eager unlinkReconciliation call could orphan a
  transaction if the match flow failed after it. All three match routes
  now persist NOTHING up front: the final transaction update overwrites
  journal_entry_id and clears reconciliation_method in the same write,
  so any failure in between leaves the existing link intact. The release
  is logged as 'unmatched' after the commit.
- Swedish review: the auto_suggested logMatchEvent in runReconciliation
  is now awaited like every other audit write.
- DECISIONS entry split into compliance/CodeRabbit lines and updated to
  describe the deferred detach.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(reconciliation): literal reconciliation_method payloads for the phantom-column scanner

The conditional spreads introduced with the deferred detach pushed the
scanner's unresolvable-expression count past its ceiling (380 > 378).
reconciliation_method: null is correct unconditionally on a confirmed
invoice/supplier match (null is already the value on every row that was
not reconciliation-linked), so the payloads become plain literals the
guard can verify. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-13 23:12:27 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 07e89d9b52
commit 08440fed94
35 changed files with 3223 additions and 99 deletions
+36
View File
@@ -1451,6 +1451,7 @@
"step_books_sie": "+ SIE",
"step_bank_title": "Connect the bank",
"step_bank_description": "Fetch transactions automatically, or import a bank statement.",
"step_bank_sweep_note": "{matched, plural, one {# matched against the import} other {# matched against the import}}, {toReview, plural, one {# to review} other {# to review}}",
"step_bank_action": "Connect the bank",
"step_assistant_title": "Build your bookkeeping assistant",
"step_assistant_beta": "Beta",
@@ -2702,6 +2703,7 @@
"delete_aria": "Delete transaction",
"edit_title_aria": "Edit title",
"edited_badge": "edited",
"pre_migration_marker": "from the period before your migration",
"original_name_tooltip": "Original bank name: {name}",
"edit_title_dialog_title": "Edit transaction title",
"edit_title_warning": "Are you sure you want to edit the title of this transaction?",
@@ -3291,6 +3293,23 @@
"saving": "Saving...",
"save": "Save transaction"
},
"tx_review": {
"intro": "{count, plural, one {# bank transaction matches a voucher in your imported bookkeeping. Confirm the link and nothing gets booked twice.} other {# bank transactions match vouchers in your imported bookkeeping. Confirm the links and nothing gets booked twice.}}",
"rerun": "Run matching again",
"confirm_all": "Confirm all ({count})",
"empty_title": "No suggestions to review",
"empty_description": "All match suggestions are handled. Remaining transactions are under To book.",
"th_date": "Date",
"th_description": "Description",
"th_amount": "Amount",
"th_suggestion": "Suggested voucher",
"th_actions": "Actions",
"suggestion_gone": "The suggestion is no longer valid",
"confirm": "Confirm",
"row_menu": "More actions",
"open_match_dialog": "Pick another voucher…",
"reject": "Reject the suggestion"
},
"tx_history": {
"search_placeholder": "Search transactions...",
"filter_all": "All",
@@ -5463,6 +5482,17 @@
"skv_err_commit_failed": "draft created, not posted",
"skv_err_other": "failed",
"mode_all": "All",
"mode_review": "Review suggestions",
"review_attn_body": "{count, plural, one {# historical bank transaction matches} other {# historical bank transactions match}} your imported bookkeeping.",
"review_attn_cta": "Review the suggestions",
"review_confirm_failed": "Could not link the suggestions",
"review_confirm_done_title": "{count, plural, one {# transaction linked} other {# transactions linked}}",
"review_confirm_done_description": "Linked to existing vouchers. No new bookkeeping was created.",
"review_confirm_done_skipped": "{count, plural, one {# was skipped} other {# were skipped}}: the suggestion had changed since it was created.",
"review_reject_failed": "Could not reject the suggestion",
"review_rerun_failed": "Matching could not run",
"review_rerun_done_title": "Matching finished",
"review_rerun_done_description": "{applied, plural, one {# transaction was linked automatically} other {# transactions were linked automatically}}, {suggested, plural, one {# new suggestion to review} other {# new suggestions to review}}.",
"footer_to_handle": "{count, plural, =0 {Nothing to handle} =1 {1 to handle} other {# to handle}}",
"recon_attn": "{count, plural, one {1 unbooked bank transaction: the bank reconciliation can find automatic matches against existing vouchers.} other {# unbooked bank transactions: the bank reconciliation can find automatic matches against existing vouchers.}}",
"recon_attn_action": "Preview matches"
@@ -6888,7 +6918,13 @@
"reveal_skipped": "{count, plural, one {# voucher was skipped, see details below.} other {# vouchers were skipped, see details below.}}",
"reveal_bridge": "The history is in place. What's missing is the present: the bank.",
"reveal_cta_bank": "Connect the bank",
"reveal_cta_csv": "Upload bank statement (CSV)",
"reveal_cta_open": "Open Accounted",
"next_steps_title": "Next step: fetch your bank history",
"next_steps_match_copy": "Connect your bank or upload bank statements, and we match the bank history against what you just imported.",
"next_steps_cta_bank": "Connect your bank",
"next_steps_cta_csv": "Upload bank statement (CSV)",
"next_steps_window_hint": "The bank connection fetches recent history (most banks stop around 90 days). For older periods, upload bank statements as CSV, matching works the same way.",
"vat_review_title": "{count, plural, one {# account has no VAT treatment} other {# accounts have no VAT treatment}}",
"vat_review_description": "Set VAT treatments on imported revenue and purchase accounts so the VAT return uses the correct boxes.",
"vat_review_action": "Review VAT treatments in the chart",
+36
View File
@@ -1451,6 +1451,7 @@
"step_books_sie": "+ SIE",
"step_bank_title": "Koppla banken",
"step_bank_description": "Hämta transaktioner automatiskt, eller importera ett kontoutdrag.",
"step_bank_sweep_note": "{matched, plural, one {# matchad mot importen} other {# matchade mot importen}}, {toReview, plural, one {# att granska} other {# att granska}}",
"step_bank_action": "Koppla banken",
"step_assistant_title": "Bygg din bokföringsassistent",
"step_assistant_beta": "Beta",
@@ -2702,6 +2703,7 @@
"delete_aria": "Ta bort transaktion",
"edit_title_aria": "Ändra titel",
"edited_badge": "redigerad",
"pre_migration_marker": "från perioden före din migrering",
"original_name_tooltip": "Bankens originalnamn: {name}",
"edit_title_dialog_title": "Ändra transaktionens titel",
"edit_title_warning": "Är du säker på att du vill ändra titeln på den här transaktionen?",
@@ -3291,6 +3293,23 @@
"saving": "Sparar...",
"save": "Spara transaktion"
},
"tx_review": {
"intro": "{count, plural, one {# banktransaktion matchar ett verifikat i din importerade bokföring. Bekräfta kopplingen så bokförs ingenting dubbelt.} other {# banktransaktioner matchar verifikat i din importerade bokföring. Bekräfta kopplingarna så bokförs ingenting dubbelt.}}",
"rerun": "Kör matchning igen",
"confirm_all": "Bekräfta alla ({count})",
"empty_title": "Inga förslag att granska",
"empty_description": "Alla matchförslag är hanterade. Kvarvarande transaktioner hittar du under Att bokföra.",
"th_date": "Datum",
"th_description": "Beskrivning",
"th_amount": "Belopp",
"th_suggestion": "Föreslaget verifikat",
"th_actions": "Åtgärder",
"suggestion_gone": "Förslaget är inte längre giltigt",
"confirm": "Bekräfta",
"row_menu": "Fler åtgärder",
"open_match_dialog": "Välj annat verifikat…",
"reject": "Avvisa förslaget"
},
"tx_history": {
"search_placeholder": "Sök transaktioner...",
"filter_all": "Alla",
@@ -5463,6 +5482,17 @@
"skv_err_commit_failed": "utkast skapat, ej bokfört",
"skv_err_other": "misslyckades",
"mode_all": "Alla",
"mode_review": "Granska förslag",
"review_attn_body": "{count, plural, one {# historisk banktransaktion matchar} other {# historiska banktransaktioner matchar}} din importerade bokföring.",
"review_attn_cta": "Granska förslagen",
"review_confirm_failed": "Kunde inte koppla förslagen",
"review_confirm_done_title": "{count, plural, one {# transaktion kopplad} other {# transaktioner kopplade}}",
"review_confirm_done_description": "Kopplade till befintliga verifikat. Ingen ny bokföring skapades.",
"review_confirm_done_skipped": "{count, plural, one {# hoppades över} other {# hoppades över}}: förslaget hade ändrats sedan det skapades.",
"review_reject_failed": "Kunde inte avvisa förslaget",
"review_rerun_failed": "Matchningen kunde inte köras",
"review_rerun_done_title": "Matchning klar",
"review_rerun_done_description": "{applied, plural, one {# transaktion kopplades automatiskt} other {# transaktioner kopplades automatiskt}}, {suggested, plural, one {# nytt förslag att granska} other {# nya förslag att granska}}.",
"footer_to_handle": "{count, plural, =0 {Inget att hantera} =1 {1 att hantera} other {# att hantera}}",
"recon_attn": "{count, plural, one {1 obokförd banktransaktion: bankavstämningen kan hitta automatiska träffar mot befintliga verifikationer.} other {# obokförda banktransaktioner: bankavstämningen kan hitta automatiska träffar mot befintliga verifikationer.}}",
"recon_attn_action": "Förhandsgranska träffar"
@@ -6888,7 +6918,13 @@
"reveal_skipped": "{count, plural, one {# verifikat hoppades över, se detaljer nedan.} other {# verifikat hoppades över, se detaljer nedan.}}",
"reveal_bridge": "Historiken är på plats. Det som saknas är nuet: banken.",
"reveal_cta_bank": "Koppla banken",
"reveal_cta_csv": "Ladda upp kontoutdrag (CSV)",
"reveal_cta_open": "Öppna Accounted",
"next_steps_title": "Nästa steg: hämta bankhistoriken",
"next_steps_match_copy": "Koppla din bank eller ladda upp kontoutdrag, så matchar vi bankhistoriken mot det du just importerade.",
"next_steps_cta_bank": "Koppla din bank",
"next_steps_cta_csv": "Ladda upp kontoutdrag (CSV)",
"next_steps_window_hint": "Bankkopplingen hämtar den senaste tidens historik (de flesta banker stannar vid ca 90 dagar). För äldre perioder laddar du upp kontoutdrag som CSV, matchningen fungerar likadant.",
"vat_review_title": "{count, plural, one {# konto saknar momskod} other {# konton saknar momskod}}",
"vat_review_description": "Sätt momskod på importerade intäkts- och inköpskonton så att momsdeklarationen hamnar i rätt ruta.",
"vat_review_action": "Granska momskoder i kontoplanen",