fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches (#1033)

* fix(v1): thread resolved settlement account through FX and cash-method supplier-payment branches

The v1 (MCP-facing) match-supplier-invoice route resolved paymentAccount
via resolveSettlementAccount but only passed it to
createSupplierInvoicePaymentEntry for pure-SEK matches (gated on
isPureSek) and never to createSupplierInvoiceCashEntry at all. A
foreign-currency match, or a kontantmetoden match, settling from a
bank/cash account other than the primary 1930 (e.g. a EUR account on
1940) was still misbooked to 1930: the same class of bug PR #985/#986
fixed for the pure-SEK accrual path.

Pass the resolved account through both branches unconditionally (the
generators' internal 1930 default remains the documented no-link
fallback, reached via resolveSettlementAccount's own fallback for
transactions without a cash_account_id), and widen the
findUnresolvableAccounts chart pre-validation from the pure-SEK accrual
path to every non-customLines branch, since all of them now consume the
resolved account.

The dashboard route needed no code change: its FX/cash-method branches
were already threaded inside PR #985 itself. Added branch-level
regression tests on both routes (linked non-1930 account books to that
account; no cash_account_id falls back to 1930; deactivated resolved
account rejects with ACCOUNTS_NOT_IN_CHART before booking).

Fixes #1000

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(v1): validate settlement account against the chart before the conflicting-JE storno

The widened findUnresolvableAccounts pre-validation ran after the
conflicting-categorization storno, so a request rejected with
ACCOUNTS_NOT_IN_CHART could first reverse the transaction's posted
categorization entry: an irreversible side effect on a failed request.
Move the paymentAccount resolution and the chart validation ahead of
the storno block (same !customLines guard, same error shape) and add a
regression test asserting reverseEntry is never called when the chart
validation fails. The dashboard route has no storno block and no chart
pre-validation on this path, so it is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-16 18:13:09 +02:00
committed by GitHub
parent 1dc85736d8
commit f0907da7e9
4 changed files with 315 additions and 50 deletions
+1
View File
@@ -164,6 +164,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. 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.
@@ -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({
@@ -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()
})
})
})
@@ -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
}