* feat: voucher linking, recovery ops, and salary overrides
Adds reversible/correction-style write paths that customers and agents have
been asking for, plus per-run salary employee overrides.
Invoice → voucher linking
- POST /api/invoices/[id]/link-to-voucher and
GET /api/invoices/[id]/voucher-candidates
- lib/invoices/voucher-matching.ts with full + pg test coverage
- LinkVoucherPicker UI in PaymentBookingDialog
- pending_operations.operation_type expanded with link_invoice_voucher
(medium risk) and a (journal_entry_id, invoice_id) unique guard
- MCP: gnubok_find_voucher_candidates_for_invoice and
gnubok_link_invoice_to_voucher tools
SIE undo
- POST /api/import/sie/[id]/undo + undo_sie_import RPC
- sie_imports.status gains 'undone'
- ImportResultStep surfaces the action; structured error SIE_UNDO_FAILED
Edit-recreate journal entries
- POST /api/bookkeeping/journal-entries/[id]/edit-recreate
- Bookkeeping detail page wires it into the existing edit flow
Delete-last-voucher clears IB link
- Trigger + pg test ensure deleting the last voucher of a period nulls the
opening_balance_journal_entry_id link so a re-import lands cleanly
Salary employee overrides
- salary_run_employees gains per-run override fields + migration
- lib/salary/effective-values.ts centralises resolved values; all payslip,
payment, AGI, KU, and booking routes read through it
- SalaryOverridePanel on the employee detail page
Account classifier
- lib/bookkeeping/account-classifier.ts + tests; AddAccountDialog uses it
- backfill-import-accounts script updated
Misc
- toast: minor styling tweak
- AGI generate-declaration: respect effective values
- structured-errors: new LINK_INVOICE_VOUCHER namespace
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: add link_invoice_voucher operation type to pending_operations
* feat: refactor salary run calculations and update error handling for SIE imports
* fix: PR review feedback on voucher linking and SIE recovery
pg-real (blocking):
- tests/pg/delete-last-voucher-ib: drop posted_at = now() from the seed
UPDATE — journal_entries has no posted_at column.
- lib/invoices/__tests__/voucher-matching.pg: seed the posted voucher
before closing the fiscal period so enforce_period_lock doesn't block
the INSERT during setup.
voucher-matching error codes and rollback:
- Add LINK_VOUCHER_DB_ERROR (HTTP 500) and return it on real invoice
UPDATE / payment INSERT failures. Previously these returned
LINK_VOUCHER_VOUCHER_NOT_FOUND (404) which the pending-op dispatcher
auto-rejects on transient DB errors.
- Log rollback failures explicitly so an invoice left in a half-linked
state (advanced status, no payment row) surfaces for manual
reconciliation instead of disappearing silently.
resyncNextPeriodOpeningBalance ordering:
- Create the new IB first, relink the period FK, then storno the old IB.
Previously the storno ran first; if createJournalEntry failed the next
period was left with a reversed IB and nothing to replace it, and
executeSIEImport swallows the error as a non-fatal warning.
replace_period_opening_balance_link:
- Tighten role check to owner/admin (was owner/admin/member). Matches
delete_last_voucher and undo_sie_import.
Data minimisation:
- /api/invoices/[id]/voucher-candidates and the matching MCP tools now
project only the invoice and customer fields the matcher reads, instead
of returning the full customer row.
Schema bounds:
- SalaryEmployeeOverrideSchema caps each numeric override at 10 MSEK to
catch typos before they reach the ledger or AGI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tests): supply user_id when seeding voucher_sequences
voucher_sequences.user_id is NOT NULL (per the multi-tenant refactor in
20260330130000). The previous test seed only set company_id /
fiscal_period_id / voucher_series, which made the seed fail with a
constraint violation on the latest pg-real run. Pass the same userId
used elsewhere in the seed helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tests): scope delete-last-voucher RPC assertions inside the tx
withUserContext always ROLLBACKs, so any DELETE the RPC performs is
discarded when the callback returns. The previous test then queried
journal_entries via a fresh getPool() connection that only saw the
pre-RPC committed seed state — hence "expected '1' to be '0'".
Move every post-RPC assertion (entry count, period FK clear,
opening_balances_set flip, audit log entry, sie_imports clear) inside
the same withUserContext callback so they observe the uncommitted state
before ROLLBACK fires.
Also fix the sie_imports INSERT: the column is `filename`, not
`file_name`, and `sie_type` is NOT NULL.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(tests): assert against the IB-marker audit row directly
DELETE on journal_entries fires two audit_log writes: the generic
write_audit_log() trigger row ("Deleted journal_entries record") and the
delete_last_voucher RPC's explicit "(was period IB)" entry. Both land
at the same statement_timestamp(), so ORDER BY created_at DESC LIMIT 1
returned the trigger row non-deterministically in CI.
Switch to a presence check with a LIKE filter on the IB marker so the
test verifies what it actually cares about — that the RPC's IB-aware
audit row exists.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(db): set company_id on delete_last_voucher audit_log rows
20260528120000_delete_last_voucher_clears_ib_link.sql inserts directly
into audit_log without setting company_id. audit_log's SELECT policy
filters company_id IN user_company_ids(), so those rows landed with
company_id=NULL and were invisible to every reader — only the generic
write_audit_log() trigger row remained visible. That broke BFL audit-
trail intent: the "(was period IB)" provenance row was never readable.
Republish delete_last_voucher with p_company_id populated on both
audit_log INSERTs (draft path and posted path). Behavior is otherwise
unchanged; the pg-real test for the IB-clear flow now sees the
RPC-written marker row as expected.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>