feat(reconciliation): manual adapter for the whole balance sheet (Reko bilagor, PR 1) (#1854)

* feat(reconciliation): manual adapter so the whole balance sheet is reconcilable and signable (Reko bilagor, PR 1)

Every class 1-2 account the bank and skattekonto adapters do not own now
appears on the Avstämning page under "Övriga balanskonton" with IB, movement
and UB through the balansdag, a system specification where one exists
(1510 kundreskontra, 2440 leverantörsreskontra, 2920/2940 semesterlöneskuld)
and, for every other account, the balance the signer states from their
underlag at sign-off. Same three doors as before: dashboard routes, v1 API
and the MCP tools take manual:<BAS> keys and an external_balance.

The ledger side is computed per fiscal period via generateTrialBalance,
never as an all-history sum: year-end re-books every balance account in an
opening_balance verifikat, so an all-history sum counts a closed year twice.

A stated external_balance is refused (EXTERNAL_BALANCE_NOT_ALLOWED) wherever
the system already has an outside truth, so it can never hide a difference.

No migration: account_reconciliations already accepts manual:NNNN keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

* chore(api-skill): regenerate banking reference for the sign-off external_balance field

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz

---------

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-25 09:23:14 +02:00
committed by GitHub
co-authored by Claude Fable 5 Jakob Wennberg
parent 1e6e19afe9
commit ebbdf96b74
22 changed files with 1312 additions and 116 deletions
@@ -11,6 +11,8 @@ const SignoffBodySchema = z.object({
through_date: z.string().regex(ISO_DATE_RE),
note: z.string().max(2000).nullable().optional(),
force: z.boolean().optional(),
/** Manual accounts without a system specification: the balance per the signer's underlag, ledger sign. */
external_balance: z.number().finite().nullable().optional(),
dry_run: z.boolean().optional(),
})
@@ -65,7 +67,12 @@ export const POST = withRouteContext<{ params: Promise<{ accountKey: string }> }
companyId,
user.id,
accountKey,
{ through_date: parsed.data.through_date, note: parsed.data.note ?? null, force: parsed.data.force },
{
through_date: parsed.data.through_date,
note: parsed.data.note ?? null,
force: parsed.data.force,
external_balance: parsed.data.external_balance ?? null,
},
{ dryRun: parsed.data.dry_run === true },
)
if (!result) {
@@ -93,11 +93,39 @@ describe('dashboard sign-off routes', () => {
'company-1',
'user-1',
'skattekonto',
{ through_date: '2026-07-31', note: 'ok', force: undefined },
{ through_date: '2026-07-31', note: 'ok', force: undefined, external_balance: null },
{ dryRun: true },
)
})
it('POST forwards a stated external_balance for a manual account and 400s a non-numeric one', async () => {
const res = await signPOST(
createMockRequest('http://localhost/api/reconciliation/accounts/manual:2350/signoff', { method: 'POST', body: {
through_date: '2026-07-31',
external_balance: -250000,
note: 'Enligt engagemangsbesked',
} }),
p({ accountKey: 'manual:2350' }),
)
expect(res.status).toBe(200)
expect(signMock).toHaveBeenCalledWith(
supabase,
'company-1',
'user-1',
'manual:2350',
{ through_date: '2026-07-31', note: 'Enligt engagemangsbesked', force: undefined, external_balance: -250000 },
{ dryRun: false },
)
const bad = await signPOST(
createMockRequest('http://localhost/api/reconciliation/accounts/manual:2350/signoff', { method: 'POST', body: {
through_date: '2026-07-31',
external_balance: '250 000',
} }),
p({ accountKey: 'manual:2350' }),
)
expect(bad.status).toBe(400)
})
it('POST 400s a missing or malformed through_date', async () => {
const res = await signPOST(
createMockRequest('http://localhost/api/reconciliation/accounts/skattekonto/signoff', { method: 'POST', body: { through_date: '31/07/2026' } }),