feat(reconciliation): match one bank event to several verifikationer (1:N) (#1553) (#2029)

One bank row can now settle several vouchers: journal_entry_id stays NULL and one transaction_voucher_links row per voucher carries a signed allocated_amount slice (sum must equal the row within the link tolerance, each slice bounded by the voucher's net line on the account). linkTransactionToVouchers does the locked transaction UPDATE first and rolls back on a failed junction insert; unlink and the re-booking guards understand junction-only rows; a storno of one of the N vouchers releases the row when the remaining slices no longer sum to its amount. The worksheet's right pane becomes multi-select when exactly one bank row is picked (Koppla only at difference 0); the v1/dashboard pair schemas accept allocations; the MCP reconcile resolver and executor carry 1:N pairs; skattekonto keeps single-pointer semantics. Closes #1553
This commit is contained in:
Jakob Wennberg
2026-08-30 11:56:15 +02:00
committed by GitHub
parent 749f90fe62
commit d11d0a2e90
36 changed files with 2252 additions and 158 deletions
+22 -8
View File
@@ -2413,14 +2413,28 @@ export const PruneAccountsSchema = z
// Bank reconciliation schemas
// ============================================================
export const BankLinkSchema = z.object({
transaction_id: uuid,
journal_entry_id: uuid,
// Settlement account being reconciled. The voucher must have a line on this
// account and the transaction must belong to it. Defaults to '1930' in the
// route for back-compat.
account_number: accountNumber.optional(),
})
export const BankLinkSchema = z
.object({
transaction_id: uuid,
// One verifikat (1:1, or N:1 when other transactions already point at it).
journal_entry_id: uuid.optional(),
// Or several verifikat settled by this one transaction (1:N, #1553): the
// signed slice per verifikat in the transaction's sign convention. The
// slices must sum to the transaction amount; the engine enforces it.
allocations: z
.array(z.object({ journal_entry_id: uuid, amount: z.number() }))
.min(2)
.max(50)
.optional(),
// Settlement account being reconciled. The voucher must have a line on this
// account and the transaction must belong to it. Defaults to '1930' in the
// route for back-compat.
account_number: accountNumber.optional(),
})
.refine((v) => (v.journal_entry_id ? !v.allocations : Boolean(v.allocations)), {
message: 'Ange journal_entry_id eller allocations, inte båda.',
path: ['journal_entry_id'],
})
export const BankUnlinkSchema = z.object({
transaction_id: uuid,