* 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>