* 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>