feat(reconciliation): manual N:1 matching: two-pane worksheet + group links (#1851)

'Matcha manuellt' as designed: outside rows on the left (multi-select),
verifikat without an outside row on the right (single-select), the
selection's arithmetic in the footer, and one Koppla that is enabled only
when the difference is 0. Mode lives in the URL (?mode=match).

Engine: a pair is now one OR MANY outside rows against one verifikat.
Bank groups link per transaction (manualLink allows N:1 by design, so
partial success is reported per row). Skattekonto groups go through the
new linkSkattekontoRows: the verifikat's 1630 side must settle the sum,
one guarded UPDATE links the whole group, and a partial hit is rolled
back as LINK_RACE. 1:M stays UNSUPPORTED_PAIR_SHAPE until the residual
link table (6c). v1 pitfalls + API skill regenerated.

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:
Jakob Wennberg
2026-08-24 16:24:07 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 4d6dd68df4
commit 34ad1b1936
11 changed files with 633 additions and 42 deletions
@@ -61,7 +61,7 @@ registerEndpoint({
doNotUseFor:
'Booking new verifikat for rows that have no counterpart (use the transactions or skattekonto booking endpoints); reconciling across accounts.',
pitfalls: [
'This version links one outside row to one verifikat per pair; other shapes come back as UNSUPPORTED_PAIR_SHAPE, never silently reduced.',
'A pair is one OR MANY outside rows against exactly one verifikat (bank: independent links per transaction; skattekonto: all-or-nothing, the rows must sum to what the verifikat settles). One row against several verifikat is UNSUPPORTED_PAIR_SHAPE until residual booking lands, never silently reduced.',
'A pair must close to the row\'s amount on the expected side (a single matching line, or the entry\'s lines on the account netting to it); a fee or rounding difference is PAIR_NOT_CLOSED here and needs a residual booking first.',
'Links never touch the ledger, so they succeed in locked periods; unlink with DELETE .../links/{linkId} (linkId = the outside row id).',
'Idempotency-Key is required; repeating the same key replays the first response.',