diff --git a/DECISIONS.md b/DECISIONS.md index ba3de17f..ac7cf9fc 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -164,6 +164,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-15] Superseded the hard deletion part of the 2026-07-14 credit note draft decision: numbered credit note drafts are now retained as cancelled rows and reopened on retry so the KR series remains complete. [2026-07-15] Customer personnummer uses application field encryption with masked API and UI output rather than a database-only cipher: the existing key custody and AES-256-GCM implementation can protect values before they reach Postgres, while ordinary reads never expose the full identifier. [2026-07-15] Credit note creation uses a completion marker plus unique company guards instead of a large creation RPC: incomplete parents are never returned, concurrent requests converge, and all journal writes remain in the bookkeeping engine. +[2026-07-16] Closed #1000 (v1 match-supplier-invoice FX + cash-method settlement account): threaded the already-resolved paymentAccount into createSupplierInvoicePaymentEntry (was gated on isPureSek) and createSupplierInvoiceCashEntry (was passed undefined), and widened the findUnresolvableAccounts chart pre-validation from the pure-SEK accrual path to every !customLines branch, since every non-customLines branch now consumes the resolved account. The dashboard route needed no code change (its FX/cash branches were already threaded inside PR #985 itself); added branch-level regression tests on both routes instead. The entry generators keep their internal 1930 default: it is the documented no-link fallback, reached via resolveSettlementAccount returning 1930 for transactions without a cash_account_id. [2026-07-16] Two bugs from one customer report (an AB). Bug 1 (acct 2893 showed 2393's "langfristig del" memo after an andringsverifikation): root cause = CorrectionEntryDialog never re-derived line_description on account change (JournalEntryForm does). Fixed forward via a pure helper (correction-line-description.ts) that refreshes the memo only when it is empty or still equals the prev account's name (preserves hand-typed memos). Chose NO prod data repair: the wrong memo sits on a POSTED verifikat (immutable per migration-017 trigger); it is cosmetic (account number + amounts correct, all reports key off the number); ~26 posted lines across 11 cos share this stale-echo pattern, all fix-forward only. Deferred the twin entry-level header fix (#1031). Bug 2 (auto tax-deadlines never appeared): root cause = generation only fired on a settings save where a TAX field CHANGED value (didTaxFieldsChange); settings are filled once at onboarding so re-saving generated nothing -> only 5/776 real cos had system deadlines. Chose count-based self-heal (regenerate when the company has 0 system deadlines) over always-regenerate, because generateTaxDeadlinesForUser deletes+reinserts and would reset is_completed/status on every unrelated save. Also wired the /deadlines empty-state to the existing (dead) /api/tax-deadlines/generate route, and fixed a 1000-row PostgREST cap in the annual cron. Backfilled 771 real cos with zero system deadlines via scripts/backfill-tax-deadlines.ts. Deferred moms_period=yearly config (#1030, 295 filers, largest VAT cohort): helarsmoms deadline (SFL 26 kap. 33-33b) depends on EU-trade status (no flag in CompanySettingsForDeadlines) and, for AB, the income-tax-return date. [2026-07-15] Repaired the single legacy paid credit note blocking invoices_credit_note_not_paid validation by normalizing its invoice metadata to sent, clearing payment fields, setting zero payable remainder, and linking its existing balanced posted V44 reversal: the immutable voucher already exactly reversed V42 and was not edited or duplicated. diff --git a/app/api/transactions/[id]/match-supplier-invoice/__tests__/route.test.ts b/app/api/transactions/[id]/match-supplier-invoice/__tests__/route.test.ts index b8bd45e2..cd157776 100644 --- a/app/api/transactions/[id]/match-supplier-invoice/__tests__/route.test.ts +++ b/app/api/transactions/[id]/match-supplier-invoice/__tests__/route.test.ts @@ -85,7 +85,12 @@ function makeReq() { } function enqueueHappyPath(opts: { - transaction: { amount: number; currency: string; amount_sek?: number | null } + transaction: { + amount: number + currency: string + amount_sek?: number | null + cash_account_id?: string | null + } invoice: { currency: string exchange_rate?: number | null @@ -94,6 +99,9 @@ function enqueueHappyPath(opts: { status?: string } accountingMethod?: string + // ledger_account returned by the cash_accounts lookup; only enqueued when + // the transaction carries a cash_account_id. + cashAccountLedger?: string }) { // 1. transactions fetch enqueue({ @@ -104,6 +112,7 @@ function enqueueHappyPath(opts: { currency: opts.transaction.currency, amount_sek: opts.transaction.amount_sek ?? null, supplier_invoice_id: null, + cash_account_id: opts.transaction.cash_account_id ?? null, date: '2026-05-12', }, error: null, @@ -124,11 +133,15 @@ function enqueueHappyPath(opts: { }) // 3. company_settings fetch enqueue({ data: { accounting_method: opts.accountingMethod ?? 'accrual' }, error: null }) - // 4. supplier_invoices update (CAS) + // 4. cash_accounts lookup (only when the transaction is linked to one) + if (opts.transaction.cash_account_id) { + enqueue({ data: { ledger_account: opts.cashAccountLedger ?? '1930' }, error: null }) + } + // 5. supplier_invoices update (CAS) enqueue({ data: [{ id: SI_UUID }], error: null }) - // 5. supplier_invoice_payments insert + // 6. supplier_invoice_payments insert enqueue({ data: null, error: null }) - // 6. transactions update (link) + // 7. transactions update (link) enqueue({ data: null, error: null }) } @@ -362,6 +375,65 @@ describe('POST /api/transactions/[id]/match-supplier-invoice: settlement account }) }) +describe('POST /api/transactions/[id]/match-supplier-invoice: settlement account on FX and cash-method branches', () => { + // Regression locks for #1000: the FX branch (createSupplierInvoicePaymentEntry) + // and cash-method branch (createSupplierInvoiceCashEntry) must receive the + // resolved settlement account, not fall back to their internal 1930 default + // whenever the transaction is linked to a different cash account. + it('FX branch: passes the linked non-1930 account to createSupplierInvoicePaymentEntry', async () => { + enqueueHappyPath({ + transaction: { amount: -2400, currency: 'SEK', cash_account_id: 'ca-1940' }, + invoice: { currency: 'EUR', exchange_rate: 10.6254, remaining_amount: 225 }, + cashAccountLedger: '1940', + }) + const res = await POST(makeReq(), createMockRouteParams({ id: TX_UUID })) + expect(res.status).toBe(200) + expect(mockCreatePaymentEntry).toHaveBeenCalledTimes(1) + const args = mockCreatePaymentEntry.mock.calls[0] + // Confirm the FX branch actually ran: booked 2390.72, paid 2400 -> loss 9.28. + expect(args[6]).toBeCloseTo(-9.28, 2) + expect(args[8]).toBe('1940') + }) + + it('FX branch: falls back to 1930 when the transaction has no linked cash account', async () => { + enqueueHappyPath({ + transaction: { amount: -2400, currency: 'SEK' }, + invoice: { currency: 'EUR', exchange_rate: 10.6254, remaining_amount: 225 }, + }) + const res = await POST(makeReq(), createMockRouteParams({ id: TX_UUID })) + expect(res.status).toBe(200) + expect(mockCreatePaymentEntry.mock.calls[0][8]).toBe('1930') + }) + + it('cash-method branch: passes the linked non-1930 account to createSupplierInvoiceCashEntry', async () => { + enqueueHappyPath({ + transaction: { amount: -500, currency: 'SEK', cash_account_id: 'ca-1940' }, + invoice: { currency: 'SEK', remaining_amount: 500 }, + accountingMethod: 'cash', + cashAccountLedger: '1940', + }) + const res = await POST(makeReq(), createMockRouteParams({ id: TX_UUID })) + expect(res.status).toBe(200) + expect(mockCreateCashEntry).toHaveBeenCalledTimes(1) + expect(mockCreatePaymentEntry).not.toHaveBeenCalled() + const args = mockCreateCashEntry.mock.calls[0] + expect(args[8]).toBe('1940') + // Pure SEK settlement: no settledBankSek override. + expect(args[9]).toBeUndefined() + }) + + it('cash-method branch: falls back to 1930 when the transaction has no linked cash account', async () => { + enqueueHappyPath({ + transaction: { amount: -500, currency: 'SEK' }, + invoice: { currency: 'SEK', remaining_amount: 500 }, + accountingMethod: 'cash', + }) + const res = await POST(makeReq(), createMockRouteParams({ id: TX_UUID })) + expect(res.status).toBe(200) + expect(mockCreateCashEntry.mock.calls[0][8]).toBe('1930') + }) +}) + describe('POST /api/transactions/[id]/match-supplier-invoice: non-FX paths', () => { it('returns 200 with the expected body shape on the happy path', async () => { enqueueHappyPath({ diff --git a/app/api/v1/companies/[companyId]/transactions/[id]/__tests__/route.test.ts b/app/api/v1/companies/[companyId]/transactions/[id]/__tests__/route.test.ts index c1aed7ee..7e45c58f 100644 --- a/app/api/v1/companies/[companyId]/transactions/[id]/__tests__/route.test.ts +++ b/app/api/v1/companies/[companyId]/transactions/[id]/__tests__/route.test.ts @@ -28,12 +28,13 @@ vi.mock('@supabase/supabase-js', async () => { }) // Engine stubs: happy-path returns reusable across cases. -const { createTxJE, reverseEntryMock, createInvPmtJE, createInvCashJE, createSupplierInvPmtJE, findMissingAccountsMock } = vi.hoisted(() => ({ +const { createTxJE, reverseEntryMock, createInvPmtJE, createInvCashJE, createSupplierInvPmtJE, createSupplierInvCashJE, findMissingAccountsMock } = vi.hoisted(() => ({ createTxJE: vi.fn().mockResolvedValue({ id: 'je-fresh' }), reverseEntryMock: vi.fn().mockResolvedValue(undefined), createInvPmtJE: vi.fn().mockResolvedValue({ id: 'je-invpmt' }), createInvCashJE: vi.fn().mockResolvedValue({ id: 'je-invcash' }), createSupplierInvPmtJE: vi.fn().mockResolvedValue({ id: 'je-sipmt' }), + createSupplierInvCashJE: vi.fn().mockResolvedValue({ id: 'je-sicash' }), // Default: no missing accounts. Per-case overrides simulate the // template-references-inactive-account bug or a race where deactivation // happened between our validation and the engine's resolveAccountIds. @@ -52,7 +53,7 @@ vi.mock('@/lib/bookkeeping/invoice-entries', () => ({ })) vi.mock('@/lib/bookkeeping/supplier-invoice-entries', () => ({ createSupplierInvoicePaymentEntry: createSupplierInvPmtJE, - createSupplierInvoiceCashEntry: vi.fn().mockResolvedValue({ id: 'je-sicash' }), + createSupplierInvoiceCashEntry: createSupplierInvCashJE, })) vi.mock('@/lib/invoices/match-log', () => ({ logMatchEvent: vi.fn(), @@ -1120,4 +1121,192 @@ describe('POST :id/match-supplier-invoice', () => { expect(createSupplierInvPmtJE).not.toHaveBeenCalled() }) }) + + // Regression for #1000: the FX and cash-method branches were left on the + // entry generators' internal 1930 default when the pure-SEK path was fixed + // (#986). A foreign-currency match, or a kontantmetoden match, settling + // from a non-primary account (e.g. a EUR account on 1940) was still + // misbooked to 1930. + describe('settlement account resolution (FX and cash-method branches)', () => { + function fxTables(cashAccountId: string | null) { + return { + company_members: { data: { company_id: COMPANY_ID, role: 'owner' }, error: null }, + transactions: { + data: { + id: TX_ID, + amount: -2400, + date: '2026-05-12', + currency: 'SEK', + amount_sek: null, + supplier_invoice_id: null, + journal_entry_id: null, + cash_account_id: cashAccountId, + }, + error: null, + }, + supplier_invoices: [ + { + data: { + id: SI_ID, + status: 'approved', + total: 225, + paid_amount: 0, + remaining_amount: 225, + currency: 'EUR', + exchange_rate: 10.6254, + supplier: { name: 'Acme GmbH', supplier_type: 'eu_business' }, + items: [], + }, + error: null, + }, + { data: [{ id: SI_ID }], error: null }, + ], + company_settings: { data: { accounting_method: 'accrual' }, error: null }, + cash_accounts: { data: { ledger_account: '1940' }, error: null }, + supplier_invoice_payments: { data: null, error: null }, + } + } + + function cashMethodTables(cashAccountId: string | null) { + return { + company_members: { data: { company_id: COMPANY_ID, role: 'owner' }, error: null }, + transactions: { + data: { + id: TX_ID, + amount: -5000, + date: '2026-05-12', + currency: 'SEK', + amount_sek: null, + supplier_invoice_id: null, + journal_entry_id: null, + cash_account_id: cashAccountId, + }, + error: null, + }, + supplier_invoices: [ + { + data: { + id: SI_ID, + status: 'approved', + total: 5000, + paid_amount: 0, + remaining_amount: 5000, + currency: 'SEK', + exchange_rate: null, + supplier: { name: 'Acme', supplier_type: 'swedish_business' }, + items: [], + }, + error: null, + }, + { data: [{ id: SI_ID }], error: null }, + ], + company_settings: { data: { accounting_method: 'cash' }, error: null }, + cash_accounts: { data: { ledger_account: '1940' }, error: null }, + supplier_invoice_payments: { data: null, error: null }, + } + } + + it('FX branch: credits the transaction\'s own linked non-1930 account', async () => { + mockServiceClient.mockReturnValue(makeFlexibleSupabase(fxTables('ca-1940'))) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(200) + expect(createSupplierInvPmtJE).toHaveBeenCalledTimes(1) + const args = createSupplierInvPmtJE.mock.calls[0] + // Confirm we exercised the FX branch: 225 EUR @ 10.6254 booked as + // 2390.72 SEK, bank paid 2400 SEK -> loss of 9.28. + expect(args[4]).toBeCloseTo(2390.72, 2) + expect(args[6]).toBeCloseTo(-9.28, 2) + expect(args[8]).toBe('1940') + }) + + it('FX branch: falls back to 1930 when the transaction has no linked cash account', async () => { + mockServiceClient.mockReturnValue(makeFlexibleSupabase(fxTables(null))) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(200) + expect(createSupplierInvPmtJE.mock.calls[0][8]).toBe('1930') + }) + + it('cash-method branch: credits the transaction\'s own linked non-1930 account', async () => { + mockServiceClient.mockReturnValue(makeFlexibleSupabase(cashMethodTables('ca-1940'))) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(200) + expect(createSupplierInvCashJE).toHaveBeenCalledTimes(1) + expect(createSupplierInvPmtJE).not.toHaveBeenCalled() + const args = createSupplierInvCashJE.mock.calls[0] + expect(args[8]).toBe('1940') + // Pure SEK settlement: no settledBankSek override. + expect(args[9]).toBeUndefined() + }) + + it('cash-method branch: falls back to 1930 when the transaction has no linked cash account', async () => { + mockServiceClient.mockReturnValue(makeFlexibleSupabase(cashMethodTables(null))) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(200) + expect(createSupplierInvCashJE.mock.calls[0][8]).toBe('1930') + }) + + it('cash-method branch: rejects with ACCOUNTS_NOT_IN_CHART when the resolved account is deactivated', async () => { + // The chart pre-validation guard must cover the branches that now + // consume paymentAccount, not only the pure-SEK accrual path. + mockServiceClient.mockReturnValue(makeFlexibleSupabase(cashMethodTables('ca-1940'))) + findMissingAccountsMock.mockResolvedValueOnce(['1940']) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(400) + expect((await res.json()).error.code).toBe('ACCOUNTS_NOT_IN_CHART') + expect(createSupplierInvCashJE).not.toHaveBeenCalled() + }) + + it('does NOT storno an existing conflicting JE when chart validation rejects the request', async () => { + // Regression: the chart pre-validation must run BEFORE the + // conflicting-categorization storno. Otherwise a request that is + // ultimately rejected with ACCOUNTS_NOT_IN_CHART would first reverse + // the transaction's posted categorization entry: an irreversible side + // effect on a failed request. + const tables = cashMethodTables('ca-1940') + ;(tables.transactions.data as { journal_entry_id: string | null }).journal_entry_id = JE_ID + mockServiceClient.mockReturnValue(makeFlexibleSupabase(tables)) + findMissingAccountsMock.mockResolvedValueOnce(['1940']) + const res = await matchSIPOST( + makeRequest( + `https://x.test/api/v1/companies/${COMPANY_ID}/transactions/${TX_ID}/match-supplier-invoice`, + { supplier_invoice_id: SI_ID }, + ), + txParams(TX_ID), + ) + expect(res.status).toBe(400) + expect((await res.json()).error.code).toBe('ACCOUNTS_NOT_IN_CHART') + expect(reverseEntryMock).not.toHaveBeenCalled() + expect(createSupplierInvCashJE).not.toHaveBeenCalled() + }) + }) }) diff --git a/app/api/v1/companies/[companyId]/transactions/[id]/match-supplier-invoice/route.ts b/app/api/v1/companies/[companyId]/transactions/[id]/match-supplier-invoice/route.ts index be37b719..e1f69460 100644 --- a/app/api/v1/companies/[companyId]/transactions/[id]/match-supplier-invoice/route.ts +++ b/app/api/v1/companies/[companyId]/transactions/[id]/match-supplier-invoice/route.ts @@ -39,7 +39,7 @@ registerEndpoint({ path: '/api/v1/companies/:companyId/transactions/:id/match-supplier-invoice', summary: 'Match a negative bank transaction to a supplier invoice.', description: - 'Confirms a supplier invoice payment match. Creates the payment journal entry (accrual: 2440 debit / 1930 credit; cash-method: collapsed registration+payment), updates supplier_invoices, inserts a supplier_invoice_payments row, and links the transaction. Handles FX differences for cross-currency payments (7960 gain / 3960 loss).', + 'Confirms a supplier invoice payment match. Creates the payment journal entry (accrual: 2440 debit, credit on the transaction\'s own settlement account, 1930 when unlinked; cash-method: collapsed registration+payment), updates supplier_invoices, inserts a supplier_invoice_payments row, and links the transaction. Handles FX differences for cross-currency payments (7960 gain / 3960 loss).', useWhen: 'You have a bank payment and a known open supplier invoice. The transaction must be negative (expense) and unlinked.', doNotUseFor: @@ -151,6 +151,41 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string }) } + // Credit the cash account THIS transaction actually belongs to, never a + // hardcoded 1930: cash_account_id -> cash_accounts.ledger_account is the + // only source of truth for which bank account a matched transaction + // settled from (mirrors the dashboard route's #985 fix). Threaded through + // every booking branch below (pure-SEK accrual, FX, and cash-method); + // customLines specify their own accounts directly (#1000). + const paymentAccount = await resolveSettlementAccount( + ctx.supabase, + ctx.companyId!, + transaction.cash_account_id, + txLog, + ) + + // Guard the resolved account against the chart (mirrors the categorize + // routes): an inactive cash_accounts.ledger_account would otherwise reach + // the engine as a generic MATCH_SI_RECORD_PAYMENT_FAILED instead of + // ACCOUNTS_NOT_IN_CHART. Gated on !customLines: custom lines specify their + // own accounts directly; every other branch consumes paymentAccount. + // This MUST run before the conflicting-JE storno below: a request rejected + // here must leave no trace, and reversing the existing categorization JE + // is an irreversible side effect (posted vouchers are immutable). + if (!customLines) { + const missingAccounts = await findUnresolvableAccounts( + ctx.supabase, + ctx.companyId!, + [paymentAccount], + ) + if (missingAccounts.length > 0) { + txLog.warn('resolved settlement account is inactive/unknown', { missingAccounts }) + return v1ErrorResponse(new AccountsNotInChartError(missingAccounts), txLog, { + requestId: ctx.requestId, + }) + } + } + // Storno any conflicting auto-categorization JE before booking the // payment. Mirrors the match-invoice path. Without this, an earlier // :categorize of the same transaction (e.g. as expense_office with a @@ -221,48 +256,12 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string .single() const accountingMethod = settings?.accounting_method || 'accrual' - // Credit the cash account THIS transaction actually belongs to, never a - // hardcoded 1930: cash_account_id -> cash_accounts.ledger_account is the - // only source of truth for which bank account a matched transaction - // settled from (mirrors the dashboard route's #985 fix). Only applied to - // the pure-SEK accrual path below; the FX path keeps its pre-existing - // internal 1930 default, matching that fix's scope. - const paymentAccount = await resolveSettlementAccount( - ctx.supabase, - ctx.companyId!, - transaction.cash_account_id, - txLog, - ) - // Route on the supplier invoice's actual booking state. An invoice // booked at receipt (registration_journal_entry_id set) must clear // 2440 regardless of the company's current setting. const siAlreadyBooked = !!(invoice as { registration_journal_entry_id?: string | null }).registration_journal_entry_id const useCashEntry = !siAlreadyBooked && accountingMethod === 'cash' - // Pure SEK: both legs of the match are SEK, so the payment account - // resolved above can safely replace the 1930 default. Kept out of scope - // for foreign-currency matches, same as the dashboard route. - const isPureSek = transaction.currency === 'SEK' && invoice.currency === 'SEK' - - // Guard the resolved account against the chart (mirrors the categorize - // routes): an inactive cash_accounts.ledger_account would otherwise reach - // the engine as a generic MATCH_SI_RECORD_PAYMENT_FAILED instead of - // ACCOUNTS_NOT_IN_CHART. Only reachable where the account is actually used. - if (isPureSek && !useCashEntry && !customLines) { - const missingAccounts = await findUnresolvableAccounts( - ctx.supabase, - ctx.companyId!, - [paymentAccount], - ) - if (missingAccounts.length > 0) { - txLog.warn('resolved settlement account is inactive/unknown', { missingAccounts }) - return v1ErrorResponse(new AccountsNotInChartError(missingAccounts), txLog, { - requestId: ctx.requestId, - }) - } - } - // Full settlement = the bank amount pays off the whole remaining balance. // Cross-currency always settles the remaining (paymentAmountInvoiceCurrency // is clamped to invoice.remaining_amount above). @@ -330,9 +329,13 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string transaction.date, invoice.supplier?.supplier_type || 'swedish_business', undefined, // supplierName (unchanged default) - undefined, // paymentAccount (unchanged default 1930) - // Pin a foreign-currency settlement to the payment-date rate so 1930 - // equals the bank movement. No-op for SEK / same-rate settlements. + // Settle from the transaction's own resolved cash account; the + // internal 1930 default only stands for unlinked transactions, via + // resolveSettlementAccount's own fallback (#1000). + paymentAccount, + // Pin a foreign-currency settlement to the payment-date rate so the + // settlement account equals the bank movement. No-op for SEK / + // same-rate settlements. exchangeRateDifference !== 0 && fullSettlement ? actualBankSek : undefined, ) if (je) journalEntryId = je.id @@ -346,10 +349,10 @@ export const POST = withApiV1<{ params: Promise<{ companyId: string; id: string transaction.date, exchangeRateDifference !== 0 ? exchangeRateDifference : undefined, undefined, // supplierName (unchanged default) - // Resolved settlement account, pure-SEK matches only: the FX - // branch keeps defaulting internally to 1930, out of scope here - // just as it was for the dashboard route's #985 fix. - isPureSek ? paymentAccount : undefined, + // Resolved settlement account for pure-SEK and FX matches alike: + // the internal 1930 default only stands for unlinked transactions, + // via resolveSettlementAccount's own fallback (#1000). + paymentAccount, ) if (je) journalEntryId = je.id }