Files
accounted/lib/bookkeeping/__tests__/booking-templates.test.ts
T
Jakob WennbergandClaude Opus 4.7 cad83d180d fix(currency): transaction-date FX rate + mall bookings in SEK (#442) (#483)
* fix(currency): use transaction-date FX rate and post mall bookings in SEK (#442)

Three related defects in non-SEK transaction handling:

1. Ingest priced every transaction in a batch at today's rate because
   fetchMultipleRates() was called without a date. Switch to a per-
   (currency, date) fetch via fetchExchangeRate() + Promise.allSettled,
   so each row stores Riksbanken's rate as of its own transaction date.

2. buildMappingResultFromTemplate emitted VAT/reverse-charge/output-VAT
   lines from Math.abs(transaction.amount) — the source-currency amount.
   Mall bookings on USD/EUR transactions therefore posted the journal
   entry in the wrong currency. Resolve to SEK once via resolveSekAmount()
   and reuse for every line, matching what createTransactionJournalEntry
   already does for the settlement line.

3. The Granska bokföring modal and the journal-entry detail page lacked
   a clear audit trail. The verifikation preview now renders in SEK with
   a separate original-currency subtitle in the header; the detail page
   gains a dedicated "Valutaomräkning" card above Kontorader showing the
   rate and original total.

Also: replace truncate with break-all on the modal description so long
UUID-style strings (e.g. BALANCE_CASHBACK-…) wrap instead of forcing
horizontal overscroll.

Tests: 4 new booking-templates cases (USD 25%, EUR fallback via
exchange_rate, EU reverse-charge on USD, output VAT on USD income) and
2 new ingest cases (per-pair fetch dedup; no fetch for SEK rows).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(currency): wire SEK conversion into SwipeCategorizationView (#442 follow-up)

Address Greptile review:

- SwipeCategorizationView rendered JournalEntryPreview without amountSek,
  so the swipe flow had the same defect QuickReviewDialog had — a USD
  transaction showed the verifikation in source currency labeled SEK.
  Now resolves SEK once via resolveSekAmount, passes it to the preview,
  and shows SEK as the headline with the original-currency subtitle
  (parallel to QuickReviewDialog).
- Drop the now-dead `currency` prop from JournalEntryPreviewProps and
  its remaining call site in QuickReviewDialog — the component
  hardcodes 'SEK' on display, so the prop was misleading.
- Correct the misleading inline comment in ingest.ts: network failures
  resolve inside fetchExchangeRate to getFallbackRate() (non-null),
  so the key only stays unset on empty-observation responses or
  outright rejections.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 15:00:12 +02:00

679 lines
24 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { makeTransaction } from '@/tests/helpers'
import {
BOOKING_TEMPLATES,
getTemplateById,
getTemplatesByGroup,
getTemplatesByMcc,
getTemplateGroups,
searchTemplates,
findMatchingTemplates,
buildMappingResultFromTemplate,
getCommonTemplates,
getAdvancedTemplates,
validateTemplateForEntity,
type BookingTemplate,
} from '../booking-templates'
// ============================================================
// Template Data Integrity
// ============================================================
describe('BOOKING_TEMPLATES data integrity', () => {
it('has exactly 51 templates', () => {
expect(BOOKING_TEMPLATES).toHaveLength(51)
})
it('all template IDs are unique', () => {
const ids = BOOKING_TEMPLATES.map((t) => t.id)
expect(new Set(ids).size).toBe(ids.length)
})
it('all templates have valid required fields', () => {
for (const t of BOOKING_TEMPLATES) {
expect(t.id).toBeTruthy()
expect(t.name_sv).toBeTruthy()
expect(t.name_en).toBeTruthy()
expect(t.group).toBeTruthy()
expect(['expense', 'income', 'transfer']).toContain(t.direction)
expect(['all', 'enskild_firma', 'aktiebolag']).toContain(t.entity_applicability)
expect(t.debit_account).toMatch(/^\d{4}$/)
expect(t.credit_account).toMatch(/^\d{4}$/)
expect(['full', 'non_deductible', 'conditional']).toContain(t.deductibility)
expect(['NONE', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']).toContain(t.risk_level)
expect(typeof t.requires_review).toBe('boolean')
expect(t.impact_score).toBeGreaterThanOrEqual(1)
expect(t.impact_score).toBeLessThanOrEqual(10)
expect(t.auto_match_confidence).toBeGreaterThanOrEqual(0.5)
expect(t.auto_match_confidence).toBeLessThanOrEqual(1.0)
expect(typeof t.default_private).toBe('boolean')
expect(t.fallback_category).toBeTruthy()
expect(t.description_sv).toBeTruthy()
expect(typeof t.common).toBe('boolean')
expect(Array.isArray(t.mcc_codes)).toBe(true)
expect(Array.isArray(t.keywords)).toBe(true)
expect(t.keywords.length).toBeGreaterThan(0)
}
})
it('all AB-specific override accounts are valid 4-digit strings', () => {
for (const t of BOOKING_TEMPLATES) {
if (t.debit_account_ab) {
expect(t.debit_account_ab).toMatch(/^\d{4}$/)
}
if (t.credit_account_ab) {
expect(t.credit_account_ab).toMatch(/^\d{4}$/)
}
}
})
it('vat_rate is consistent with vat_treatment', () => {
for (const t of BOOKING_TEMPLATES) {
if (t.vat_treatment === 'standard_25') {
expect(t.vat_rate).toBe(0.25)
} else if (t.vat_treatment === 'reduced_12') {
expect(t.vat_rate).toBe(0.12)
} else if (t.vat_treatment === 'reduced_6') {
expect(t.vat_rate).toBe(0.06)
} else if (t.vat_treatment === 'reverse_charge' || t.vat_treatment === 'export' || t.vat_treatment === 'exempt' || t.vat_treatment === null) {
expect(t.vat_rate).toBe(0)
}
}
})
})
// ============================================================
// Lookup Functions
// ============================================================
describe('getTemplateById', () => {
it('returns correct template for known ID', () => {
const t = getTemplateById('it_saas_subscription')
expect(t).toBeDefined()
expect(t!.name_sv).toBe('Programvara / SaaS')
expect(t!.debit_account).toBe('5420')
})
it('returns undefined for unknown ID', () => {
expect(getTemplateById('nonexistent')).toBeUndefined()
})
})
describe('getTemplatesByGroup', () => {
it('returns templates for the premises group', () => {
const templates = getTemplatesByGroup('premises')
expect(templates.length).toBeGreaterThan(0)
for (const t of templates) {
expect(t.group).toBe('premises')
}
})
it('returns empty array for non-existent group', () => {
expect(getTemplatesByGroup('nonexistent' as never)).toEqual([])
})
})
describe('getTemplatesByMcc', () => {
it('returns templates for MCC 5541 (fuel)', () => {
const templates = getTemplatesByMcc(5541)
expect(templates.length).toBeGreaterThan(0)
expect(templates.some((t) => t.id === 'vehicle_fuel')).toBe(true)
})
it('returns empty array for unknown MCC', () => {
expect(getTemplatesByMcc(9999)).toEqual([])
})
})
describe('getTemplateGroups', () => {
it('returns all 17 groups', () => {
const groups = getTemplateGroups()
expect(groups).toHaveLength(17)
for (const g of groups) {
expect(g.group).toBeTruthy()
expect(g.label_sv).toBeTruthy()
expect(g.label_en).toBeTruthy()
expect(Array.isArray(g.templates)).toBe(true)
}
})
it('every template is in exactly one group', () => {
const groups = getTemplateGroups()
const allTemplates = groups.flatMap((g) => g.templates)
expect(allTemplates).toHaveLength(51)
})
})
// ============================================================
// Search
// ============================================================
describe('searchTemplates', () => {
it('finds templates by Swedish name', () => {
const results = searchTemplates('lokalhyra')
expect(results.length).toBeGreaterThan(0)
expect(results.some((t) => t.id === 'premises_rent')).toBe(true)
})
it('finds templates by English name', () => {
const results = searchTemplates('software')
expect(results.length).toBeGreaterThan(0)
expect(results.some((t) => t.id === 'it_saas_subscription')).toBe(true)
})
it('finds templates by keywords', () => {
const results = searchTemplates('spotify')
expect(results.length).toBeGreaterThan(0)
})
it('returns empty for empty query', () => {
expect(searchTemplates('')).toEqual([])
})
it('filters by entity type', () => {
const results = searchTemplates('pension', 'enskild_firma')
// Should include EF-specific and 'all', but not AB-only
for (const t of results) {
expect(t.entity_applicability).not.toBe('aktiebolag')
}
})
it('supports multi-token search', () => {
const results = searchTemplates('annonsering EU')
expect(results.some((t) => t.id === 'marketing_online_ads_eu')).toBe(true)
})
})
// ============================================================
// findMatchingTemplates
// ============================================================
describe('findMatchingTemplates', () => {
it('matches by MCC code with high confidence', () => {
const tx = makeTransaction({
amount: -500,
mcc_code: 5541,
description: 'Gas station',
merchant_name: 'OKQ8',
})
const matches = findMatchingTemplates(tx)
expect(matches.length).toBeGreaterThan(0)
expect(matches[0].template.id).toBe('vehicle_fuel')
expect(matches[0].confidence).toBeGreaterThan(0.3)
})
it('matches by keywords in description', () => {
const tx = makeTransaction({
amount: -299,
description: 'Google Ads campaign',
merchant_name: 'Google',
})
const matches = findMatchingTemplates(tx)
expect(matches.some((m) => m.template.id === 'marketing_online_ads_eu')).toBe(true)
})
it('returns empty for a transaction with no signals', () => {
const tx = makeTransaction({
amount: -100,
description: 'XYZ123ABC',
mcc_code: null,
merchant_name: null,
})
const matches = findMatchingTemplates(tx)
expect(matches).toEqual([])
})
it('filters by entity type', () => {
const tx = makeTransaction({
amount: -5000,
description: 'Löneutbetalning',
})
const matches = findMatchingTemplates(tx, 'enskild_firma')
// Personnel salary is AB-only, should not appear
for (const m of matches) {
expect(m.template.entity_applicability).not.toBe('aktiebolag')
}
})
it('does not match expense templates for positive amounts', () => {
const tx = makeTransaction({
amount: 1000,
description: 'Bensin okq8',
mcc_code: 5541,
})
const matches = findMatchingTemplates(tx)
// vehicle_fuel is an expense template, should not match positive amount
expect(matches.every((m) => m.template.direction !== 'expense')).toBe(true)
})
it('returns max 10 results', () => {
const tx = makeTransaction({
amount: -100,
description: 'software subscription cloud hosting domain',
mcc_code: 5817,
})
const matches = findMatchingTemplates(tx)
expect(matches.length).toBeLessThanOrEqual(10)
})
it('results are sorted by confidence descending', () => {
const tx = makeTransaction({
amount: -999,
description: 'Google cloud hosting',
mcc_code: 4816,
})
const matches = findMatchingTemplates(tx)
for (let i = 1; i < matches.length; i++) {
expect(matches[i - 1].confidence).toBeGreaterThanOrEqual(matches[i].confidence)
}
})
})
// ============================================================
// buildMappingResultFromTemplate
// ============================================================
describe('buildMappingResultFromTemplate', () => {
const getTemplate = (id: string): BookingTemplate => {
const t = getTemplateById(id)
if (!t) throw new Error(`Template not found: ${id}`)
return t
}
it('produces valid MappingResult for expense with 25% VAT', () => {
const template = getTemplate('it_saas_subscription')
const tx = makeTransaction({ amount: -1250 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.debit_account).toBe('5420')
expect(result.credit_account).toBe('1930')
expect(result.template_id).toBe('it_saas_subscription')
expect(result.rule).toBeNull()
expect(result.confidence).toBe(1.0)
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2641')
expect(result.vat_lines[0].debit_amount).toBe(250) // 1250 * 0.25 / 1.25 = 250
})
it('produces valid MappingResult for expense with 12% VAT', () => {
const template = getTemplate('travel_hotel')
const tx = makeTransaction({ amount: -1120 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.debit_account).toBe('5820')
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
})
it('produces valid MappingResult for expense with 6% VAT', () => {
const template = getTemplate('travel_transport')
const tx = makeTransaction({ amount: -530 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2641')
expect(result.vat_lines[0].debit_amount).toBe(30) // 530 * 0.06 / 1.06 = 30
})
it('produces reverse charge lines for EU purchases (fiktiv moms + basbelopp)', () => {
const template = getTemplate('it_saas_eu')
const tx = makeTransaction({ amount: -1000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// Four lines: fiktiv-moms pair + basbelopp pair. Without the basbelopp
// pair the deklaration is rejected with FK004 (ruta 30-32 without 20-24).
expect(result.vat_lines).toHaveLength(4)
// Fiktiv ingående moms (EU: 2645)
expect(result.vat_lines[0].account_number).toBe('2645')
expect(result.vat_lines[0].debit_amount).toBe(250)
// Fiktiv utgående moms (25%: 2614)
expect(result.vat_lines[1].account_number).toBe('2614')
expect(result.vat_lines[1].credit_amount).toBe(250)
// Basbelopp EU services 25% → ruta 21
expect(result.vat_lines[2].account_number).toBe('4535')
expect(result.vat_lines[2].debit_amount).toBe(1000)
// Motkonto basbelopp
expect(result.vat_lines[3].account_number).toBe('4598')
expect(result.vat_lines[3].credit_amount).toBe(1000)
})
it('defaults to eu_business supplier type when not set on template', () => {
// it_cloud_hosting has no explicit reverse_charge_supplier_type
const template = getTemplate('it_cloud_hosting')
const tx = makeTransaction({ amount: -800 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(4)
// Defaults to EU services → 4535
expect(result.vat_lines[2].account_number).toBe('4535')
expect(result.vat_lines[2].debit_amount).toBe(800)
})
it('uses 4531 basbelopp for non-EU supplier type', () => {
const template: BookingTemplate = {
...getTemplate('it_cloud_hosting'),
reverse_charge_supplier_type: 'non_eu_business',
}
const tx = makeTransaction({ amount: -1000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(4)
expect(result.vat_lines[0].account_number).toBe('2645') // non-EU still uses 2645
expect(result.vat_lines[2].account_number).toBe('4531') // non-EU services → ruta 22
})
it('uses 4425 basbelopp and 2647 for domestic (swedish) reverse charge', () => {
const template: BookingTemplate = {
...getTemplate('it_cloud_hosting'),
reverse_charge_supplier_type: 'swedish_business',
}
const tx = makeTransaction({ amount: -1000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(4)
// Domestic RC uses 2647 (ML 16 kap) for the input pair
expect(result.vat_lines[0].account_number).toBe('2647')
// Domestic services (byggtjänster) → 4425, ruta 24
expect(result.vat_lines[2].account_number).toBe('4425')
})
it('skips basbelopp emission when template already debits a basis account', () => {
const template: BookingTemplate = {
...getTemplate('it_cloud_hosting'),
debit_account: '4535', // user-customized template that books directly to basis
}
const tx = makeTransaction({ amount: -1000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// Only the fiktiv-moms pair — basbelopp would double-count.
expect(result.vat_lines).toHaveLength(2)
expect(result.vat_lines[0].account_number).toBe('2645')
expect(result.vat_lines[1].account_number).toBe('2614')
})
it('produces no VAT lines for exempt expenses', () => {
const template = getTemplate('premises_rent')
const tx = makeTransaction({ amount: -10000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(0)
})
it('produces no VAT lines for non-deductible templates', () => {
const template = getTemplate('private_withdrawal_ef')
const tx = makeTransaction({ amount: -5000 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(0)
expect(result.default_private).toBe(true)
})
it('produces output VAT lines for income with 25% VAT', () => {
const template = getTemplate('revenue_standard_25')
const tx = makeTransaction({ amount: 12500 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.debit_account).toBe('1930')
expect(result.credit_account).toBe('3001')
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2611')
expect(result.vat_lines[0].credit_amount).toBe(2500) // 12500 * 0.25 / 1.25 = 2500
})
it('produces output VAT lines for income with 12% VAT', () => {
const template = getTemplate('revenue_reduced_12')
const tx = makeTransaction({ amount: 1120 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2621')
expect(result.vat_lines[0].credit_amount).toBe(120)
})
it('resolves AB-specific accounts for aktiebolag', () => {
const template = getTemplate('education_course')
const tx = makeTransaction({ amount: -5000 })
const efResult = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(efResult.debit_account).toBe('6991')
const abResult = buildMappingResultFromTemplate(template, tx, 'aktiebolag')
expect(abResult.debit_account).toBe('7610')
})
it('resolves AB-specific private account', () => {
const template = getTemplate('private_expense')
const tx = makeTransaction({ amount: -300 })
const efResult = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(efResult.debit_account).toBe('2013')
const abResult = buildMappingResultFromTemplate(template, tx, 'aktiebolag')
expect(abResult.debit_account).toBe('2893')
})
it('includes template_id in the MappingResult', () => {
const template = getTemplate('bank_fees')
const tx = makeTransaction({ amount: -49 })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.template_id).toBe('bank_fees')
expect(result.rule).toBeNull()
})
it('sets description with template name and transaction description', () => {
const template = getTemplate('vehicle_fuel')
const tx = makeTransaction({ amount: -800, description: 'OKQ8 tankstation' })
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
expect(result.description).toBe('Drivmedel & Laddning: OKQ8 tankstation')
})
// Foreign-currency transactions: the mall must always emit SEK amounts
// (issue #442). Previously buildMappingResultFromTemplate used
// Math.abs(transaction.amount) — which is in the source currency — to
// compute VAT lines, producing a verifikation in mixed currencies.
it('emits SEK amounts when transaction currency is USD (issue #442)', () => {
const template = getTemplate('it_saas_subscription') // 25% input VAT
const tx = makeTransaction({
amount: -125, // -125 USD
currency: 'USD',
amount_sek: -1250, // pre-converted to SEK
exchange_rate: 10,
})
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// VAT line debit must be 250 SEK (1250 * 0.25 / 1.25), not 25 USD
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2641')
expect(result.vat_lines[0].debit_amount).toBe(250)
})
it('falls back to amount * exchange_rate when amount_sek is missing', () => {
const template = getTemplate('it_saas_subscription')
const tx = makeTransaction({
amount: -100,
currency: 'EUR',
amount_sek: null,
exchange_rate: 11.5,
})
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// 100 * 11.5 = 1150 SEK; 1150 * 0.25 / 1.25 = 230 SEK
expect(result.vat_lines[0].debit_amount).toBe(230)
})
it('emits SEK amounts for EU reverse-charge on a non-SEK transaction', () => {
const template = getTemplate('it_saas_eu')
const tx = makeTransaction({
amount: -100,
currency: 'USD',
amount_sek: -1000,
exchange_rate: 10,
})
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// Fiktiv-moms pair sized at 25% of 1000 SEK = 250, not 25 USD
expect(result.vat_lines).toHaveLength(4)
expect(result.vat_lines[0].debit_amount).toBe(250) // 2645
expect(result.vat_lines[1].credit_amount).toBe(250) // 2614
expect(result.vat_lines[2].debit_amount).toBe(1000) // 4535 basbelopp
expect(result.vat_lines[3].credit_amount).toBe(1000) // 4598
})
it('emits SEK amounts for output VAT on non-SEK income', () => {
const template = getTemplate('revenue_standard_25')
const tx = makeTransaction({
amount: 100,
currency: 'USD',
amount_sek: 1000,
exchange_rate: 10,
})
const result = buildMappingResultFromTemplate(template, tx, 'enskild_firma')
// Output VAT at 25% on 1000 SEK = 200, not 20 USD
expect(result.vat_lines).toHaveLength(1)
expect(result.vat_lines[0].account_number).toBe('2611')
expect(result.vat_lines[0].credit_amount).toBe(200)
})
})
// ============================================================
// Template Curation Helpers
// ============================================================
describe('getCommonTemplates', () => {
it('returns only templates with common: true', () => {
const common = getCommonTemplates()
expect(common.length).toBeGreaterThan(0)
for (const t of common) {
expect(t.common).toBe(true)
}
})
it('filters by entity type', () => {
const efCommon = getCommonTemplates('enskild_firma')
for (const t of efCommon) {
expect(t.entity_applicability).not.toBe('aktiebolag')
}
})
it('filters by direction', () => {
const expenses = getCommonTemplates(undefined, 'expense')
for (const t of expenses) {
expect(t.direction).toBe('expense')
}
})
})
describe('getAdvancedTemplates', () => {
it('returns only templates with common: false', () => {
const advanced = getAdvancedTemplates()
expect(advanced.length).toBeGreaterThan(0)
for (const t of advanced) {
expect(t.common).toBe(false)
}
})
it('common + advanced = all templates (for a given entity/direction)', () => {
const common = getCommonTemplates()
const advanced = getAdvancedTemplates()
expect(common.length + advanced.length).toBe(BOOKING_TEMPLATES.length)
})
})
describe('validateTemplateForEntity', () => {
it('accepts template with entity_applicability "all"', () => {
const template = getTemplateById('premises_rent')!
const result = validateTemplateForEntity(template, 'aktiebolag')
expect(result.valid).toBe(true)
})
it('accepts EF template for EF entity', () => {
const template = getTemplateById('private_withdrawal_ef')!
const result = validateTemplateForEntity(template, 'enskild_firma')
expect(result.valid).toBe(true)
})
it('rejects EF template for AB entity', () => {
const template = getTemplateById('private_withdrawal_ef')!
const result = validateTemplateForEntity(template, 'aktiebolag')
expect(result.valid).toBe(false)
expect(result.error).toContain('enskild_firma')
})
it('rejects AB template for EF entity', () => {
const template = getTemplateById('personnel_salary')!
const result = validateTemplateForEntity(template, 'enskild_firma')
expect(result.valid).toBe(false)
expect(result.error).toContain('aktiebolag')
})
})
// ============================================================
// New/Split Templates
// ============================================================
describe('new and split templates', () => {
it('has marketing_online_ads_eu with reverse_charge', () => {
const t = getTemplateById('marketing_online_ads_eu')
expect(t).toBeDefined()
expect(t!.vat_treatment).toBe('reverse_charge')
expect(t!.common).toBe(true)
expect(t!.requires_vat_registration_data).toBe(true)
})
it('has marketing_online_ads_domestic with standard_25', () => {
const t = getTemplateById('marketing_online_ads_domestic')
expect(t).toBeDefined()
expect(t!.vat_treatment).toBe('standard_25')
expect(t!.common).toBe(false)
})
it('has representation_internal with account 7622', () => {
const t = getTemplateById('representation_internal')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('7622')
expect(t!.vat_treatment).toBeNull()
expect(t!.common).toBe(true)
})
it('has shareholder_loan_received (AB, D:1930 K:2393)', () => {
const t = getTemplateById('shareholder_loan_received')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('1930')
expect(t!.credit_account).toBe('2393')
expect(t!.entity_applicability).toBe('aktiebolag')
expect(t!.common).toBe(true)
})
it('has shareholder_loan_disbursed (AB, D:1680 K:1930)', () => {
const t = getTemplateById('shareholder_loan_disbursed')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('1680')
expect(t!.credit_account).toBe('1930')
expect(t!.entity_applicability).toBe('aktiebolag')
expect(t!.common).toBe(false)
})
it('personnel_employer_tax uses debit account 2731 (liability clearing)', () => {
const t = getTemplateById('personnel_employer_tax')
expect(t).toBeDefined()
expect(t!.debit_account).toBe('2731')
})
it('personnel_salary has special_rules_sv warning', () => {
const t = getTemplateById('personnel_salary')
expect(t).toBeDefined()
expect(t!.special_rules_sv).toContain('nettolön')
expect(t!.requires_review).toBe(true)
})
it('representation_external has updated deductibility note with VAT cap', () => {
const t = getTemplateById('representation_external')
expect(t).toBeDefined()
expect(t!.deductibility_note_sv).toContain('46 kr/person')
})
})