* 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>
72 lines
2.7 KiB
TypeScript
72 lines
2.7 KiB
TypeScript
/**
|
|
* Verifikationsserie per bankkonto.
|
|
*
|
|
* Resolution order for an entry booked from a bank transaction:
|
|
* 1. explicit voucher_series in the request (the booking dialog's picker)
|
|
* 2. cash_accounts.voucher_series of the transaction's account (this module)
|
|
* 3. company_settings.default_voucher_series_per_source_type (engine)
|
|
* 4. 'A' (engine)
|
|
*
|
|
* This helper covers step 2 only. It returns undefined whenever there is no
|
|
* override so callers can pass the result straight into CreateJournalEntryInput
|
|
* and let the engine handle steps 3 and 4. Lookup failures also resolve to
|
|
* undefined: a broken override must never block a booking, the entry then
|
|
* lands in the per-type default exactly as before this feature.
|
|
*
|
|
* Scope: bank-transaction bookings (book route, categorize, agent, pending
|
|
* operations). Invoice settlements matched from the bank keep the invoice
|
|
* payment series (invoice_paid, supplier_invoice_paid): those series describe
|
|
* the payment kind, not the account the money moved through.
|
|
*/
|
|
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import { createLogger } from '@/lib/logger'
|
|
|
|
const log = createLogger('cash-account-voucher-series')
|
|
const SERIES_LETTER_RE = /^[A-Z]$/
|
|
|
|
/** Pure: the override letter of a cash account row, or undefined. */
|
|
export function cashAccountSeriesOverride(
|
|
account: { voucher_series?: string | null } | null | undefined,
|
|
): string | undefined {
|
|
const value = account?.voucher_series
|
|
return typeof value === 'string' && SERIES_LETTER_RE.test(value) ? value : undefined
|
|
}
|
|
|
|
/**
|
|
* Look up the series override of one of the company's cash accounts.
|
|
* undefined when the account is unknown, has no override, or the query fails.
|
|
*/
|
|
export async function resolveCashAccountVoucherSeries(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
cashAccountId: string | null | undefined,
|
|
): Promise<string | undefined> {
|
|
if (!cashAccountId) return undefined
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('cash_accounts')
|
|
.select('voucher_series')
|
|
.eq('company_id', companyId)
|
|
.eq('id', cashAccountId)
|
|
.maybeSingle()
|
|
if (error) {
|
|
// Fail open, but never silently: the entry lands in the per-type
|
|
// default and the log says why.
|
|
log.warn('cash_accounts voucher_series lookup failed; using per-type default', {
|
|
companyId,
|
|
cashAccountId,
|
|
error: error.message,
|
|
})
|
|
return undefined
|
|
}
|
|
return cashAccountSeriesOverride(data as { voucher_series?: string | null } | null)
|
|
} catch (err) {
|
|
log.warn('cash_accounts voucher_series lookup threw; using per-type default', {
|
|
companyId,
|
|
cashAccountId,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
})
|
|
return undefined
|
|
}
|
|
}
|