Files
accounted/lib/errors/__tests__/get-error-message.test.ts
T
Mattsson f9ea9c0082 Add/pdf and templates (#705)
* fix(invoices): apply configured voucher series to payments + preview next voucher

The booking engine resolves the series from
default_voucher_series_per_source_type, but the global "Standardserie"
dropdown wrote a separate field the engine ignored, and cash-method invoice
payments (invoice_cash_payment) weren't exposed in settings — so configured
series were silently dropped to "A".

- Expose cash/private payment source types in the per-source-type form
- Write the global default through to the map on save, keeping overrides
- Resolve voucher-sequences/next by source_type (+date) to match the engine
- Show the upcoming voucher (V2) in the payment dialog title
- Share resolveInvoicePaymentSourceType so preview and booking can't drift

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

* fix(salary): keep AGI panel in sync with Skatteverket signing state

The AGI panel mixed run-scoped generation state (agi_generated_at,
agi_declarations) with period-scoped submission state (extension_data
agi_submission_{period}), so the two could drift and present
contradictory UI. Reconcile them:

- Auto-detect a Mina Sidor BankID signature: while awaiting_signing,
  poll /agi/kvittenser on mount and on tab refocus so the panel flips
  to "signed" (hiding the signing actions) without a manual
  "Hamta kvittens" click.
- Warn instead of offering to sign when the locked granskningsunderlag
  predates the run's latest AGI generation (draftIsStale) — avoids
  filing superseded figures.
- Self-heal a stale "AGI-XML saknas" error once the run's AGI is
  (re)generated out-of-band (MCP/API/other tab).
- Refetch the salary run on tab focus so agi_generated_at reflects
  out-of-band generation without a hard reload.
- /agi/lasUpp now clears the cached agi_submission_{period} record, so
  unlocking drops the panel back to the pre-submission state instead of
  stranding it on a released "redo att signeras" draft.

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

* feat: Implement VAT registration handling and invoice item line types

- Added VAT registration check in commitCreateInvoice to set VAT rate to 0% for non-VAT registered companies.
- Updated invoice creation logic to reflect 'exempt' VAT treatment and adjusted related fields accordingly.
- Introduced support for free-text and blank spacer rows in invoice items by adding a new line_type field.
- Enhanced invoice and credit note handling to accommodate new line types.
- Added new localized messages for text rows in English and Swedish.
- Created tests for salary run approval logic, ensuring bank details are validated correctly.
- Implemented effective net payout calculation for salary runs, considering tax overrides.
- Added SQL migrations to support new invoice item line types and accounting method awareness for linking invoices to vouchers.

* feat(articles): artikelregister with revenue account + VAT rate per article

Article register (non-inventory) with per-article VAT rate and optional
BAS class-3 revenue-account override. Includes API routes, UI pages,
MCP tools, pending-operation staging, and the activate-or-create
account flow (ACCOUNTS_NOT_IN_CHART -> ActivateAccountsDialog,
unknown numbers -> AddAccountDialog) reusing the journal entry UX.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): no-doc-required batch + bulk-missing endpoints

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(payments): supplier payment lines + cash-method invoice matching

Shared payment-line proposal for supplier invoices, improved
match-invoice/match-supplier-invoice flows (kontantmetoden-aware),
and voucher-link support without requiring a 151x clearing entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bookkeeping): new journal entry dialog, SIE import tweaks, misc

New journal entry dialog component, journal list/page updates,
invoice editor updates, SIE import adjustments, transaction ingest
and api-key tweaks, pr-agent workflow update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(invoices): implement tax reduction features and localization updates

* feat(tests): add VAT registration gate to pending operations commit tests

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:52:24 +02:00

