Refactor bookkeeping error handling and introduce new error classes (#356)

- Introduced new error classes for better error categorization:
  - JournalEntryNotBalancedError
  - FiscalPeriodNotFoundError
  - EntryDateOutsideFiscalPeriodError
  - JournalEntryNotFoundError
  - CannotReverseNonPostedError
  - CannotCorrectNonPostedError
  - EntryAlreadyReversedError
  - CurrencyRevaluationAlreadyExistsError
  - InvalidMappingResultError
  - BookkeepingDatabaseError

- Updated existing functions in engine.ts and transaction-entries.ts to throw specific errors instead of generic ones.
- Enhanced error response handling in get-error-message.ts to provide localized messages for new error types.
- Added unit tests for new error classes and error handling functions to ensure correctness and coverage.
This commit is contained in:
Mattsson
2026-04-23 14:49:45 +02:00
committed by GitHub
parent e137a9f452
commit 0222e084bb
32 changed files with 944 additions and 126 deletions
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { eventBus } from '@/lib/events/bus'
import { makeJournalEntry, makeJournalEntryLine } from '@/tests/helpers'
import { BookkeepingDatabaseError } from '@/lib/bookkeeping/errors'
// ============================================================
// Mock — separate client (no .then) from query builder (thenable)
@@ -172,7 +173,7 @@ describe('correctEntry', () => {
const supabase = makeClient()
await expect(
correctEntry(supabase as never, 'company-1', 'user-1', 'orig-1', correctedLines)
).rejects.toThrow('Failed to create corrected entry')
).rejects.toThrow(BookkeepingDatabaseError)
})
it('cancels reversal entry when reversal lines fail', async () => {
@@ -189,7 +190,7 @@ describe('correctEntry', () => {
const supabase = makeClient()
await expect(
correctEntry(supabase as never, 'company-1', 'user-1', 'orig-1', correctedLines)
).rejects.toThrow('Failed to create reversal lines')
).rejects.toThrow(BookkeepingDatabaseError)
})
it('emits journal_entry.corrected event', async () => {
+18 -13
View File
@@ -6,7 +6,14 @@ import type {
JournalEntryLine,
} from '@/types'
import { validateBalance, getNextVoucherNumber, getSwedishLocalDate } from '@/lib/bookkeeping/engine'
import { AccountsNotInChartError } from '@/lib/bookkeeping/errors'
import {
AccountsNotInChartError,
BookkeepingDatabaseError,
CannotCorrectNonPostedError,
EntryAlreadyReversedError,
JournalEntryNotBalancedError,
JournalEntryNotFoundError,
} from '@/lib/bookkeeping/errors'
/**
* Storno Service - 3-step correction flow per Bokföringslagen
@@ -55,9 +62,7 @@ export async function correctEntry(
// Validate the corrected lines are balanced
const balance = validateBalance(correctedLines)
if (!balance.valid) {
throw new Error(
`Corrected entry is not balanced: debits (${balance.totalDebit}) != credits (${balance.totalCredit})`
)
throw new JournalEntryNotBalancedError(balance.totalDebit, balance.totalCredit, 'correction')
}
// Fetch original entry with lines
@@ -69,11 +74,11 @@ export async function correctEntry(
.single()
if (fetchError || !original) {
throw new Error('Original journal entry not found')
throw new JournalEntryNotFoundError()
}
if (original.status !== 'posted') {
throw new Error('Can only correct posted entries')
throw new CannotCorrectNonPostedError(original.status)
}
const originalLines = (original.lines as JournalEntryLine[]) || []
@@ -104,7 +109,7 @@ export async function correctEntry(
.single()
if (reversalError || !reversalEntry) {
throw new Error(`Failed to create reversal entry: ${reversalError?.message}`)
throw new BookkeepingDatabaseError('create_reversal_entry', reversalError?.message)
}
// Insert reversed lines (swap debit and credit)
@@ -130,7 +135,7 @@ export async function correctEntry(
if (reversalLinesError) {
await cancelEntry(supabase, reversalEntry.id)
throw new Error(`Failed to create reversal lines: ${reversalLinesError.message}`)
throw new BookkeepingDatabaseError('create_reversal_lines', reversalLinesError.message)
}
// Post the reversal entry
@@ -141,7 +146,7 @@ export async function correctEntry(
if (postReversalError) {
await cancelEntry(supabase, reversalEntry.id)
throw new Error(`Failed to post reversal entry: ${postReversalError.message}`)
throw new BookkeepingDatabaseError('post_reversal_entry', postReversalError.message)
}
// NOTE: Original entry is NOT marked as 'reversed' here. We defer that
@@ -200,7 +205,7 @@ export async function correctEntry(
.single()
if (correctedError || !newEntry) {
throw new Error(`Failed to create corrected entry: ${correctedError?.message}`)
throw new BookkeepingDatabaseError('create_corrected_entry', correctedError?.message)
}
correctedEntry = newEntry
@@ -230,7 +235,7 @@ export async function correctEntry(
if (correctedLinesError) {
await cancelEntry(supabase, correctedEntry.id)
throw new Error(`Failed to create corrected lines: ${correctedLinesError.message}`)
throw new BookkeepingDatabaseError('create_corrected_lines', correctedLinesError.message)
}
// Post the corrected entry
@@ -241,7 +246,7 @@ export async function correctEntry(
if (postCorrectedError) {
await cancelEntry(supabase, correctedEntry.id)
throw new Error(`Failed to post corrected entry: ${postCorrectedError.message}`)
throw new BookkeepingDatabaseError('post_corrected_entry', postCorrectedError.message)
}
} catch (err) {
// Cancel the reversal entry (posted → cancelled). Original was never
@@ -265,7 +270,7 @@ export async function correctEntry(
// Concurrent reversal beat us — cancel both our entries
await cancelEntry(supabase, reversalEntry.id)
await cancelEntry(supabase, correctedEntry!.id)
throw new Error('Entry was already reversed by a concurrent operation')
throw new EntryAlreadyReversedError()
}
// ===== Step 3: Fetch complete entries =====