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
+8
View File
@@ -2831,6 +2831,7 @@ export const tools: McpTool[] = [
amount: { type: 'number', description: 'Positive = income, negative = expense.' },
description: { type: 'string', description: 'Free-text description shown in /transactions.' },
currency: { type: 'string', description: 'ISO 4217 code. Default SEK.' },
ledger_account: { type: 'string', description: 'Optional BAS 19xx cash account (e.g. "1935") this row settles on. Binds it to a manual kassakonto so reconciliation and voucher matching resolve the right account instead of falling back to 1930.' },
bank_connection_id: { type: 'string', description: 'Optional UUID of a bank_connections row to associate with.' },
external_id: { type: 'string', description: 'Optional external reference (e.g., Airtable record ID). Shown in the preview; the DB enforces uniqueness per user, so the second commit of the same external_id will fail at approval.' },
},
@@ -2862,6 +2863,7 @@ export const tools: McpTool[] = [
const amount = Number(item.amount)
const description = ((item.description as string) ?? '').trim()
const currency = ((item.currency as string) || 'SEK').toUpperCase()
const ledgerAccount = (item.ledger_account as string) || null
const bankConnectionId = (item.bank_connection_id as string) || null
const externalId = (item.external_id as string) || null
@@ -2874,12 +2876,18 @@ export const tools: McpTool[] = [
if (!description) {
throw new Error(`transactions[${i}].description is required.`)
}
// Restrict the hint to BAS group 19 (kassa/bank): binding a transaction
// to a non-cash account would misroute reconciliation and matching.
if (ledgerAccount && !/^19\d{2}$/.test(ledgerAccount)) {
throw new Error(`transactions[${i}].ledger_account must be a BAS 19xx cash account (e.g. "1935").`)
}
const params = {
date,
amount,
description,
currency,
ledger_account: ledgerAccount,
bank_connection_id: bankConnectionId,
external_id: externalId,
}