181 lines
7.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { getErrorMessage } from '../get-error-message'
describe('getErrorMessage — typed bookkeeping error codes', () => {
it('ACCOUNTS_NOT_IN_CHART → lists accounts to activate', () => {
const msg = getErrorMessage({
error: { code: 'ACCOUNTS_NOT_IN_CHART', message: '...', account_numbers: ['1930', '2641'] },
})
expect(msg).toBe('Följande konton behöver aktiveras: 1930, 2641')
})
it('JOURNAL_ENTRY_NOT_BALANCED with details → rich amount message', () => {
const msg = getErrorMessage({
error: {
code: 'JOURNAL_ENTRY_NOT_BALANCED',
message: 'Journal entry is not balanced: debits (100) != credits (80)',
details: { totalDebit: 100, totalCredit: 80, kind: 'draft' },
},
})
expect(msg).toContain('balanserar inte')
expect(msg).toContain('debet')
expect(msg).toContain('kredit')
expect(msg).toMatch(/100/)
expect(msg).toMatch(/80/)
})
it('JOURNAL_ENTRY_NOT_BALANCED without details → fallback Swedish message', () => {
const msg = getErrorMessage({
error: { code: 'JOURNAL_ENTRY_NOT_BALANCED', message: '...' },
})
expect(msg).toBe('Verifikationen balanserar inte. Kontrollera att debet och kredit är lika stora.')
})
it('FISCAL_PERIOD_NOT_FOUND → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'FISCAL_PERIOD_NOT_FOUND', message: '...' } })
expect(msg).toBe('Räkenskapsperioden kunde inte hittas.')
})
it('ENTRY_DATE_OUTSIDE_FISCAL_PERIOD → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'ENTRY_DATE_OUTSIDE_FISCAL_PERIOD', message: '...' } })
expect(msg).toBe('Datumet ligger utanför det valda räkenskapsåret.')
})
it('JOURNAL_ENTRY_NOT_FOUND → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'JOURNAL_ENTRY_NOT_FOUND', message: '...' } })
expect(msg).toBe('Verifikationen kunde inte hittas.')
})
it('CANNOT_REVERSE_NON_POSTED → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'CANNOT_REVERSE_NON_POSTED', message: '...' } })
expect(msg).toBe('Endast bokförda verifikationer kan stornas.')
})
it('CANNOT_CORRECT_NON_POSTED → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'CANNOT_CORRECT_NON_POSTED', message: '...' } })
expect(msg).toBe('Endast bokförda verifikationer kan rättas.')
})
it('ENTRY_ALREADY_REVERSED → Swedish concurrent-conflict message', () => {
const msg = getErrorMessage({ error: { code: 'ENTRY_ALREADY_REVERSED', message: '...' } })
expect(msg).toContain('redan stornats')
expect(msg).toContain('Ladda om sidan')
})
it('CURRENCY_REVALUATION_ALREADY_EXISTS → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'CURRENCY_REVALUATION_ALREADY_EXISTS', message: '...' } })
expect(msg).toBe('En valutaomvärdering finns redan för denna period.')
})
it('INVALID_MAPPING_RESULT → Swedish message', () => {
const msg = getErrorMessage({ error: { code: 'INVALID_MAPPING_RESULT', message: '...' } })
expect(msg).toBe('Kontering saknas för transaktionen. Kontrollera bokföringsreglerna.')
})
it('BOOKKEEPING_DATABASE_ERROR → generic "kunde inte sparas" when no pattern matches', () => {
const msg = getErrorMessage({
error: {
code: 'BOOKKEEPING_DATABASE_ERROR',
message: 'Database operation "commit_entry" failed: some random constraint',
},
})
expect(msg).toBe('Verifikationen kunde inte sparas. Försök igen.')
})
it('BOOKKEEPING_DATABASE_ERROR falls through to regex pattern for period lock', () => {
// Period-lock trigger errors come through as DB errors — message should still
// match the locked-period pattern and produce the specific Swedish message.
const msg = getErrorMessage({
error: {
code: 'BOOKKEEPING_DATABASE_ERROR',
message: 'Cannot create entry in locked/closed fiscal period',
},
})
expect(msg).toBe('Perioden är låst. Verifikationen kan inte skapas i en stängd eller låst period.')
})
})
describe('getErrorMessage — English locale uses registry English (C9)', () => {
it('returns the registry English message for a known structured code instead of Swedish', () => {
const code = 'FISCAL_PERIOD_NOT_FOUND'
const sv = getErrorMessage({ error: { code, message: '...' } })
const en = getErrorMessage({ error: { code, message: '...' } }, { locale: 'en' })
expect(sv).toMatch(/[åäö]/i) // default (Swedish) path is unchanged
expect(en).not.toBe(sv) // English locale now differs
expect(en).not.toMatch(/[åäö]/i) // …and is no longer Swedish prose
expect(en.toLowerCase()).toContain('fiscal period')
})
it('leaves the Swedish (default-locale) message identical to before', () => {
expect(getErrorMessage({ error: { code: 'CANNOT_REVERSE_NON_POSTED', message: '...' } })).toBe(
'Endast bokförda verifikationer kan stornas.',
)
})
})
describe('getErrorMessage — accumulated validation details', () => {
it('surfaces the specific per-item reasons instead of the generic 400 message', () => {
const msg = getErrorMessage(
{
error: 'Valideringsfel — korrigera innan godkännande',
details: ['Tomas Tysén: Bankuppgifter saknas (clearingnummer och/eller kontonummer)'],
warnings: [],
},
{ context: 'salary', statusCode: 400 },
)
expect(msg).toContain('Tomas Tysén')
expect(msg).toContain('Bankuppgifter saknas')
expect(msg).toContain('Valideringsfel')
// Must NOT collapse to the generic HTTP-400 fallback.
expect(msg).not.toBe('Förfrågan innehåller ogiltiga uppgifter.')
})
it('joins multiple items and caps the list with an overflow hint', () => {
const details = Array.from({ length: 7 }, (_, i) => `Anställd ${i + 1}: Bankuppgifter saknas`)
const msg = getErrorMessage({ error: 'Valideringsfel', details }, { statusCode: 400 })
expect(msg).toContain('Anställd 1')
expect(msg).toContain('Anställd 5')
expect(msg).toContain('•')
expect(msg).toContain('(+2 till)')
expect(msg).not.toContain('Anställd 6')
})
it('ignores a non-string details array and falls through to the status fallback', () => {
const msg = getErrorMessage({ error: 'oklart fel', details: [{ x: 1 }] }, { statusCode: 400 })
expect(msg).toBe('Förfrågan innehåller ogiltiga uppgifter.')
})
})
describe('getErrorMessage — existing patterns still work', () => {
it('regex match for "Entry date ... outside fiscal period" on plain string', () => {
const msg = getErrorMessage('Entry date 2024-06-15 is outside fiscal period "FY 2025"')
expect(msg).toBe('Datumet ligger utanför det valda räkenskapsåret.')
})
it('regex match for "locked/closed fiscal period" on plain string', () => {
const msg = getErrorMessage('Cannot create entry in locked/closed fiscal period')
expect(msg).toBe('Perioden är låst. Verifikationen kan inte skapas i en stängd eller låst period.')
})
it('Swedish message passes through unchanged', () => {
const msg = getErrorMessage('Bokföringen är låst t.o.m. 2024-12-31.')
expect(msg).toBe('Bokföringen är låst t.o.m. 2024-12-31.')
})
it('falls through to context fallback when no pattern matches', () => {
const msg = getErrorMessage('Random English error', { context: 'transaction' })
expect(msg).toBe('Kunde inte hantera transaktionen. Försök igen.')
})
it('falls through to HTTP status map', () => {
const msg = getErrorMessage(null, { statusCode: 404 })
expect(msg).toBe('Resursen kunde inte hittas.')
})
it('falls through to generic message', () => {
const msg = getErrorMessage(null)
expect(msg).toBe('Något gick fel. Försök igen.')
})
})