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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6629964780
commit
4da87e5e4c
@@ -1816,6 +1816,39 @@
|
||||
"exact_match": "Exact match",
|
||||
"no_search_results": "No invoice matches \"{term}\""
|
||||
},
|
||||
"tx_bulk_book": {
|
||||
"title": "Book {count} transactions from {date}",
|
||||
"description_income": "Create a single combined verifikat covering all selected incoming payments (BFL 5 kap 6§).",
|
||||
"description_expense": "Create a single combined verifikat covering all selected outgoing payments (BFL 5 kap 6§).",
|
||||
"summary_count": "{count, plural, one {# transaction} other {# transactions}}",
|
||||
"template_label": "Booking template",
|
||||
"no_templates": "No active templates available.",
|
||||
"system_badge": "Standard",
|
||||
"mode_label": "How should the lines be grouped?",
|
||||
"mode_per_tx": "One line per transaction",
|
||||
"mode_per_tx_hint": "Preserves per-transaction audit detail — more lines.",
|
||||
"mode_sum": "Sum per account",
|
||||
"mode_sum_hint": "Compact verifikat — all amounts summed per account.",
|
||||
"description_label": "Voucher description",
|
||||
"default_description": "Combined verifikat {date}",
|
||||
"preview_label": "Preview ({count} lines)",
|
||||
"preview_truncated": "+ {remaining} more lines on booking",
|
||||
"col_account": "Account",
|
||||
"col_description": "Description",
|
||||
"col_debit": "Debit",
|
||||
"col_credit": "Credit",
|
||||
"total_label": "Total",
|
||||
"balance_ok": "The verifikat balances.",
|
||||
"balance_off": "The verifikat doesn't balance (delta {delta}).",
|
||||
"bank_ok": "Bank line matches the transactions.",
|
||||
"bank_off": "The bank line's net amount doesn't match the transactions.",
|
||||
"error_title": "Bulk booking failed",
|
||||
"success_title": "Combined verifikat created",
|
||||
"success_description": "{count} transactions booked to verifikat {voucher}.",
|
||||
"unknown_voucher": "(no number)",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm booking"
|
||||
},
|
||||
"tx_match_allocation": {
|
||||
"title": "Split payment",
|
||||
"description_customer": "Allocate the incoming payment across one or more customer invoices. The verifikat lands as a samlingsverifikation per BFL 5 kap 6§.",
|
||||
|
||||
@@ -1816,6 +1816,39 @@
|
||||
"exact_match": "Exakt match",
|
||||
"no_search_results": "Ingen faktura matchar \"{term}\""
|
||||
},
|
||||
"tx_bulk_book": {
|
||||
"title": "Bokför {count} transaktioner från {date}",
|
||||
"description_income": "Skapa en samlingsverifikation som täcker alla valda inbetalningar (BFL 5 kap 6§).",
|
||||
"description_expense": "Skapa en samlingsverifikation som täcker alla valda utbetalningar (BFL 5 kap 6§).",
|
||||
"summary_count": "{count, plural, one {# transaktion} other {# transaktioner}}",
|
||||
"template_label": "Bokföringsmall",
|
||||
"no_templates": "Inga aktiva mallar tillgängliga.",
|
||||
"system_badge": "Standard",
|
||||
"mode_label": "Hur ska raderna grupperas?",
|
||||
"mode_per_tx": "En rad per transaktion",
|
||||
"mode_per_tx_hint": "Behåller spårbarhet per transaktion — fler rader.",
|
||||
"mode_sum": "Summera per konto",
|
||||
"mode_sum_hint": "Kompakt verifikation — alla belopp summeras per konto.",
|
||||
"description_label": "Beskrivning på verifikationen",
|
||||
"default_description": "Samlingsverifikation {date}",
|
||||
"preview_label": "Förhandsvisning ({count} rader)",
|
||||
"preview_truncated": "+ {remaining} fler rader visas vid bokföring",
|
||||
"col_account": "Konto",
|
||||
"col_description": "Beskrivning",
|
||||
"col_debit": "Debet",
|
||||
"col_credit": "Kredit",
|
||||
"total_label": "Totalt",
|
||||
"balance_ok": "Verifikationen balanserar.",
|
||||
"balance_off": "Verifikationen balanserar inte (differens {delta}).",
|
||||
"bank_ok": "Bankraden matchar transaktionerna.",
|
||||
"bank_off": "Bankradens nettobelopp matchar inte transaktionerna.",
|
||||
"error_title": "Bokföringen misslyckades",
|
||||
"success_title": "Samlingsverifikation skapad",
|
||||
"success_description": "{count} transaktioner bokförda till verifikat {voucher}.",
|
||||
"unknown_voucher": "(utan nummer)",
|
||||
"cancel": "Avbryt",
|
||||
"confirm": "Bekräfta bokföring"
|
||||
},
|
||||
"tx_match_allocation": {
|
||||
"title": "Dela betalning",
|
||||
"description_customer": "Fördela inbetalningen på en eller flera kundfakturor. Verifikationen skapas som en samlingsverifikation per BFL 5 kap 6§.",
|
||||
|
||||
Reference in New Issue
Block a user