fix(transactions): bind manually-fed transactions to a cash account (#1016) (#1017)

* fix(transactions): bind manually-fed transactions to a cash account (#1016)

create_transactions inserted rows with cash_account_id = null, so ledger
accounts fed via MCP/CSV without a PSD2 feed (e.g. 1935 Wise SEK) had no
kassakonto: get_reconciliation_status 404'd with "Okänt kassakonto" and the
"Matcha mot befintlig verifikation" dialog fell back to 1930.

No schema change: cash_accounts.bank_connection_id is already nullable and
source='manual' already exists (every company is seeded a manual 1930). This
is the creation-side leg of the #985-#987 root cause: the resolution chain was
fixed, but manually-fed accounts never got the cash_account_id link.

- Add ensureManualCashAccount (lib/cash-accounts/service.ts): find-or-create a
  manual (source='manual', bank_connection_id=null) cash_accounts row for a
  ledger slot, tolerating the (company_id, ledger_account) UNIQUE race.
- Add an optional ledger_account hint (^19xx) to gnubok_create_transactions;
  commitCreateTransaction resolves/creates the manual account and sets
  cash_account_id on the inserted row.

Reconciliation and voucher matching then resolve the real account unchanged.
Forward-looking; historical cash_account_id=null remediation stays in #1001.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

* fix(cash-accounts): guard ensureManualCashAccount against currency mismatch (CodeRabbit #1017)

The existing-row lookup matched only on (company_id, ledger_account) and
returned the row id ignoring currency, so a SEK transaction hinting at a ledger
already claimed for USD would bind to the wrong-currency cash account. Since
that pair is UNIQUE (one currency per ledger), a mismatch is a real conflict:
throw instead of silently mis-binding.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Alexander Reinthal <email@reinthal.me>

---------

Signed-off-by: Alexander Reinthal <email@reinthal.me>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com>
This commit is contained in:
Alexander Reinthal
2026-07-16 16:15:06 +02:00
committed by GitHub
co-authored by Claude Opus 4.8 Jakob Wennberg
parent aa5edd3aa7
commit f8033cb32d
6 changed files with 262 additions and 0 deletions
+15
View File
@@ -26,6 +26,7 @@ import {
createCreditNoteJournalEntry,
} from '@/lib/bookkeeping/invoice-entries'
import { resolveSettlementAccount } from '@/lib/bookkeeping/settlement-account'
import { ensureManualCashAccount } from '@/lib/cash-accounts/service'
import { createJournalEntry, findFiscalPeriod, reverseEntry, validateBalance } from '@/lib/bookkeeping/engine'
import { coerceDimensionsBag } from '@/lib/bookkeeping/dimension-resolver'
import { cancelOrphanedPaymentEntry } from '@/lib/bookkeeping/cancel-orphaned-entry'
@@ -648,12 +649,25 @@ async function commitCreateTransaction(
const amount = Number(params.amount)
const description = (params.description as string) ?? ''
const currency = ((params.currency as string) || 'SEK') as Currency
const ledgerAccount = (params.ledger_account as string) || null
const bankConnectionId = (params.bank_connection_id as string) || null
const externalId = (params.external_id as string) || null
if (!date || !description.trim() || !Number.isFinite(amount)) {
return { error: 'date, description, and amount are required', status: 400 }
}
if (ledgerAccount && !/^19\d{2}$/.test(ledgerAccount)) {
return { error: 'ledger_account must be a BAS 19xx cash account', status: 400 }
}
// Bind the row to a manual kassakonto when a ledger account is given, so
// reconciliation and voucher matching resolve the real account instead of
// falling back to 1930 (issue #1016). Find-or-create; the row's currency
// follows this transaction.
let cashAccountId: string | null = null
if (ledgerAccount) {
cashAccountId = await ensureManualCashAccount(supabase, companyId, ledgerAccount, currency)
}
const { data, error } = await supabase
.from('transactions')
@@ -661,6 +675,7 @@ async function commitCreateTransaction(
user_id: userId,
company_id: companyId,
bank_connection_id: bankConnectionId,
cash_account_id: cashAccountId,
external_id: externalId,
date,
description: description.trim(),