feat(reconciliation): the Avstämning page: one body for every account with an outside truth (#1834)
* feat(reconciliation): skattekonto bridge engine, sync-time twin proposals, account-keyed facade The engine half of the reconciliation page (design: Avstämningsmotorn). - lib/reconciliation/skattekonto-reconciliation.ts: getSkattekontoReconciliationStatus anchors at the saldo snapshot and returns the bridge (saldo hos Skatteverket, händelser som saknas, 1630-rader utan händelse, ignorerade, ingående skillnad, bokfört), the item buckets the page shows (proposed, unmatched external, unmatched ledger, matched, ignored, upcoming), opening_difference, unexplained_difference (0,00 by construction when data is consistent), dead-link handling (a link to a reversed/draft entry counts as unlinked and is flagged), awaiting_external for ledger lines within 5 days of the snapshot, staleness, and a window that scopes item lists without hiding older rows. Core reads skattekonto_transactions and the extension's snapshot row directly; no @/extensions import. - lib/reconciliation/gl-balance.ts: one ledger-balance helper with the trial-balance predicate status IN (posted, reversed). The drift check summed posted only, which misstated 1630 for any company with a storno on the account; skattekonto-drift.ts now delegates to the helper. - Proposals at sync: migration 20260823120000 adds suggested_journal_entry_id / suggested_at (ON DELETE SET NULL, partial index on open rows); the sync calls refreshSkattekontoProposals after the upsert. findMatchSuggestionsBulk now assigns one-to-one across rows (AGI period first, then nearest date) and falls back to an entry whose 1630 lines net to the amount (split lines); a proposal is never a link. - lib/reconciliation/service.ts + schemas.ts: the account-keyed facade (bank:<cash_account_id> | skattekonto | manual:NNNN) with listReconciliationAccounts (enabled cash accounts folded per IBAN, skattekonto when configured) and getAccountStatus dispatching to the bank engine or the new one; shared Zod shapes for the v1 registry, MCP schemas and the UI (PR 2). Tests: identity on a mixed fixture, storno pair, stale snapshot, awaiting window, window scoping, failed ledger read, live-linked entries never proposed; matcher one-to-one and split-line cases; proposal refresh writes/clears; service dedupe and dispatch. No UI in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): roundOre instead of inline öre rounding (guard ratchet) The antipattern ratchet counts Math.round(x*100)/100; the new engine used it in five places. Switch to roundOre from @/lib/money and ratchet the baseline down by the three occurrences this removes net of the matcher rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): three doors over one engine: dashboard routes, v1 API and MCP tools for account-keyed reconciliation PR 2 of the Avstämning build (design: Avstämning via API och MCP). Every door calls lib/reconciliation/{service,items,actions}.ts; none re-implements a link. - lib/reconciliation/items.ts: listAccountItems per account_key, the page's buckets (proposed, unmatched_external, unmatched_ledger, matched, ignored, upcoming), limit/offset; skattekonto from the engine, bank from the scoped transactions + unlinked GL lines (netted per entry). - lib/reconciliation/actions.ts: matchPairs (pairs or use_proposals, dry run, partial success with codes), unmatchLink, setItemIgnored; emits reconciliation.matched / reconciliation.unmatched. - lib/skatteverket/skattekonto-link.ts: canonical core link semantics for a skattekonto row (single line or entry net on 1630, live-link guard, race-safe update, unlink, ignore); the extension keeps its own matchSkattekontoToEntry until its tests are ported. - Dashboard routes /api/reconciliation/accounts[...]: list, status, items, links (POST), links/{linkId} (DELETE), items/{itemId}/ignore (POST); apply directly (a human clicked). - v1 routes /api/v1/companies/{id}/reconciliation/accounts[...]: same six, withApiV1, new scopes reconciliation:read / reconciliation:write (write is a staging scope for SoD), Idempotency-Key + dry_run on writes, registered for OpenAPI, load-routes, skills/accounted-api regenerated. Legacy bank routes and their transactions:* scopes unchanged. - MCP: gnubok_get_reconciliation_status takes account_key (legacy bank path untouched), new gnubok_list_reconciliation_items (default catalog), gnubok_reconcile_match (stages reconciliation_match, preflight = status) and gnubok_reconcile_unmatch (stages reconciliation_unmatch), both search-only to stay under the tools/list payload ceiling; gnubok_link_transaction_to_journal_entry moved to search. Executors in commit.ts; risk tiers medium/low; migration pair 20260823130000/130001 adds the two op types to the CHECK constraint (value list = live prod as of 2026-08-23 + the two); close_period loadout updated. Tests: service/actions/items/link unit tests, v1 route tests (401/403/400/404/ happy, idempotency, dry run), dashboard route tests, MCP tool tests + the guard suite (payload ceiling, descriptions, staging meta, qualified ids). Guards and apiskill:check green; no type errors in changed files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): refresh the v1 spec snapshot and keep the ignore update readable by the phantom-column guard The six new v1 reconciliation endpoints and the two new scopes were not recorded in the spec snapshot, and setSkattekontoRowIgnored updated through one conditional payload, which the phantom-column scanner cannot read (ceiling 380 -> 381). Two literal payloads instead; snapshot updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reconciliation): the Avstämning page, one body for every account with an outside truth /reconciliation in Arbeta (after Transaktioner), on the approved layout: an account rail on the left (bank accounts and the skattekonto, logo or monogram, last fetch, status dot, URL-owned selection), and for the selected account four tiles (outside, ledger, difference, unexplained), the bridge that explains the difference, an actions row (link the proposed pairs, book the unbooked skattekonto events, run the bank matcher) and a full-width table banded by bucket with proposal rows linkable one by one. Every read and write goes through the PR 2 dashboard routes, so the page shows exactly what the v1 API and the MCP tools see. Also: nav item, command palette entry, sv/en strings. Period picker, manual match mode and sign-off are deliberately not here (PR 4/5). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
3a62c5419e
commit
c609dbb228
@@ -91,6 +91,7 @@
|
||||
"suppliers": "Suppliers",
|
||||
"review": "Review",
|
||||
"transactions": "Transactions",
|
||||
"reconciliation": "Reconciliation",
|
||||
"bookkeeping": "Bookkeeping",
|
||||
"chart_of_accounts": "Chart of accounts",
|
||||
"dimensions": "Cost centres & projects",
|
||||
@@ -7793,6 +7794,82 @@
|
||||
"skattekonto_body": "Connect with BankID and the balance and events are fetched continuously, ready to book.",
|
||||
"skattekonto_primary": "Connect Skatteverket"
|
||||
},
|
||||
"reconciliation": {
|
||||
"title": "Reconciliation",
|
||||
"help_text": "Every account with a truth outside the ledger, the bank or the tax account, is reconciled here. What exists outside is compared with what is booked, each row on one side is linked to a row on the other, and whatever remains is explained or booked.",
|
||||
"rail_heading": "Accounts",
|
||||
"rail_synced": "fetched {date}",
|
||||
"rail_never_synced": "never fetched",
|
||||
"rail_superseded": "replaced",
|
||||
"state_reconciled": "Reconciled",
|
||||
"state_open": "Open items",
|
||||
"state_stale": "Stale data",
|
||||
"state_not_configured": "Not connected",
|
||||
"empty_title": "Nothing to reconcile yet",
|
||||
"empty_body": "Connect the bank or Skatteverket and the accounts show up here.",
|
||||
"empty_connect_bank": "Connect bank",
|
||||
"empty_connect_skv": "Connect Skatteverket",
|
||||
"load_failed": "Could not load the reconciliation. Try again in a moment.",
|
||||
"tile_external_skv": "Balance at Skatteverket",
|
||||
"tile_external_bank": "Bank transactions in the period",
|
||||
"tile_ledger": "Booked on {account}",
|
||||
"tile_ledger_bank": "Booked on {account} in the period",
|
||||
"tile_difference": "Difference",
|
||||
"tile_unexplained": "Unexplained",
|
||||
"tile_per": "as of {date}",
|
||||
"tile_synced": "fetched {date}",
|
||||
"tile_unknown": "unknown",
|
||||
"reconciled_line": "Everything is explained: the difference is covered by the rows below.",
|
||||
"unexplained_line": "{amount} is not explained yet.",
|
||||
"stale_line": "The data from {source} is older than seven days. Fetch again before trusting the numbers.",
|
||||
"source_skv": "Skatteverket",
|
||||
"source_bank": "the bank",
|
||||
"bridge_heading": "How it adds up",
|
||||
"action_match_proposals": "Link {count} proposed",
|
||||
"action_book_rows": "Book {count} events",
|
||||
"action_open_skattekonto": "Open the tax account",
|
||||
"action_run_bank_matcher": "Match automatically",
|
||||
"action_open_bank_view": "Open bank reconciliation",
|
||||
"older_unmatched": "{count} older unmatched rows before the period start",
|
||||
"older_show": "Show",
|
||||
"col_date": "Date",
|
||||
"col_event": "Event",
|
||||
"col_amount": "Amount",
|
||||
"col_voucher": "Voucher",
|
||||
"bucket_proposed": "To link: proposed pairs",
|
||||
"bucket_unmatched_external_skv": "At Skatteverket, missing in the ledger",
|
||||
"bucket_unmatched_external_bank": "At the bank, missing in the ledger",
|
||||
"bucket_unmatched_ledger_skv": "In the ledger, missing at Skatteverket",
|
||||
"bucket_unmatched_ledger_bank": "In the ledger, missing at the bank",
|
||||
"bucket_matched": "Linked",
|
||||
"bucket_ignored": "Ignored",
|
||||
"bucket_upcoming": "Upcoming at Skatteverket",
|
||||
"row_match": "Link",
|
||||
"row_unmatch": "Unlink",
|
||||
"row_book": "Book",
|
||||
"row_review": "Review",
|
||||
"row_ignore": "Ignore",
|
||||
"row_unignore": "Restore",
|
||||
"row_open": "Open",
|
||||
"chip_awaiting": "awaiting {source}",
|
||||
"chip_draft": "draft",
|
||||
"chip_reversed": "reversed",
|
||||
"chip_missing": "missing",
|
||||
"chip_proposal_via_total": "via the voucher total",
|
||||
"confidence": "{percent}% confident",
|
||||
"show_all": "Show all {count}",
|
||||
"hide": "Hide",
|
||||
"all_clear": "No open items. Everything on the account is linked.",
|
||||
"toast_matched": "{applied} linked",
|
||||
"toast_matched_skipped": "{applied} linked, {skipped} skipped",
|
||||
"toast_unmatched": "Link removed",
|
||||
"toast_ignored": "Row ignored",
|
||||
"toast_unignored": "Row restored",
|
||||
"toast_booked": "{count} booked",
|
||||
"toast_book_partial": "{ok} booked, {failed} failed",
|
||||
"toast_failed": "That did not work",
|
||||
"truncated": "Showing the first {count} rows per group."
|
||||
},
|
||||
"skattekonto": {
|
||||
"help_text": "The balance and events are fetched from Skatteverket and synced automatically every night. Completed events are booked against 1630 Skattekonto, usually automatically; anything that cannot be matched is flagged in the list. Pay in via bankgiro 5050-1055 with your OCR number.",
|
||||
"copy": "copy",
|
||||
|
||||
@@ -91,6 +91,7 @@
|
||||
"suppliers": "Leverantörer",
|
||||
"review": "Granskning",
|
||||
"transactions": "Transaktioner",
|
||||
"reconciliation": "Avstämning",
|
||||
"bookkeeping": "Bokföring",
|
||||
"chart_of_accounts": "Kontoplan",
|
||||
"dimensions": "Kostnadsställen & projekt",
|
||||
@@ -7793,6 +7794,82 @@
|
||||
"skattekonto_body": "Anslut med BankID så hämtas saldo och händelser löpande, färdiga att bokföra.",
|
||||
"skattekonto_primary": "Anslut Skatteverket"
|
||||
},
|
||||
"reconciliation": {
|
||||
"title": "Avstämning",
|
||||
"help_text": "Varje konto med en sanning utanför bokföringen, banken eller skattekontot, stäms av här. Det som finns utanför jämförs med det som är bokfört, varje rad på ena sidan kopplas till en rad på den andra, och det som blir kvar förklaras eller bokförs.",
|
||||
"rail_heading": "Konton",
|
||||
"rail_synced": "hämtat {date}",
|
||||
"rail_never_synced": "aldrig hämtat",
|
||||
"rail_superseded": "ersatt",
|
||||
"state_reconciled": "Avstämt",
|
||||
"state_open": "Öppna poster",
|
||||
"state_stale": "Gamla uppgifter",
|
||||
"state_not_configured": "Ej kopplat",
|
||||
"empty_title": "Inget att stämma av ännu",
|
||||
"empty_body": "Koppla banken eller Skatteverket så dyker kontona upp här.",
|
||||
"empty_connect_bank": "Koppla bank",
|
||||
"empty_connect_skv": "Koppla Skatteverket",
|
||||
"load_failed": "Kunde inte hämta avstämningen. Försök igen om en stund.",
|
||||
"tile_external_skv": "Saldo hos Skatteverket",
|
||||
"tile_external_bank": "Banktransaktioner i perioden",
|
||||
"tile_ledger": "Bokfört på {account}",
|
||||
"tile_ledger_bank": "Bokfört på {account} i perioden",
|
||||
"tile_difference": "Differens",
|
||||
"tile_unexplained": "Oförklarat",
|
||||
"tile_per": "per {date}",
|
||||
"tile_synced": "hämtat {date}",
|
||||
"tile_unknown": "okänt",
|
||||
"reconciled_line": "Allt är förklarat: differensen täcks av raderna nedan.",
|
||||
"unexplained_line": "{amount} är inte förklarat ännu.",
|
||||
"stale_line": "Uppgifterna från {source} är äldre än sju dagar. Hämta igen innan du litar på siffrorna.",
|
||||
"source_skv": "Skatteverket",
|
||||
"source_bank": "banken",
|
||||
"bridge_heading": "Så hänger det ihop",
|
||||
"action_match_proposals": "Koppla {count} föreslagna",
|
||||
"action_book_rows": "Bokför {count} händelser",
|
||||
"action_open_skattekonto": "Öppna skattekontot",
|
||||
"action_run_bank_matcher": "Matcha automatiskt",
|
||||
"action_open_bank_view": "Öppna bankavstämningen",
|
||||
"older_unmatched": "{count} äldre omatchade rader före periodstart",
|
||||
"older_show": "Visa",
|
||||
"col_date": "Datum",
|
||||
"col_event": "Händelse",
|
||||
"col_amount": "Belopp",
|
||||
"col_voucher": "Verifikat",
|
||||
"bucket_proposed": "Att koppla: föreslagna par",
|
||||
"bucket_unmatched_external_skv": "Hos Skatteverket, saknas i bokföringen",
|
||||
"bucket_unmatched_external_bank": "På banken, saknas i bokföringen",
|
||||
"bucket_unmatched_ledger_skv": "I bokföringen, saknas hos Skatteverket",
|
||||
"bucket_unmatched_ledger_bank": "I bokföringen, saknas på banken",
|
||||
"bucket_matched": "Kopplade",
|
||||
"bucket_ignored": "Ignorerade",
|
||||
"bucket_upcoming": "Kommande hos Skatteverket",
|
||||
"row_match": "Koppla",
|
||||
"row_unmatch": "Koppla bort",
|
||||
"row_book": "Bokför",
|
||||
"row_review": "Granska",
|
||||
"row_ignore": "Ignorera",
|
||||
"row_unignore": "Återställ",
|
||||
"row_open": "Öppna",
|
||||
"chip_awaiting": "väntar på {source}",
|
||||
"chip_draft": "utkast",
|
||||
"chip_reversed": "makulerat",
|
||||
"chip_missing": "saknas",
|
||||
"chip_proposal_via_total": "via verifikatets summa",
|
||||
"confidence": "{percent} % säker",
|
||||
"show_all": "Visa alla {count}",
|
||||
"hide": "Dölj",
|
||||
"all_clear": "Inga öppna poster. Allt på kontot är kopplat.",
|
||||
"toast_matched": "{applied} kopplade",
|
||||
"toast_matched_skipped": "{applied} kopplade, {skipped} hoppades över",
|
||||
"toast_unmatched": "Kopplingen togs bort",
|
||||
"toast_ignored": "Raden ignoreras",
|
||||
"toast_unignored": "Raden är tillbaka",
|
||||
"toast_booked": "{count} bokförda",
|
||||
"toast_book_partial": "{ok} bokförda, {failed} misslyckades",
|
||||
"toast_failed": "Det gick inte",
|
||||
"truncated": "Visar de första {count} raderna per grupp."
|
||||
},
|
||||
"skattekonto": {
|
||||
"help_text": "Saldot och händelserna hämtas från Skatteverket och synkas automatiskt varje natt. Genomförda händelser bokförs mot 1630 Skattekonto, oftast automatiskt; det som inte kan matchas flaggas i listan. Betala in via bankgiro 5050-1055 med ditt OCR-nummer.",
|
||||
"copy": "kopiera",
|
||||
|
||||
Reference in New Issue
Block a user