* feat(bulk-book): manual booking mode + document inheritance
Two pieces of user feedback from PR #606:
1. "How come it is only mallar? Is it not possible to have manuell
bokfoering?" - BulkBookDialog was template-only. Added a Tabs
primitive with Mall / Manuell tabs. Manual tab pre-fills lines from
the selected txs (one line per tx on 1930 + counterparty
placeholder on 3001/5800 by direction), then the user edits Konto /
Debet / Kredit / Beskrivning. Live balance + bank-leg checks drive
the confirm button - same invariants the RPC enforces server-side.
2. "Documents attached does not follow into the bookkeeping. And if
there are two different documents attached, none of them follow."
The bulk_book_transactions RPC now propagates each tx's document
onto the target verifikat (new in Branch B, existing in Branch A)
as verifikationsunderlag. Per BFL 5 kap 6§ + BFNAR 2013:2 kap 4 a
verifikat may have multiple underlag; every receipt that justified
a tx is now retention-protected on the combined entry. The dialog
shows a small count chip ("N bilagor foeljer med") so the user
sees what will inherit.
Also dropped p_user_id from the RPC signature (round-3 hardening
pattern applied consistently across all multi-tx RPCs after PR #607).
Caller resolves from auth.uid() inside the function.
Schema: BulkBookSchema is now a 3-way XOR
(existing_journal_entry_id | template_id+mode | manual_lines), with
manual_lines validated as accountNumber + nonNegativeAmount per line.
pg-real tests:
- doc inheritance into a new combined verifikat (mixed: 2 of 3 txs
have docs - docs_linked should be 2, not 3)
- doc inheritance into an existing posted verifikat (link branch)
- manual lines path (no template expansion artifacts in the
resulting JE - just the 2 user lines)
- unbalanced manual lines still rejected by BULK_BOOK_UNBALANCED
Migration applied to remote.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bulk-book): PR #610 review - pg-real signature, account allowlist, account-number validity
Three review findings on PR #610:
1. pg-real failure: 2 link-existing tests still used 5-arg SELECT
bulk_book_transactions($1::uuid[], $2, $3, $4, $5) after the userId
removal. My earlier replace_all caught only the patterns that had
::jsonb on $3; the link-existing tests pass null for new_entry and
used a bare $3 so they slipped through. (Greptile P1)
2. Manual lines bypassed chart_of_accounts validation. A typo or
adversarial caller could post to a BAS account that doesn't exist
in this company's chart, corrupting the hauptbok and breaking SIE
export. Both compliance-swarm (OWASP V2.3) and swedish-compliance
flagged this. Added a single-roundtrip allowlist check in the
route: query chart_of_accounts for distinct account_numbers in
manual_lines and reject with BULK_BOOK_INVALID_ACCOUNT if any are
missing or inactive.
3. UI canConfirm guard missed invalid account numbers. Account input
allows 1-3 digits and JS string comparison '193' >= '1900' is false,
so a 3-digit entry escapes bankLineNet, the bank match could pass
via other lines, and the server returned 400 only after submit.
Added previewLines.every(l => /^\d{4}$/.test(l.account_number)) to
canConfirm so the Confirm button stays disabled inline.
(Greptile P2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(bulk-book): PR #610 round 2 - RPC chart-of-accounts, doc tenant isolation, GRANTs
Seven compliance findings from the round-1 bot reviews:
Migration (20260602121000_bulk_book_round2_fixes.sql):
- RPC chart-of-accounts allowlist (defense-in-depth): every line in
p_new_entry.lines is now verified to be an active BAS account for
p_company_id. Closes the gap where the template branch and direct
DB callers (psql, future MCP) bypassed the route's manual-branch
check. Returns BULK_BOOK_INVALID_ACCOUNT with the offending list.
(OWASP V8.2.1 + SOC 2 CC6.3)
- Document inheritance CTE: added "AND d.company_id = p_company_id"
to the UPDATE join so the tenant isolation is enforced on both
sides (tx + doc), not just the tx side. Four bots converged on this
finding (V1.2.5, A.8.2, CC6.6, swedish-compliance).
- Bank-leg range check: "length(account_number) = 4 AND account_number
BETWEEN '1900' AND '1999'" replaces the bare lexicographic comparison.
Lexicographic-on-4-digit is safe today; the length guard is
defense-in-depth against schema drift. (swedish-compliance)
- Explicit role grants: REVOKE ALL FROM PUBLIC + GRANT EXECUTE TO
authenticated on both bulk_book_transactions and match_batch_allocate.
(SOC 2 CC6.1)
UI (BulkBookDialog):
- Manual-mode prefill no longer suggests a hardcoded 3001/5800
counterpart. Reason (swedish-compliance): a user accepting the
prefill could submit a verifikat with no VAT line (26xx),
under-reporting utgaaende moms. The bank side stays pre-filled
(unambiguous); the counterpart row scaffolds blank for the user
to choose.
Schema (BulkBookSchema):
- manual_lines.debit_amount + credit_amount bounded at 99,999,999 SEK
per line. Catches typos before the RPC. (compliance-swarm V4.5)
i18n:
- docs_inherit_hint terminology: "bilaga" -> "verifikationsunderlag"
and an explicit "sparas i 7 ar enligt BFL 7 kap" reminder.
swedish-compliance flagged that "bilaga" risks users treating the
files as deletable attachments rather than retention-bound
raekenskapsinformation.
Migration applied to remote.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): seed chart_of_accounts in bulk-book pg-real seedTenant
The round-2 RPC fix added a chart_of_accounts allowlist check inside
bulk_book_transactions, but the test fixtures don't seed COA — so
every existing test that submits lines (1930, 3001, 2611, etc.) now
returns BULK_BOOK_INVALID_ACCOUNT instead of the expected error code.
Seed the 8 accounts the suite actually uses directly in seedTenant
(cheaper than calling seed_chart_of_accounts which inserts the full
BAS 2026 chart).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): expose match_batch_allocate + bulk_book_transactions as MCP tools
Surfaces the multi-tx flows shipped in PRs #603/#606/#608/#610 so
Claude Desktop/Code can drive them via chat.
- migration 20260603120000: expand pending_operations.operation_type
CHECK to include match_batch_allocate, bulk_book_transactions, plus
undo_sie_import (which was missing from prior expansions despite
being wired in risk-tiers.ts and the commit dispatcher).
- types/index.ts: extend PendingOperationType.
- lib/pending-operations/risk-tiers.ts: match_batch_allocate = medium
(same tier as single-tx match), bulk_book_transactions = high
(creates a verifikat with arbitrary lines, same surface as
create_voucher).
- lib/pending-operations/commit.ts: thin commit handlers that call
the SQL RPCs and translate the structured error envelope. The RPCs
themselves do all the locking, balance checks, JE creation, voucher
number, payment/junction rows, and doc inheritance.
- extensions/general/mcp-server/server.ts: two new tool definitions.
Both stage via stagePendingOperation with period_status hint and
pre-validate inputs (direction, sum-equals-tx-abs, same-date,
not-already-booked) so the agent gets a clear error inline before
the RPC runs.
- payload-size.bench: bump from 30K to 31K tokens (with rationale).
Two new tools earn the bump; descriptions already trimmed to fit
the <=280-char description limit.
Migration applied to remote and version aligned with local filename.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 review - allocation guard, IDOR pre-check, currency + JE-date
Round-1 review fixes on PR #614:
- Greptile P1: per-allocation invoice_id / supplier_invoice_id guard.
The inputSchema marks both as optional (they're mutually exclusive
by kind), so JSON Schema can't express "X required iff Y=A". Added
explicit check in the execute handler: customer_invoice rows must
carry invoice_id; supplier_invoice rows must carry supplier_invoice_id.
- OWASP V8.2.1: IDOR pre-check on match_batch_allocate. Verify every
invoice / supplier_invoice referenced in the allocations belongs to
this company BEFORE staging. The RPC re-checks (BATCH_INVOICE_NOT_FOUND),
but failing fast at the MCP layer gives the agent a clear error.
- OWASP V8.2.1: same pre-check on bulk_book_transactions for
existing_journal_entry_id. Fetches the JE at stage time, verifies
status=posted and company_id, throws if not found.
- swedish-compliance: currency homogeneity check on bulk_book. Mixed
SEK + EUR in one samlingsverifikat violates BFL 5 kap 6§ st 3 motpart
clarity. Cross-currency batches go through match_batch_allocate
instead (which handles FX diff on 7960/3960).
- swedish-compliance: period-lock check on the link-existing branch now
uses MAX(tx_date, JE.entry_date), not just tx_date. Otherwise a tx in
an open period could attach to a verifikat in a locked period and
the guard would miss it.
- A.8.11 + CC7.2: sanitised RPC error logging. log.error now emits only
{ code, message } instead of the full error object — error.details can
echo invoice IDs, amounts, and counterparty identifiers.
Not actioned (PR-comment, no code change):
- V2.3 double-validation in commit handler — RPC enforces balance,
accounts, bank-leg via the chart_of_accounts allowlist (PR #610 round 2).
Commit handler is a thin pass-through by design.
- A.8.2 step-up approval for high-tier ops — architectural change
affecting all high-tier ops, not PR-scoped.
- V2.4 rate limiting on bulk endpoints — platform-level concern.
- 0.005 epsilon / account-class allowlist — pre-existing patterns.
- undo_sie_import storno requirement — separate RPC, this PR only
backfilled the missing CHECK constraint.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 2 - trust-boundary comments + balance pre-check + audit log
Round-2 review fixes (compliance-swarm went 14 -> 9 after round 1;
remaining HIGHs are all "do the same tenant check at multiple
layers"). The bot itself offers the alternative: "or document and
reference the specific RPC line that enforces this." Following that.
- commit.ts: trust-boundary comment blocks on both
commitMatchBatchAllocate and commitBulkBookTransactions, citing the
exact RPC + migration where tenant isolation + chart_of_accounts
allowlist are enforced authoritatively. The commit handler stays a
thin pass-through by design; re-querying would triple the same check
without adding security. (V8.2.1, A.8.2)
- commit.ts: structured success-path log.info() on both handlers with
companyId, operationType, journal_entry_id, and tx count. No raw
amounts or IDs that could echo PII. (V16)
- server.ts: balance pre-check on bulk_book create-new path. RPC
enforces BULK_BOOK_UNBALANCED authoritatively, but failing fast at
staging gives the agent a clear error before pending_operations is
even touched. (V2.3 / swedish-compliance)
Not actioned this round:
- V2.2 oneOf/if-then-else in JSON Schema for mutual exclusivity — JSON
Schema vocabulary support is shaky across MCP clients; runtime check
in execute() is the canonical pattern across the existing toolset.
- CC6.1 generic error string to caller — RPC error codes are
user-actionable (BULK_BOOK_UNBALANCED, BATCH_INVOICE_NOT_FOUND); a
generic string would degrade UX.
- CC7.2 audit RPC RAISE messages for PII — separate audit; not
PR-scoped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 3 — last 5 LOWs + salary_run/agi constraint backfill
Compliance-swarm went 14 → 9 → 5 (all LOW). Cleaning the last 5 + the
swedish-compliance findings.
- migration 20260603121000: backfill create_salary_run + generate_agi
into pending_operations.operation_type CHECK. Both have risk-tier
entries and commit executors but were never added (same bug class
as undo_sie_import). Production has no rows of either type today.
(swedish-compliance)
- server.ts: Number.isFinite guard in bulk_book balance pre-check.
Number(x) || 0 silently treats NaN as 0 — a malformed amount could
pass the balance check by accident. (compliance-swarm A.8.28)
- server.ts: count-equality + missing-set assertion in match_batch_allocate
tenant pre-check. Belt-and-suspenders so a null/undefined row in the
Supabase JSON response can't pass silently. Same pattern on both
invoice and supplier_invoice branches. (CC6.1)
- server.ts: fix BFL paragraph citation in currency-homogeneity
comment. Was "BFL 5 kap 6§ st 3", should be "BFL 5 kap 2§" (SEK
denomination) read with 5 kap 6§ (valutakurs). (swedish-compliance)
- server.ts: clarify 0.005 tolerance comment — it's for floating-point
equalisation only, not a rounding allowance. RPC enforces exact
balance to the öre. (swedish-compliance)
- commit.ts: expand audit-log txId comment — included intentionally
for trail-to-source join, scoped to companyId already logged.
(compliance-swarm A.8.15/CC7.2)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 4 — Swedish plural typo + balance comment parity + agent-routing hint
Round-3 review caught:
- swedish-compliance: \`kundfakturaor\` typo (real räkenskapsinformation
defect under BFL 5 kap 7§). Swedish plural for \`kundfaktura\` is
\`kundfakturor\` (drop the final \`a\`, add \`or\`), same for
\`leverantörsfaktura\` → \`leverantörsfakturor\`. Fixed via slice(-1) + 'or'.
- swarm A.8.28: match_batch_allocate balance tolerance check was
missing the equivalent "RPC enforces exact balance" comment that
bulk_book has. Added.
- swedish-compliance: currency-mismatch error message now routes the
agent to gnubok_match_batch_allocate for cross-currency allocations
instead of letting it retry with hand-built FX lines.
Not actioned (out of pattern / out of scope):
- Integer arithmetic for balance checks (codebase pattern is float +
epsilon; would diverge from match_batch_allocate, supplier-payment,
invoice-payment, etc.)
- DSD docs / runbook for txId-in-log and stripped-error.details
trade-offs (out of PR scope; tracked separately)
- Link-existing target verifikat description match (architectural;
every link-existing op would need this)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): expose link_transaction_to_journal_entry as MCP tool
The REST endpoint /api/transactions/[id]/link-journal-entry already lets the
duplicate-payment UI attach a bank tx to an already-posted verifikat without
creating new bookkeeping. Agents had no equivalent — closing that parity gap
so users on Claude can match bank txs against vouchers they booked manually.
The core link logic moves to lib/transactions/link-journal-entry.ts so both
the REST route and the new commit handler share one implementation (preserves
all structured-error codes, optimistic-lock invoice update, and compensating
rollback). New 'link_transaction_journal_entry' op type wired through the
risk tiers (medium), TOOL_SCOPE_MAP (transactions:write), and dispatcher.
Bumps the tools/list payload-size ceiling 31K → 31.5K — same family bump
PRs #603/#606 made when adding match_batch_allocate / bulk_book_transactions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 5 — bot findings on link_transaction_journal_entry
Addresses the swedish-compliance + compliance-swarm findings on commit 5b884c3a:
1. **CHECK constraint backfill** — new migration adding 'link_transaction_journal_entry'
to pending_operations.operation_type. Same bug class as the salary_run/agi backfill
in 20260603121000; without it, every staged op would be rejected silently in
production (BFL 5 kap 6–7§ audit-trail gap).
2. **Payment-date exchange rate** — invoice_payments.exchange_rate now uses
transaction.exchange_rate (rate on payment date) instead of invoice.exchange_rate
(rate on invoice date), per BFL 5 kap 2§ + ML 8 kap 21–23§. The full 3960/7960
posting still belongs to createInvoicePaymentJournalEntry by contract — this path
only links to an EXISTING verifikat.
3. **voucherLabel format centralized** — exported formatVoucherLabel helper returns
the canonical `A-12` format (with hyphen, matches gnubok_link_invoice_to_voucher
and SIE #VER cross-references). Both the MCP staging preview and the committed
service result import it, so the user can't approve one label and have a
different one land in the audit trail.
4. **Rollback warn log restored** — txLog.warn-equivalent (IDs only, no PII) when
the compensating rollback itself fails, surfacing partial-state gaps for
reconciliation per GDPR Art.5(1)(f) / SOC 2 CC7.2. Lost in the refactor that
extracted the shared service; now present in both rollback call sites.
5. **Commit-layer log.info** — structured success log mirroring
commitMatchBatchAllocate / commitBulkBookTransactions (companyId, tx/JE IDs,
settledInvoice boolean). No raw amounts or counterparty names.
6. **Data minimization on invoice fetch** — explicit column list replaces
select('*, customer:customers(name)') in the shared service; the MCP staging
pre-check now fetches only invoice_number + remaining_amount (drops total +
paid_amount). voucher_description omitted from preview_data per Art.25.
Test impact: existing route + dispatcher tests updated to expect `A-12` instead
of `A12`. All 4308 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): correct FX bookkeeping + UI for match-invoice flow
User report: matching a 230 SEK bank tx against a 140 USD invoice produced
1930 Dr 2 142,50 / 1510 Cr 2 142,50 — fictitious numbers that didn't match
either the bank receipt or the booked AR. Root cause: the preview route
called resolveSekAmount(tx.amount, null, INV.currency, INV.rate), treating
the SEK tx number as if it were in the invoice's currency and multiplying
by the invoice's stored rate. Both the preview and the commit then used
the bogus number on both legs and silently dropped the FX gain/loss.
A second issue surfaced in the same dialog: for a 1 250 SEK invoice with a
prior 230 SEK partial, the comparison row showed "Differens: 250 kr" (off
the original total) instead of "20 kr" (off the actual 1 020 kr remaining).
This patch:
1. **New shared helper** lib/bookkeeping/invoice-payment-lines.ts
- buildInvoicePaymentClearingLines(tx, invoice, description) → bank-leg,
AR-leg, fx-diff, and a balanced line array. Bank-leg is always the
actual SEK that hit the bank (resolveSekAmount with the TX's currency
context, honouring tx.amount_sek when set). AR-leg is the SEK value
of the customer-debt reduction at the invoice's stored rate. Diff
posts to 3960 (gain) or 7960 (loss) so the verifikat balances per
BFL 5 kap 4–5§. Mirrors the match_batch_allocate RPC's contract:
when the tx is cross-currency, the single match fully clears the
invoice's remaining amount.
2. **Preview route** uses the helper for the clearing branch — replaces
the buggy resolveSekAmount call. Now byte-identical to what commit
builds.
3. **Match-invoice POST** uses the helper + createJournalEntry directly
for the clearing path, bypassing createInvoicePaymentJournalEntry on
this single flow. mark-paid and other callers of that function still
work as before (full payment + caller-supplied exchangeRateDifference).
4. **InvoiceMatchDialog** compares the bank tx against
invoice.remaining_amount (not invoice.total) for both customer and
supplier branches; cross-currency dialogs now show the different-
currencies warning instead of a meaningless numeric diff. The dialog's
invoice card also displays remaining_amount.
8 new unit tests cover same-currency full/partial, cross-currency gain/loss,
exact match (no FX line), sub-öre tolerance, and USD-on-USD with pre-
populated amount_sek. All 4316 tests pass.
Scope note: this expands PR #614 beyond the original "expose multi-tx RPCs
as MCP tools" since the same FX bug class affected the new MCP tool too
(round 5 already addressed the invoice_payments.exchange_rate side).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 7 — CI build + 4 HIGH bot findings
Core Build was failing on e29a0ba2/5e9d4c3d due to a TypeScript type-cast
error in linkTransactionToJournalEntry. Plus the swedish-compliance review
flagged four substantive bugs in my recent commits.
1. **TS build error** — `invoice = invoiceRow as typeof invoice` inferred
`never` because the LHS type included `null`. Switched to a named
`FetchedInvoice` alias and `as unknown as FetchedInvoice`.
2. **TOOL_SCOPE_MAP missing two write-capable tools** (🟠 HIGH OWASP V8.2.1).
`gnubok_match_batch_allocate` and `gnubok_bulk_book_transactions` (added
in PRs #603/#606) were never registered, meaning any API key could invoke
them regardless of scope. Backfilled both with `transactions:write`.
3. **`paymentExchangeRate` fallback wrong-date rate** (swedish-compliance).
`transaction.exchange_rate ?? invoice.exchange_rate ?? null` falls back
to the INVOICE date's rate when the tx rate is null. Per ML 8 kap 21–23§
the payment row must record the PAYMENT-date rate. Removed the fallback
— `null` is correct when the tx is SEK; downstream lookups can populate
it lazily from Riksbanken if needed.
4. **Currency-mismatch corrupts paid_amount** (swedish-compliance). The
link path was accumulating `tx.amount` into `invoice.paid_amount` without
checking that the currencies matched. A 230 SEK tx applied to a USD
invoice would record "230 USD paid" silently. Added explicit
LINK_TX_INVOICE_CURRENCY_MISMATCH guard (400) — cross-currency
settlement must go through the match-invoice flow which routes through
buildInvoicePaymentClearingLines.
5. **Cross-currency PARTIAL overstates FX gain/loss** (swedish-compliance,
BFL 5 kap 4–5§). `buildInvoicePaymentClearingLines` was crediting the
FULL invoice remaining to 1510 on every cross-currency match — zeroing
the GL balance while the invoice row stayed at status=partially_paid,
and booking a fake huge FX diff to 3960/7960. Fix: only book FX-diff
when `bankSek >= arSekFullRemaining`. Partials default to
1930 = 1510 = bankSek, deferring the FX adjustment to the final
settlement (or to a manual mark-paid with explicit
exchange_rate_difference). Documented the helper as customer-invoice-
only (supplier-side has different DR/CR polarity and goes through
match_batch_allocate RPC).
Test impact: 1 helper test updated to match the defer-on-ambiguous-loss
behavior, 1 new test covers the partial-defers-FX path explicitly. All
4317 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): PR #614 round 8 — close out remaining bot findings
CI green on round 7 (4 of 4 checks), HIGH count 2 → 1. Round-8 closes
the remaining HIGH and the smaller doc/guard items.
1. **PI1.3 risk acknowledgment restored** (SOC 2 HIGH). The shared
rollbackTxLink helper already had warn-level logging on rollback
failure, but the explicit PI1.3 reference comment from the original
route was lost in the refactor. Added inline so the reconciliation-
gap risk is visible to future maintainers.
2. **MCP currency-mismatch pre-stage check.** gnubok_link_transaction_to_
journal_entry now fetches invoice.currency and rejects cross-currency
matches before staging, saving the user an approval round-trip when
the commit handler's LINK_TX_INVOICE_CURRENCY_MISMATCH guard would
fire anyway.
3. **fxDiffSek JSDoc clarified.** The sign convention (positive = loss,
negative = gain) is correct for verifikat balancing but counter-
intuitive at a P&L glance. Documented explicitly + pointed callers
needing a "gain" number at `bankSek - arSek`.
4. **Reject both invoice_id + supplier_invoice_id** on the same
match_batch_allocate row (V4.5). Extra IDs previously leaked into
preview_data silently.
5. **Reject zero-amount tx** in bulk_book_transactions direction guard
(A.8.28). A txs[0].amount === 0 would have mis-classified the batch
as 'expense'. Mirrors the existing guard in match_batch_allocate.
6. **Reject debit=0 && credit=0 lines** in bulk_book new_entry (BFL 5
kap 6§ — every verifikat line must represent a real bokföringspost
with a non-zero amount).
7. **Data-minimization comments** added on the match-invoice preview
route (amount_sek + exchange_rate fetch is for the FX-fix bank-leg
math) and on the bulk_book_transactions preview_data block (aggregate
counts only — no per-tx PII). Mirrors the pattern already documented
on gnubok_link_transaction_to_journal_entry.
Skipped:
- 1510 vs 1515 (osäkra kundfordringar) — future improvement, needs
reading the original invoice JE's account, not a single-tool fix.
- transaction_description PII masking in preview_data — needs product
call on the truncation strategy and would degrade approval-UX.
- "invoice.match_confirmed event removed" finding — false positive; the
event is emitted at lib/transactions/link-journal-entry.ts:270-280.
All 4317 tests pass; payload-size guard still under ceiling.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(invoices): PR #614 round 9 — block cross-currency in single match-invoice path
Closes the swedish-compliance finding from round-8 review: a SEK bank tx
matched against a USD invoice through /api/transactions/[id]/match-invoice
would silently corrupt invoice.paid_amount (accumulator treats SEK as USD)
and flip a 140 USD invoice to status='paid' after a tiny partial. The
round-6/7 FX fix corrected the JOURNAL ENTRY lines but the invoice STATE
update still ran the same broken accumulator.
Proper cross-currency settlement on this path requires converting tx.amount
to invoice.currency at the bank-date rate AND storing invoice_payments rows
with the right (amount, currency) pair. That's a larger design call that
belongs in its own PR.
This change blocks cross-currency on the single-allocation path:
- New MATCH_INVOICE_CURRENCY_MISMATCH structured error (400, bilingual)
- Same-currency check inserted right after MATCH_INVOICE_NOT_OPEN
- Mirrors the LINK_TX_INVOICE_CURRENCY_MISMATCH guard added to the link
path in round-7
- Routes the user to the multi-allocation flow (gnubok_match_batch_allocate)
which DOES handle 3960/7960 FX-diff postings end-to-end
Same-currency (SEK→SEK or USD→USD) remains fully supported including
partials; the buildInvoicePaymentClearingLines helper handles those correctly.
For SEK tx → USD invoice the user now gets a clean 400 error pointing at
the right flow, instead of silently corrupted ledger state.
1 new route test covers the guard. All 4318 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>