fix(bookkeeping): resolve settlement account in all categorization flows and ship mis-booking audit (#1383)

Completes the #985/#986/#987 caller sweep: categorize-core, v1
batch-categorize, pending-operation edits and the MCP categorize path now
resolve the settlement leg from the transaction's cash account instead of
inheriting a hardcoded or stale account. Extends the correct_entry preview
with currency, tax and dimension line metadata so staged corrections
preserve full line fidelity. Adds a read-only audit query and a runbook for
reviewing and correcting historical mis-bookings via staged storno with
explicit approval; no automated bulk mutation.

Fixes #1001

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-03 18:45:38 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent e40dc64485
commit 8299ee9fb4
11 changed files with 726 additions and 4 deletions
@@ -247,6 +247,43 @@ describe('bulkBookMatchedInboxItems: booking', () => {
)
})
it('books against the linked cash account instead of the 1930 mapping default', async () => {
const supabase = queuedSupabase([
{ data: { id: 'i1', matched_transaction_id: 'tx-1', created_journal_entry_id: null, created_supplier_invoice_id: null } },
{
data: {
id: 'tx-1',
date: '2026-06-01',
amount: -700.28,
currency: 'SEK',
cash_account_id: 'cash-1',
journal_entry_id: null,
},
},
{ data: { entity_type: 'aktiebolag', fiscal_year_start_month: 1 } },
{ data: { ledger_account: '1931' } },
{ data: [{ id: 'fp-1' }] },
{ error: null },
{ data: [] },
])
const { booked, skipped } = await bulkBookMatchedInboxItems(supabase, 'u1', 'c1', {
item_ids: ['i1'],
category: 'expense_software',
})
expect(skipped).toEqual([])
expect(booked).toHaveLength(1)
expect(mockCreateJE).toHaveBeenCalledWith(
expect.anything(),
'c1',
'u1',
expect.objectContaining({ id: 'tx-1', cash_account_id: 'cash-1' }),
expect.objectContaining({ debit_account: '5420', credit_account: '1931' }),
undefined,
)
})
it('forwards the shared dimensions bag onto every booked mapping result', async () => {
// Fresh mapping object per call: the core mutates it in place, and a
// shared fixture would leak dimensions across tests.
+10 -1
View File
@@ -24,6 +24,8 @@
import type { SupabaseClient } from '@supabase/supabase-js'
import { eventBus } from '@/lib/events'
import { buildMappingResultFromCategory } from '@/lib/bookkeeping/category-mapping'
import { applySettlementAccount } from '@/lib/bookkeeping/mapping-engine'
import { resolveSettlementAccount } from '@/lib/bookkeeping/settlement-account'
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
import { upsertCounterpartyTemplate } from '@/lib/bookkeeping/counterparty-templates'
import { isBookkeepingError } from '@/lib/bookkeeping/errors'
@@ -329,9 +331,16 @@ export async function categorizeMatchedTransaction(
const entityType: EntityType = (settings?.entity_type as EntityType) || 'enskild_firma'
const fiscalYearStartMonth = settings?.fiscal_year_start_month ?? 1
const mappingResult = buildMappingResultFromCategory(
let mappingResult = buildMappingResultFromCategory(
category, transaction as Transaction, isBusiness, entityType, vatTreatment, vatAmount
)
const settlementAccount = await resolveSettlementAccount(
supabase,
companyId,
transaction.cash_account_id,
log,
)
mappingResult = applySettlementAccount(mappingResult, settlementAccount)
// Dimensions PR7: tag the business lines of the generated verifikat.
if (dimensions && Object.keys(dimensions).length > 0) {
mappingResult.dimensions = dimensions