diff --git a/lib/bookkeeping/__tests__/booking-templates.test.ts b/lib/bookkeeping/__tests__/booking-templates.test.ts index 4f52cfe6..dbed5d5c 100644 --- a/lib/bookkeeping/__tests__/booking-templates.test.ts +++ b/lib/bookkeeping/__tests__/booking-templates.test.ts @@ -426,7 +426,10 @@ describe('buildMappingResultFromTemplate', () => { const tx = makeTransaction({ amount: -1120 }) const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma') - expect(result.debit_account).toBe('5820') + // 5830 Kost och logi, not 5820 Hyrbilskostnader. This assertion used to + // read 5820, which pinned a real bug: the Hotell template posted hotel + // nights into car hire. It balanced, so nothing ever complained. + expect(result.debit_account).toBe('5830') expect(result.vat_lines).toHaveLength(1) expect(result.vat_lines[0].account_number).toBe('2641') expect(result.vat_lines[0].debit_amount).toBe(120) // 1120 * 0.12 / 1.12 = 120 diff --git a/lib/bookkeeping/__tests__/template-accounts-exist.test.ts b/lib/bookkeeping/__tests__/template-accounts-exist.test.ts new file mode 100644 index 00000000..3cd6f06b --- /dev/null +++ b/lib/bookkeeping/__tests__/template-accounts-exist.test.ts @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest' +import { BOOKING_TEMPLATES } from '@/lib/bookkeeping/booking-templates' +import { getBASReference } from '@/lib/bookkeeping/bas-reference' + +/** + * Every account a booking template names must exist in BAS 2026. + * + * This is not pedantry: `lib/bookkeeping/account-backfill.ts` seeds a missing + * account into a company's chart on demand ONLY if it appears in + * BAS_REFERENCE. An account that does not is unreachable, so the template + * fails with AccountsNotInChartError every single time it is used, for every + * company. It is a template that can never work. + * + * Three shipped templates were in exactly that state until this test existed: + * vehicle_parking 5614 (the BAS 561x run skips 5614) + * it_cloud_hosting 5421 (BAS has 5420 Programvaror, no 5421) + * preliminar-f-skatt-ef 2012 (found separately by the pack validator) + * + * The pack catalogue gets this check from `scripts/validate-packs.ts`. The + * static registry had nothing, which is why it drifted. + */ +describe('BOOKING_TEMPLATES reference only real BAS accounts', () => { + const accountFields = ['debit_account', 'credit_account', 'debit_account_ab', 'credit_account_ab'] as const + + it('every referenced account resolves in BAS 2026', () => { + const missing: string[] = [] + + for (const t of BOOKING_TEMPLATES) { + for (const field of accountFields) { + const account = (t as unknown as Record)[field] + if (!account) continue + if (!getBASReference(account)) { + missing.push(`${t.id}.${field} = ${account}`) + } + } + } + + expect( + missing, + `These templates name accounts that do not exist in BAS 2026, so account-backfill ` + + `cannot seed them and every booking through them fails:\n ${missing.join('\n ')}`, + ).toEqual([]) + }) + + it('account numbers are strings, never numbers', () => { + for (const t of BOOKING_TEMPLATES) { + for (const field of accountFields) { + const account = (t as unknown as Record)[field] + if (account === undefined) continue + expect(typeof account, `${t.id}.${field}`).toBe('string') + } + } + }) + + it('a VAT-bearing purchase never debits equity or revenue', () => { + // Catches a transposed account number that happens to exist. Deliberately + // narrow: class 1 is legitimate here (equipment_capital debits 1250 + // Inventarier and reclaims VAT, which is how capex is booked), and classes + // 4-7 are ordinary costs. What a purchase can never debit is class 2 + // (equity and liabilities) or class 3 (revenue): those would be a sign + // error, not a categorisation choice. + const FORBIDDEN = new Set([2, 3]) + + for (const t of BOOKING_TEMPLATES) { + if (!t.vat_rate || t.direction !== 'expense') continue + const cls = Number(t.debit_account.charAt(0)) + expect( + FORBIDDEN.has(cls), + `${t.id} charges VAT ${t.vat_rate} but debits ${t.debit_account} (class ${cls}): ` + + `a purchase cannot debit equity/liabilities or revenue`, + ).toBe(false) + } + }) +}) diff --git a/lib/bookkeeping/booking-templates.ts b/lib/bookkeeping/booking-templates.ts index e6b3ae75..21473e28 100644 --- a/lib/bookkeeping/booking-templates.ts +++ b/lib/bookkeeping/booking-templates.ts @@ -268,7 +268,9 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [ group: 'vehicle', direction: 'expense', entity_applicability: 'all', - debit_account: '5614', + debit_account: '5619', // 5614 does not exist in BAS 2026 (the 561x run skips it), so every + // booking through this template failed with AccountsNotInChartError. + // 5619 is the sibling 'Övriga kostnader för personbilar och mc'. credit_account: '1930', vat_treatment: 'standard_25', vat_rate: 0.25, @@ -341,7 +343,8 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [ group: 'it_software', direction: 'expense', entity_applicability: 'all', - debit_account: '5421', + debit_account: '5420', // 5421 does not exist in BAS 2026. 5420 Programvaror is what the two + // sibling SaaS templates already use. credit_account: '1930', vat_treatment: 'reverse_charge', vat_rate: 0, @@ -538,7 +541,9 @@ export const BOOKING_TEMPLATES: readonly BookingTemplate[] = [ group: 'travel', direction: 'expense', entity_applicability: 'all', - debit_account: '5820', + debit_account: '5830', // 5820 is Hyrbilskostnader (rental car). This template is Hotell, so it + // posted hotel nights into car hire: it balanced and was silently wrong. + // 5830 is 'Kost och logi'. credit_account: '1930', vat_treatment: 'reduced_12', vat_rate: 0.12,