Files
accounted/lib/transactions/__tests__/quick-review-defaults.test.ts
T
858ad49852 fix(booking): explicit Ingen moms in quick review books exempt, not the derived default (#1450)
QuickReviewDialog collapsed 'none' to undefined on the wire, so the server
re-derived the category default and booked 25% moms against an explicit
no-VAT choice while the preview showed no VAT line. 'none' now maps to
'exempt' only when it deviates from the seeded default: untouched bookings
on exempt-default categories (bank fees) stay byte-identical, an explicit
Ingen moms books no VAT line with the classification the momsdeklaration
should see, and the class-2 account auto-'none' stops booking a stray moms
line. Same gap the swedish-compliance bot flagged on #1443.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:58:46 +02:00

94 lines
4.1 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { resolveQuickReviewDefaults, resolveExplicitVat, type ReviewTemplate } from '../quick-review-defaults'
import { getDefaultAccountForCategory } from '@/lib/bookkeeping/category-mapping'
/**
* Regression cover for the "Tidigare motparter" crash: picking a learned
* counterparty template in the Bokför-transaktion modal replaced the whole
* page with the "Något gick fel" error boundary.
*
* The synthetic counterparty template carried only { id, name_sv }, so
* `template.debit_account` was undefined, the dialog's `defaultAccount` prop
* (declared `string`) received undefined, and the first render threw on
* `accountOverride.startsWith('2')`.
*/
describe('resolveQuickReviewDefaults', () => {
const counterparty: ReviewTemplate = {
id: 'counterparty:11111111-1111-1111-1111-111111111111',
name_sv: 'Fee',
debit_account: '6570',
credit_account: '1930',
vat_treatment: null,
}
it('never returns undefined for the account, whatever the template omits', () => {
const bare: ReviewTemplate = { id: 'counterparty:abc', name_sv: 'Fee' }
const { account, vat } = resolveQuickReviewDefaults(bare, undefined, 'expense_other')
expect(account).toBe(getDefaultAccountForCategory('expense_other'))
expect(typeof account).toBe('string')
expect(vat).toBe('none')
})
it('returns an empty account rather than undefined when there is nothing at all', () => {
expect(resolveQuickReviewDefaults(null, undefined, null)).toEqual({ account: '', vat: 'none' })
expect(resolveQuickReviewDefaults({ id: 'counterparty:abc', name_sv: 'Fee' }, undefined, null))
.toEqual({ account: '', vat: 'none' })
})
it('seeds from the counterparty template accounts, not the category fallback', () => {
const { account, vat } = resolveQuickReviewDefaults(counterparty, undefined, 'expense_other')
expect(account).toBe('6570')
expect(vat).toBe('none')
})
it('carries the learned VAT treatment of a counterparty template', () => {
const { vat } = resolveQuickReviewDefaults(
{ ...counterparty, debit_account: '5420', vat_treatment: 'standard_25' },
undefined,
'expense_other',
)
expect(vat).toBe('standard_25')
})
it('ignores the template and uses the category when a catalog templateId is present', () => {
const catalog: ReviewTemplate = {
id: 'bank_fees',
name_sv: 'Bankavgifter',
debit_account: '6570',
credit_account: '1930',
vat_treatment: null,
}
const { account } = resolveQuickReviewDefaults(catalog, 'bank_fees', 'expense_other')
// Catalog templates are validated server-side by id; the form's account
// field is not the source of truth for them.
expect(account).toBe(getDefaultAccountForCategory('expense_other'))
})
it('falls back to the category defaults when no template is involved', () => {
const { account, vat } = resolveQuickReviewDefaults(null, undefined, 'expense_other')
expect(account).toBe(getDefaultAccountForCategory('expense_other'))
expect(vat === 'none' || typeof vat === 'string').toBe(true)
})
})
describe('resolveExplicitVat', () => {
it('passes explicit rate-bearing treatments through unchanged', () => {
expect(resolveExplicitVat('standard_25', 'standard_25')).toBe('standard_25')
expect(resolveExplicitVat('reduced_12', 'standard_25')).toBe('reduced_12')
expect(resolveExplicitVat('reverse_charge', 'none')).toBe('reverse_charge')
})
it("keeps a seeded 'none' default off the wire (server derives, no VAT line)", () => {
// Bank fees etc.: category default is null -> displayed as 'none'.
// Untouched submit must stay byte-identical to the pre-fix behavior.
expect(resolveExplicitVat('none', 'none')).toBeUndefined()
})
it("sends explicit 'exempt' when the user deviates to Ingen moms", () => {
// The old collapse to undefined made the server re-derive the category
// default and book 25% moms against an explicit no-VAT choice.
expect(resolveExplicitVat('none', 'standard_25')).toBe('exempt')
expect(resolveExplicitVat('none', 'reduced_12')).toBe('exempt')
})
})