Files
accounted/lib/auth
Jakob Wennberg 13be0c569a feat(mcp): expose multi-tx RPCs (match_batch_allocate + bulk_book_transactions) (#614)
* 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>
2026-05-30 13:36:46 +02:00
..
2026-05-18 19:02:42 +02:00