fix(errors): translate typed engine errors instead of leaking raw messages as journal_entry_error (#1048)
Typed bookkeeping Error instances passed to getErrorMessage() matched the bare-envelope branch (any object with string code + message) and returned their raw English message verbatim, so the categorize and match-invoice routes surfaced strings like DB check-constraint violations directly in the user's toast (issue #337). - get-error-message.ts: when the bare-envelope shape is an Error instance, normalize it into the structured envelope ({ error: { code, message, account_numbers, details } }) so the existing per-code Swedish branches own the translation; plain forwarded envelopes keep the passthrough. - get-error-message.ts: structured-path final fallback now prefers the registry's message_sv for known codes whose message is not Swedish, so typed codes without a dynamic branch (e.g. CANNOT_REVERSE_STORNO) cannot surface English either. - categorize + match-invoice routes: always map the caught error through getErrorMessage (the raw error is already logged); untyped errors fall to the Swedish context fallback instead of leaking err.message. - Tests: new instance-translation suite in lib/errors, typed-error case in the categorize route suite, and deliberate updates of the two tests that pinned raw 'Period locked' passthrough. Fixes #337 Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
d182cf5d93
commit
88f53350de
@@ -1,5 +1,11 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { getErrorMessage } from '../get-error-message'
|
||||
import {
|
||||
AccountsNotInChartError,
|
||||
BookkeepingDatabaseError,
|
||||
CannotReverseStornoError,
|
||||
JournalEntryNotBalancedError,
|
||||
} from '@/lib/bookkeeping/errors'
|
||||
|
||||
describe('getErrorMessage: typed bookkeeping error codes', () => {
|
||||
it('ACCOUNTS_NOT_IN_CHART → lists accounts to activate', () => {
|
||||
@@ -95,6 +101,57 @@ describe('getErrorMessage: typed bookkeeping error codes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getErrorMessage: typed bookkeeping Error instances (issue #337)', () => {
|
||||
it('JournalEntryNotBalancedError instance → rich Swedish amount message', () => {
|
||||
const msg = getErrorMessage(new JournalEntryNotBalancedError(100, 80), { context: 'transaction' })
|
||||
expect(msg).toContain('balanserar inte')
|
||||
expect(msg).toMatch(/100/)
|
||||
expect(msg).toMatch(/80/)
|
||||
expect(msg).not.toContain('Journal entry is not balanced')
|
||||
})
|
||||
|
||||
it('BookkeepingDatabaseError instance → Swedish, never the raw constraint string', () => {
|
||||
const msg = getErrorMessage(
|
||||
new BookkeepingDatabaseError(
|
||||
'commit_entry',
|
||||
'new row for relation "journal_entries" violates check constraint "check_balanced"',
|
||||
),
|
||||
{ context: 'transaction' },
|
||||
)
|
||||
expect(msg).toBe('Verifikationen kunde inte sparas. Försök igen.')
|
||||
expect(msg).not.toContain('check constraint')
|
||||
expect(msg).not.toContain('Database operation')
|
||||
})
|
||||
|
||||
it('BookkeepingDatabaseError instance wrapping a period-lock trigger → specific Swedish message', () => {
|
||||
const msg = getErrorMessage(
|
||||
new BookkeepingDatabaseError('commit_entry', '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('AccountsNotInChartError instance → Swedish account-activation message', () => {
|
||||
const msg = getErrorMessage(new AccountsNotInChartError(['1930']))
|
||||
expect(msg).toBe('Följande konton behöver aktiveras: 1930')
|
||||
})
|
||||
|
||||
it('CannotReverseStornoError instance → registry Swedish message (no dynamic branch)', () => {
|
||||
const msg = getErrorMessage(new CannotReverseStornoError('reversal'))
|
||||
expect(msg).toBe('En stornering eller rättelse kan inte stornas.')
|
||||
expect(msg).not.toContain('Cannot reverse')
|
||||
})
|
||||
|
||||
it('locale "en" on a typed instance → registry English message', () => {
|
||||
const msg = getErrorMessage(new CannotReverseStornoError('reversal'), { locale: 'en' })
|
||||
expect(msg).toBe('A storno or correction entry cannot be reversed.')
|
||||
})
|
||||
|
||||
it('regression: plain-object bare envelope with a Swedish message passes through unchanged', () => {
|
||||
const msg = getErrorMessage({ code: 'SOME_CODE', message: 'Kunde inte hantera fakturan. Försök igen.' })
|
||||
expect(msg).toBe('Kunde inte hantera fakturan. Försök igen.')
|
||||
})
|
||||
})
|
||||
|
||||
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'
|
||||
|
||||
@@ -273,6 +273,28 @@ export function getErrorMessage(
|
||||
// of the whole `result`. Pick the English variant when the UI locale is
|
||||
// English; otherwise fall back to the Swedish `message`.
|
||||
if (typeof obj.code === 'string' && typeof obj.message === 'string' && obj.message.trim()) {
|
||||
// Typed domain exceptions (lib/bookkeeping/errors.ts classes) also match
|
||||
// this shape, but their `message` is raw English (often a DB constraint
|
||||
// string) and must never reach the user verbatim. Normalize the instance
|
||||
// into the structured envelope so the per-code branches below own the
|
||||
// translation. Class fields are enumerable own props, so { ...obj }
|
||||
// carries exactly the details those branches expect (totalDebit,
|
||||
// lockDate, reason, issues, ...), while the non-enumerable Error.message
|
||||
// stays out of details. Plain objects (forwarded inner envelopes,
|
||||
// PostgrestError-shaped literals) keep the passthrough behavior.
|
||||
if (error instanceof Error) {
|
||||
return getErrorMessage(
|
||||
{
|
||||
error: {
|
||||
code: obj.code,
|
||||
message: obj.message,
|
||||
account_numbers: (obj as { accountNumbers?: unknown }).accountNumbers,
|
||||
details: { ...obj },
|
||||
},
|
||||
},
|
||||
options
|
||||
)
|
||||
}
|
||||
if (locale === 'en' && typeof obj.message_en === 'string' && obj.message_en.trim()) {
|
||||
return obj.message_en
|
||||
}
|
||||
@@ -407,6 +429,13 @@ export function getErrorMessage(
|
||||
return structured.message_en
|
||||
}
|
||||
if (typeof structured.message === 'string' && structured.message.trim()) {
|
||||
// Known codes without a dynamic branch above (e.g. CANNOT_REVERSE_STORNO)
|
||||
// carry raw English engine messages: prefer the registry's Swedish
|
||||
// message so no typed code surfaces English in a Swedish UI.
|
||||
if (locale === 'sv' && typeof structured.code === 'string' && !isSwedishUserMessage(structured.message)) {
|
||||
const entry = getErrorEntry(structured.code)
|
||||
if (entry?.message_sv) return entry.message_sv
|
||||
}
|
||||
return structured.message
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user