fix(bookkeeping): settle unbound transactions on the company's single enabled cash account (#1831)
* fix(bookkeeping): settle unbound transactions on the company's single enabled cash account A transaction with no cash_account_id booked its bank leg on the hardcoded 1930 from the standard templates and category mappings even when the company's only bank account is e.g. 1920 (PlusGiro), while the booking dialogs previewed the right account via the client-side resolveAccount fallback. resolveSettlementAccount now mirrors that fallback: with a NULL cash_account_id it lists the company's enabled cash accounts and, when EXACTLY ONE matches the transaction's currency, settles there; zero or several candidates keep the 1930 fallback. The explicit-cash_account_id branch (including its throw-on-error path, issue #842) is byte-identical. Transaction currency is threaded into the categorize, batch-categorize, pending-operation edit, MCP staging, and invoice-inbox call sites; other callers get the SEK default. Forward-only: historical wrong verifikat are corrected only via the existing storno runbook (docs/SETTLEMENT_ACCOUNT_REMEDIATION.md). Fixes #1722 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF * test: align duplicate-guard mock queue with combined pre-FY and settlement-fallback lookups The merge of main (PR #1828) into this branch combined two changes that each add one query to the categorize commit flow; the strictly ordered queued mock in the allow_duplicate test needed the cash_accounts listing entry inserted between the period lookup and the pre-FY clamp lookup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SyDuePXxUFowaPBKpAv8SF --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
8e3015e541
commit
7cf15a105f
@@ -2651,6 +2651,7 @@ export const invoiceInboxExtension: Extension = {
|
||||
ctx.companyId,
|
||||
(tx as Transaction).cash_account_id,
|
||||
createLogger('invoice-inbox.suggest-booking'),
|
||||
(tx as Transaction).currency,
|
||||
)
|
||||
// evaluateMappingRules applies the settlement account itself on every
|
||||
// return path. Applying it again rewrote a legitimate 1930 leg, which
|
||||
|
||||
@@ -92,6 +92,7 @@ describe('gnubok_categorize_transaction: account_override', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow() }) // core: transactions
|
||||
enqueue({ data: settingsRow }) // core: company_settings
|
||||
enqueue({ data: [] }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: null }) // applyAccountOverride: chart_of_accounts miss
|
||||
|
||||
await expect(
|
||||
@@ -108,6 +109,7 @@ describe('gnubok_categorize_transaction: account_override', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow() })
|
||||
enqueue({ data: settingsRow })
|
||||
enqueue({ data: [] }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: { account_number: '4020', account_class: 4, is_active: false } })
|
||||
|
||||
await expect(
|
||||
@@ -124,6 +126,7 @@ describe('gnubok_categorize_transaction: account_override', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow() })
|
||||
enqueue({ data: settingsRow })
|
||||
enqueue({ data: [] }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
|
||||
await expect(
|
||||
categorize.execute(
|
||||
@@ -139,6 +142,7 @@ describe('gnubok_categorize_transaction: account_override', () => {
|
||||
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow() }) // core: transactions
|
||||
enqueue({ data: settingsRow }) // core: company_settings
|
||||
enqueue({ data: [] }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: { account_number: '4020', account_class: 4, is_active: true } }) // override chart hit
|
||||
enqueue({ data: guardTxRow() }) // tool: transactions re-fetch
|
||||
enqueue({ data: null }) // resolvePeriodStatusForDate: company_settings
|
||||
|
||||
@@ -120,6 +120,7 @@ describe('gnubok_categorize_transaction: category presence guard (#1662)', () =>
|
||||
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow() }) // core: transactions
|
||||
enqueue({ data: settingsRow }) // core: company_settings
|
||||
enqueue({ data: [] }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: guardTxRow() }) // tool: transactions re-fetch
|
||||
enqueue({ data: null }) // resolvePeriodStatusForDate: company_settings
|
||||
enqueue({ data: null }) // resolvePeriodStatusForDate: fiscal_periods
|
||||
|
||||
@@ -102,6 +102,7 @@ describe('gnubok_categorize_transaction duplicate-guard message', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow(), error: null }) // core: transactions
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null }) // core: settings
|
||||
enqueue({ data: [], error: null }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: guardTxRow(), error: null }) // tool: transactions re-fetch
|
||||
|
||||
mockDetectDup.mockResolvedValue(
|
||||
@@ -127,6 +128,7 @@ describe('gnubok_categorize_transaction duplicate-guard message', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow({ amount: -1616, currency: 'SEK' }), error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: [], error: null }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: guardTxRow({ amount: -1616, currency: 'SEK' }), error: null })
|
||||
|
||||
mockDetectDup.mockResolvedValue(candidate({ amount: -1616 }))
|
||||
@@ -143,6 +145,7 @@ describe('gnubok_categorize_transaction duplicate-guard message', () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueue({ data: coreTxRow({ amount: -8570.87 }), error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: [], error: null }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: guardTxRow({ amount: -8570.87 }), error: null })
|
||||
|
||||
mockDetectDup.mockResolvedValue(
|
||||
|
||||
@@ -731,7 +731,7 @@ describe('gnubok_categorize_transaction: dimensions bag', () => {
|
||||
})
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: { ledger_account: '1931' }, error: null })
|
||||
enqueue({ data: { ledger_account: '1931' }, error: null }) // resolveSettlementAccount: explicit cash_account_id lookup
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: null, error: null })
|
||||
enqueue({ data: null, error: null })
|
||||
@@ -764,6 +764,7 @@ describe('gnubok_categorize_transaction: dimensions bag', () => {
|
||||
// categorizeTransactionCore: transaction fetch + company_settings
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: [], error: null }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
// transaction fetch for the title
|
||||
enqueue({ data: tx, error: null })
|
||||
// resolveDimensionBags: settings → ensure rpc → dimensions → dimension_values
|
||||
@@ -820,6 +821,7 @@ describe('gnubok_categorize_transaction: dimensions bag', () => {
|
||||
const tx = makeTransaction({ id: 'tx-1', amount: -500 })
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null })
|
||||
enqueue({ data: [], error: null }) // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
enqueue({ data: tx, error: null })
|
||||
enqueue({ data: null, error: null }) // period status layer 1
|
||||
enqueue({ data: null, error: null }) // period status layer 2
|
||||
|
||||
@@ -361,6 +361,7 @@ describe('MCP Receipt Matcher', () => {
|
||||
enqueueMany([
|
||||
{ data: tx, error: null }, // fetch transaction (preview)
|
||||
{ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null },
|
||||
{ data: [], error: null }, // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
{ data: tx, error: null }, // fetch transaction for title
|
||||
{ data: null, error: null }, // resolvePeriodStatusForDate: company_settings
|
||||
{ data: null, error: null }, // resolvePeriodStatusForDate: fiscal_periods
|
||||
@@ -404,6 +405,7 @@ describe('MCP Receipt Matcher', () => {
|
||||
enqueueMany([
|
||||
{ data: tx, error: null },
|
||||
{ data: { entity_type: 'enskild_firma', fiscal_year_start_month: 1 }, error: null },
|
||||
{ data: [], error: null }, // resolveSettlementAccount: no enabled cash accounts -> 1930
|
||||
{ data: tx, error: null }, // fetch transaction for title
|
||||
{ data: null, error: null }, // resolvePeriodStatusForDate: company_settings
|
||||
{ data: null, error: null }, // resolvePeriodStatusForDate: fiscal_periods
|
||||
|
||||
@@ -1141,6 +1141,7 @@ async function categorizeTransactionCore(
|
||||
companyId,
|
||||
transaction.cash_account_id,
|
||||
log,
|
||||
transaction.currency,
|
||||
)
|
||||
mappingResult = applySettlementAccount(mappingResult, settlementAccount)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user