* 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>
108 lines
4.0 KiB
TypeScript
108 lines
4.0 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { withRouteContext } from '@/lib/api/with-route-context'
|
|
import { errorResponse } from '@/lib/errors/get-structured-error'
|
|
import { validateQuery } from '@/lib/api/validate'
|
|
import { VoucherSequenceNextQuerySchema } from '@/lib/api/schemas'
|
|
import { resolveDefaultSeriesForSource } from '@/lib/bookkeeping/voucher-series-resolver'
|
|
import { resolveCashAccountVoucherSeries } from '@/lib/bookkeeping/cash-account-voucher-series'
|
|
|
|
export const GET = withRouteContext(
|
|
'voucher_sequence.next',
|
|
async (request, ctx) => {
|
|
const { supabase, companyId, log, requestId } = ctx
|
|
|
|
const query = validateQuery(request, VoucherSequenceNextQuerySchema, {
|
|
log,
|
|
operation: 'voucher_sequence.next',
|
|
})
|
|
if (!query.success) return query.response
|
|
const {
|
|
period_id: overridePeriodId,
|
|
series: overrideSeries,
|
|
source_type: sourceType,
|
|
cash_account_id: cashAccountId,
|
|
} = query.data
|
|
|
|
const today = new Date().toISOString().split('T')[0]
|
|
// Vouchers are numbered per fiscal period, so the preview must reflect the
|
|
// period of the entry's date (e.g. a back-dated payment), not today's.
|
|
const date = query.data.date || today
|
|
|
|
const [{ data: period, error: periodError }, { data: settings, error: settingsError }] =
|
|
await Promise.all([
|
|
overridePeriodId
|
|
? supabase
|
|
.from('fiscal_periods')
|
|
.select('id')
|
|
.eq('company_id', companyId)
|
|
.eq('id', overridePeriodId)
|
|
.maybeSingle()
|
|
: supabase
|
|
.from('fiscal_periods')
|
|
.select('id')
|
|
.eq('company_id', companyId)
|
|
.lte('period_start', date)
|
|
.gte('period_end', date)
|
|
.maybeSingle(),
|
|
overrideSeries
|
|
? Promise.resolve({ data: null, error: null })
|
|
: supabase
|
|
.from('company_settings')
|
|
.select('default_voucher_series, default_voucher_series_per_source_type')
|
|
.eq('company_id', companyId)
|
|
.maybeSingle(),
|
|
])
|
|
|
|
if (periodError) {
|
|
log.error('fiscal_periods lookup failed', periodError)
|
|
return errorResponse(periodError, log, { requestId })
|
|
}
|
|
if (settingsError) {
|
|
log.error('company_settings lookup failed', settingsError)
|
|
return errorResponse(settingsError, log, { requestId })
|
|
}
|
|
|
|
// When a source_type is supplied, resolve the series exactly as the booking
|
|
// engine does (cash account override → per-source-type map → 'A'), so the
|
|
// preview can never disagree with the verifikat that actually gets created.
|
|
// Without a source_type, keep the legacy generic default for callers that
|
|
// just want "the next number".
|
|
// The account override only applies to entries booked from bank
|
|
// transactions; for any other source type it must not colour the preview.
|
|
const cashAccountSeries =
|
|
!overrideSeries && cashAccountId && sourceType === 'bank_transaction'
|
|
? await resolveCashAccountVoucherSeries(supabase, companyId, cashAccountId)
|
|
: undefined
|
|
const series = overrideSeries
|
|
? overrideSeries
|
|
: cashAccountSeries
|
|
? cashAccountSeries
|
|
: sourceType
|
|
? resolveDefaultSeriesForSource(settings, sourceType)
|
|
: settings?.default_voucher_series || 'A'
|
|
|
|
if (!period) {
|
|
return NextResponse.json({ data: { next: null, series, fiscal_period_id: null } })
|
|
}
|
|
|
|
const { data: sequence, error: sequenceError } = await supabase
|
|
.from('voucher_sequences')
|
|
.select('last_number')
|
|
.eq('company_id', companyId)
|
|
.eq('fiscal_period_id', period.id)
|
|
.eq('voucher_series', series)
|
|
.maybeSingle()
|
|
|
|
if (sequenceError) {
|
|
log.error('voucher_sequences lookup failed', sequenceError)
|
|
return errorResponse(sequenceError, log, { requestId })
|
|
}
|
|
|
|
const next = (sequence?.last_number ?? 0) + 1
|
|
|
|
return NextResponse.json({
|
|
data: { next, series, fiscal_period_id: period.id },
|
|
})
|
|
},
|
|
)
|