Commit Graph

3 Commits

Author SHA1 Message Date
Jonas Flodén a3491b6d89 fix(transactions): resolve bank account from cash_account_id in bulk-book and direct-book dialogs (#770)
Follow-up to #769: applies the same fetch-on-open + resolveAccount pattern to
BulkBookDialog (manual tab bank-leg pre-fill) and BookDirectlyDialog (settlement
line in buildPrefillLines). Both dialogs now fetch /api/cash-accounts when they
open, gate the pre-fill on the fetch resolving, and resolve the correct BAS
ledger account per transaction instead of always emitting '1930'.

Adds lib/cash-accounts/resolve-account.ts (cherry of the utility from #769)
since that PR is not yet merged into main.

Signed-off-by: Jonas Flodén <jonas@floden.nu>
2026-06-25 12:40:09 +02:00
Jakob Wennberg 28f7cefc86 feat(bulk-book): manual booking mode + document inheritance (#610)
* 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>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 10:23:14 +02:00
Jakob Wennberg 4da87e5e4c feat(transactions): bulk-book + is-booked predicate (#606)
* feat(transactions): bulk-book + is-booked predicate

Closes the second of the two multi-tx ↔ multi-voucher flows from the
original plan. Where PR #603's match_batch_allocate took 1 tx and
spread it across N invoices (samlingsbetalning), this PR takes N bank
transactions on the same day and rolls them up into ONE combined
verifikat (samlingsverifikation per BFL 5 kap 6§ st 3) — the kiosk
masshantering pattern the user explicitly asked for.

## Backend (Phase 3b)

- **PL/pgSQL RPC** bulk_book_transactions: two branches, both atomic.

  1. Link to existing posted verifikat (p_existing_journal_entry_id):
     no new JE. Validates the JE's 19xx net equals sum(tx.amount),
     inserts N transaction_voucher_links rows, and for N=1 also sets
     transactions.journal_entry_id (1:1 reader-path back-compat).

  2. Create new combined verifikat (p_new_entry with pre-computed
     balanced lines): the route's applyTemplate() has already done
     ratio + VAT expansion per the chosen mode. The RPC validates the
     lines balance and the 1930 net matches sum(tx.amount), then
     commits via commit_journal_entry.

  Same security pattern as match_batch_allocate: company-member check
  via auth.uid(), SELECT … FOR UPDATE on each tx in id order,
  deterministic fiscal-period resolution (ORDER BY period_start DESC).

- **Endpoint** POST /api/transactions/bulk-book — fetches template via
  RLS, expands per mode (one_line_per_tx | sum_per_account) using
  lib/bookkeeping/template-library.applyTemplate, passes the resulting
  lines to the RPC. On success emits one transaction.reconciled event
  per tx.

- **22 new BULK_BOOK_* error codes** (sv + en) covering all guard
  paths.

## UI (Phase 5b)

- **BulkBookDialog** — template picker + mode toggle (segmented
  control: en rad per transaktion / summera per konto) + live preview
  table with balance + bank-leg invariant indicators. Confirm only
  enabled when both pass.

- **Multi-select inbox** — sticky action bar gains a "Bokför i klump"
  button gated by same-date + same-direction across selected txs.
  Tooltip explains the disabled state.

## Phase 6: is-booked predicate

New lib/transactions/is-booked.ts. After multi-allocation and bulk-
book, tx.journal_entry_id can be NULL even though the tx is anchored
(via invoice_payments / supplier_invoice_payments /
transaction_voucher_links). The helper checks all three storage
locations so future readers don't falsely show multi-anchored txs as
"unbooked". Companion getPrimaryJournalEntryId() resolves the best
JE link to surface in UI. SQL mirror is_transaction_booked() exists
from the PR #602 foundation migration.

Existing readers (TransactionHistoryList, TransactionInboxCard) are
not yet refactored to use the helper — that's a follow-up that
touches per-tx JE links across multiple call sites. The helper is
documented + tested so subsequent refactors are mechanical.

## Tests

- tests/pg/bulk-book-transactions.pg.test.ts — 8 pg-real scenarios
  (happy path create-new with 3 txs, happy path link-existing, date
  mismatch, direction mismatch, amount mismatch, unbalanced lines,
  unauthorized).
- app/api/transactions/bulk-book/__tests__/route.test.ts — 5 unit
  tests (schema XOR, link path, create-new with template fetch +
  applyTemplate, structured-error mapping).
- lib/transactions/__tests__/is-booked.test.ts — 11 cases covering
  all three storage locations + primary-JE resolution.

26 unit tests pass on touched paths. RPC migration applied to remote
via Supabase MCP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(bulk-book): PR #606 review round 1 + CI fixes

Closes the build failure and the two real Greptile findings.

## CI

- **core-only + Vercel build fail**: I used useMemo for
  selectedTransactions and bulkBookEligible on the transactions page
  without importing it. TypeScript build (`next build`) caught it
  with "Cannot find name 'useMemo'". Fixed the import.

## Review findings

- **(P1) Currency mismatch returned BULK_BOOK_DIRECTION_MISMATCH**
  whose user-facing message blames direction. Mixed SEK + EUR batches
  would show "All transactions must be the same direction" which is
  factually wrong. Introduced dedicated BULK_BOOK_MIXED_CURRENCY code
  (sv + en) explaining the actual constraint, and switched the route
  to use it.

- **(P1) Branch B (create-new) N=1 missed
  reconciliation_method='manual'**. Branch A's N=1 UPDATE sets it
  alongside journal_entry_id; Branch B's didn't, leaving the
  reconciliation_method NULL even though the single tx was reconciled
  via the same flow. Downstream readers (reconciliation reports,
  status indicators) would treat the two N=1 paths differently. New
  follow-up migration patches Branch B's final UPDATE.

RPC patch applied to remote via Supabase MCP. 26 unit 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-29 14:55:24 +02:00