f1230282a9
* feat(bookkeeping): verifikationsserie per bankkonto for bank-transaction bookings A company running several bank accounts (main bank on A, company card on M, both imported via CSV) could not route each account's bookings into its own series: every bank_transaction booking took the single company-wide default from default_voucher_series_per_source_type. - cash_accounts.voucher_series (nullable, single letter): per-account override, editable under Inställningar → Bokföring → Verifikationsserier per bankkonto (new PATCH /api/cash-accounts/[id]). - resolveCashAccountVoucherSeries(): step 2 of the resolution order (explicit pick → account override → per-type map → A). Wired into the book route and createTransactionJournalEntry, which covers categorize, the agent, pending operations and the v1 API. - Booking dialog gets the series picker, seeded from the server via /voucher-sequences/next?source_type&cash_account_id so dialog and route can never disagree. An unresolved embedded picker omits voucher_series so a stray 'A' never overrides the account's series. Scope: bank_transaction bookings only. Invoice settlements matched from the bank keep their payment series; bulk-book resolves inside its RPC (see DECISIONS.md). Migration applied to staging as 20260902121420. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWSLbQc3jgpfqnxWe6nteh * fix(bookkeeping): audit and document the per-bankkonto series, tighten preview and PATCH Consolidated pass over the PR #2160 findings (skeptics, CodeRabbit, Swedish compliance review): - Behandlingshistorik (BFNAR 2013:2 p. 9.16): changing cash_accounts.voucher_series is a behandlingsregel that outranks the audited per-type map. New trigger audit_cash_accounts_voucher_series (UPDATE only, WHEN the series changes, so bank-sync churn never logs), cash_accounts added to AUDITED_TABLES and the audit_log filter, "Bankkonto ... Verifikationsserie: (tomt) -> M" events in the report, pg-real test. Applied to staging as 20260902124513. - Systemdokumentation (p. 9.2-9.15): revision/systemdokumentation.json gains a verifikationsserier_regler block with the resolution order and the two exceptions (invoice settlements, samlingsverifikat); the per-account mapping itself is in data/cash_accounts.json. - Settings picker uses the same closed list as the manual verifikat form (presets plus letters already in use) instead of all 26 letters; strings moved to messages/sv.json and messages/en.json. - /voucher-sequences/next applies the account override only for source_type=bank_transaction (CodeRabbit), so a manual-entry preview cannot show a series the entry will not get. - Book route resolves the series from the account the row ends up on after a stranded-row repoint, not the stale one. - PATCH /api/cash-accounts/[id] answers 404 for a non-UUID id instead of a Postgres cast 500; the series lookup logs a warning when it fails open. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JWSLbQc3jgpfqnxWe6nteh --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
75 lines
2.8 KiB
TypeScript
75 lines
2.8 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { resolveAccount } from '@/lib/cash-accounts/resolve-account'
|
|
import type { CashAccount } from '@/types'
|
|
|
|
function makeCashAccount(overrides: Partial<CashAccount> = {}): CashAccount {
|
|
return {
|
|
id: 'ca-1',
|
|
company_id: 'company-1',
|
|
bank_connection_id: null,
|
|
external_uid: null,
|
|
iban: null,
|
|
bg_pg: null,
|
|
name: null,
|
|
currency: 'SEK',
|
|
ledger_account: '1930',
|
|
balance: null,
|
|
available_balance: null,
|
|
balance_updated_at: null,
|
|
enabled: true,
|
|
is_primary: true,
|
|
source: 'manual',
|
|
voucher_series: null,
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('resolveAccount', () => {
|
|
it('returns the ledger_account for the bound cash account when cash_account_id matches', () => {
|
|
const accounts = [
|
|
makeCashAccount({ id: 'ca-1', ledger_account: '1930' }),
|
|
makeCashAccount({ id: 'ca-2', ledger_account: '1940', is_primary: false }),
|
|
]
|
|
const result = resolveAccount(accounts, 'ca-2', 'SEK')
|
|
expect(result).toEqual({ account: '1940', fallback: false })
|
|
})
|
|
|
|
it('falls back to the sole enabled same-currency account when cash_account_id is null', () => {
|
|
const accounts = [makeCashAccount({ id: 'ca-1', ledger_account: '1920', currency: 'EUR' })]
|
|
const result = resolveAccount(accounts, null, 'EUR')
|
|
expect(result).toEqual({ account: '1920', fallback: false })
|
|
})
|
|
|
|
it('falls back to 1930 when cash_account_id is null and multiple same-currency accounts exist', () => {
|
|
const accounts = [
|
|
makeCashAccount({ id: 'ca-1', ledger_account: '1930', currency: 'SEK' }),
|
|
makeCashAccount({ id: 'ca-2', ledger_account: '1940', currency: 'SEK', is_primary: false }),
|
|
]
|
|
const result = resolveAccount(accounts, null, 'SEK')
|
|
expect(result).toEqual({ account: '1930', fallback: true })
|
|
})
|
|
|
|
it('falls back to 1930 when cash_account_id does not match any account', () => {
|
|
const accounts = [makeCashAccount({ id: 'ca-1', ledger_account: '1930' })]
|
|
const result = resolveAccount(accounts, 'ca-unknown', 'SEK')
|
|
expect(result).toEqual({ account: '1930', fallback: true })
|
|
})
|
|
|
|
it('falls back to 1930 when accounts list is empty', () => {
|
|
const result = resolveAccount([], null, 'SEK')
|
|
expect(result).toEqual({ account: '1930', fallback: true })
|
|
})
|
|
|
|
it('ignores disabled accounts in the currency fallback path', () => {
|
|
const accounts = [
|
|
makeCashAccount({ id: 'ca-1', ledger_account: '1930', enabled: true }),
|
|
makeCashAccount({ id: 'ca-2', ledger_account: '1940', enabled: false, is_primary: false }),
|
|
]
|
|
// Only one enabled SEK account → resolves without fallback
|
|
const result = resolveAccount(accounts, null, 'SEK')
|
|
expect(result).toEqual({ account: '1930', fallback: false })
|
|
})
|
|
})
|