fix: unignore transactions during categorization (#1683)

Fixes #1660
This commit is contained in:
Mattsson
2026-08-19 11:00:02 +02:00
committed by GitHub
parent b069d9a9fe
commit 43cde6deb9
22 changed files with 1270 additions and 190 deletions
@@ -102,6 +102,31 @@ describe('errorResponse', () => {
expect(body.error.details).toMatchObject({ pgCode: '23505' })
})
it('maps the ignored-transaction journal constraint to a typed conflict', async () => {
const pgErr = Object.assign(
new Error(
'new row for relation "transactions" violates check constraint "transactions_is_ignored_no_journal_entry"',
),
{ code: '23514' },
)
const res = errorResponse(pgErr, noopLogger, { requestId: 'req_ignored_tx' })
expect(res.status).toBe(409)
const body = await readEnvelope(res)
expect(body.error.code).toBe('TX_CATEGORIZE_IGNORED_CONFLICT')
expect(body.error.message).not.toContain('check constraint')
expect(body.error.details).toMatchObject({ pgCode: '23514' })
})
it('does not apply unrelated message heuristics to Postgres errors', async () => {
const pgErr = Object.assign(new Error('Invoice not found'), { code: 'P0001' })
const res = errorResponse(pgErr, noopLogger, { requestId: 'req_pg_unrelated' })
expect(res.status).toBe(500)
const body = await readEnvelope(res)
expect(body.error.code).toBe('INTERNAL_ERROR')
})
it('maps Postgres no-data-found to NOT_FOUND with pgCode', async () => {
const pgErr = Object.assign(new Error('invoice not found'), { code: 'P0002' })
const res = errorResponse(pgErr, noopLogger, { requestId: 'req_pg_not_found' })
+13 -2
View File
@@ -189,7 +189,9 @@ export function getStructuredError(
const message_sv = getErrorMessage(error)
const transient = isTransientFailure(error, message_en)
let code = extractCode(error) ?? inferCode(message_en) ?? 'UNKNOWN_ERROR'
let code = isIgnoredTransactionJournalConstraint(error)
? 'TX_CATEGORIZE_IGNORED_CONFLICT'
: extractCode(error) ?? inferCode(message_en) ?? 'UNKNOWN_ERROR'
// Nothing more specific matched but the failure is transient: surface the
// stable TRANSIENT_ERROR code so agents can dispatch on it.
if (code === 'UNKNOWN_ERROR' && transient) code = 'TRANSIENT_ERROR'
@@ -313,6 +315,13 @@ function isPostgresError(err: unknown): err is { code: string; message: string }
)
}
export function isIgnoredTransactionJournalConstraint(error: unknown): boolean {
return (
isPostgresError(error) &&
/transactions_is_ignored_no_journal_entry/i.test(error.message)
)
}
/**
* Build the canonical REST error envelope for any thrown value.
*
@@ -358,7 +367,9 @@ export function errorResponse(
// 3. Postgres errors
if (isPostgresError(err)) {
const mapped = postgresCodeToStructured(err.code)
const mapped = isIgnoredTransactionJournalConstraint(err)
? 'TX_CATEGORIZE_IGNORED_CONFLICT'
: postgresCodeToStructured(err.code)
if (mapped) {
const entry = entryFor(mapped)
logAtLevel(log, entry.httpStatus, 'database error', err as unknown as Error, {
+10
View File
@@ -447,6 +447,16 @@ const TRANSACTIONS: Record<string, StructuredErrorEntry> = {
message_sv: 'Transaktionen kategoriserades av en annan förfrågan. Ladda om och försök igen.',
message_en: 'Transaction was already categorized by another request.',
},
TX_CATEGORIZE_IGNORED_CONFLICT: {
httpStatus: 409,
message_sv:
'Transaktionen är fortfarande markerad som ignorerad och kan därför inte kopplas till en verifikation.',
message_en:
'The transaction is still marked as ignored and cannot be linked to a journal entry.',
remediation: {
description: 'Reload and retry categorization. Report the conflict if it persists.',
},
},
TX_CATEGORIZE_SUGGEST_SI_MATCH: {
httpStatus: 409,
message_sv: