* feat(enable-banking): per-account ledger mapping for multicurrency setups (#443)
Today every bank account routes to BAS 1930. Multicurrency setups (Wise,
SEB foreign-currency sub-accounts) get pooled into a single SEK ledger
account, making year-end FX revaluation a mess.
This change lets each bank account under a PSD2 consent map to its own BAS
account (SEK→1930, EUR→1932, USD→1933, etc.). The mapping engine already
honors IngestOptions.settlementAccount (lib/bookkeeping/mapping-engine.ts:55-57)
so the wiring is small:
- StoredAccount gains an optional ledger_account field (no migration —
bank_connections.accounts_data is already JSONB).
- syncAccountTransactions passes account.ledger_account through as
settlementAccount so the bank-side leg routes to the right BAS account.
- PATCH /accounts accepts account_mappings, validates 4-digit BAS format,
and verifies each ledger_account exists in chart_of_accounts before
persisting. Selection edits without account_mappings preserve existing
values for back-compat.
- AccountPickerDialog shows a per-account "Bokför till konto" combobox
populated from the company's 19xx accounts, with currency-based
defaults (SEK→1930, EUR→1932, USD→1933, GBP→1934) and a non-blocking
warning when two enabled accounts route to the same BAS account with
different currencies.
Reconciliation still scans 1930 only — foreign-currency accounts skip
auto-matching until follow-up PR 4 (filed in the plan).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(enable-banking): address review feedback on PR #487
Two real bugs flagged in review:
1. AccountPickerDialog row wrapped a Radix <Checkbox> (which renders as its
own <button role="checkbox">) inside <button onClick={toggle}>. Browsers
silently flatten nested interactive elements, the Checkbox lost its
onCheckedChange handler, and the toggle interaction was effectively
broken. Reverted to <label> + <Checkbox onCheckedChange> with the <Select>
as a sibling outside the label so clicking it doesn't also toggle.
2. BAS_ACCOUNT_PATTERN was /^[0-9]{4}$/ — accepted 3001 (revenue), 2640 (input
VAT), or any 4-digit account that happens to exist in the chart. Direct
API calls would bypass the UI's 19% picker filter and silently misroute
every bank-side journal-entry leg into the wrong BAS class, corrupting both
the ledger and momsdeklaration. Tightened to /^19[0-9]{2}$/ (kassa/bank only).
Tests updated to assert non-19xx rejection.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(enable-banking): validate account_mappings UIDs against accounts_data
A typo'd UID in account_mappings was silently dropped — the entry never
landed in the resulting accounts_data while the response was still 200,
leaving the client to believe the mapping was applied. Mirror the existing
enabled_uids guard: reject unknown UIDs with 400 + unknown_uids in the body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(enable-banking): catch syncPromise rejections to prevent process crash
When the 60s timeout wins the Promise.race, the underlying Promise.all
keeps running. A subsequent bank-API rejection has no registered handler,
which surfaces as an unhandledRejection — Node 22 (the self-hosted Docker
runtime) terminates the process by default on those.
The cron self-heals via initial_sync_completed_at IS NULL, so a no-op
catch is the right policy: drop the late rejection, let the cron retry.
Caught by Greptile review on PR #488.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>