feat(mcp): bulk-link documents to vouchers in one staged approval (#1411)

gnubok_link_documents_to_vouchers stages up to 300 document-to-verifikat
links as a single pending operation, addressed by voucher_series /
voucher_number / fiscal_year instead of journal_entry_id UUIDs, for bulk
receipt-migration jobs where N separate tools mean N separate approvals.

Staging resolves every row server-side and returns a per-row hit or miss,
so a systematic offset such as a wrong fiscal_year is visible before
anything is approved rather than after N approvals. Only resolved rows
enter the staged operation.

The WORM precondition and the document lookup are shared with the
single-document executor through precheckDocumentLink: a bulk call must
enforce exactly the invariants N single calls would, and a second copy of
a BFL 5 kap 6 § guard is a copy that keeps the old behaviour when the
first is hardened.

A batch that links nothing returns 409 instead of a committed no-op.
Partial skips stay committed, but an approval-gated operation on
räkenskapsinformation must not leave an audit record asserting a run that
changed nothing.

The tool is search-only: a one-off migration tool does not belong in the
default catalog every session pays for in context, and keeping it there
pushed the tools/list projection past the 58.5K token ceiling that
payload-size.bench.test.ts guards.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bjornbergenheim
2026-08-17 14:52:58 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 4300cb711b
commit 3841ab9f54
12 changed files with 682 additions and 32 deletions
@@ -0,0 +1,99 @@
-- Add 'link_documents_to_vouchers' to the pending_operations operation_type
-- CHECK constraint.
--
-- New bulk counterpart to 'link_document_to_voucher': addressed by
-- voucher_series/voucher_number/fiscal_year instead of journal_entry_id
-- UUIDs (the MCP tool resolves the UUID server-side), and stages up to 300
-- rows as ONE pending_operation instead of one per document. Built for bulk
-- receipt-migration jobs where staging N separate operations would mean N
-- separate approvals.
--
-- Risk tier: 'medium', same rationale as the single-document tool: linking a
-- doc to a posted verifikation becomes part of räkenskapsinformation (BFL 5
-- kap 6 §) once approved, so a human confirms the batch before it locks.
--
-- NOTE on the value list: this constraint is re-created wholesale (the
-- established pattern here; see 20260727110000's own note), so the list
-- below is every value from 20260813124507 PLUS the one new value. Dropping
-- any existing value here would silently revoke it.
--
-- NOT VALID + separate VALIDATE migration (paired file, same pattern as
-- 20260727110000 / 20260727110001): avoids a full-table scan under the
-- stronger lock this ALTER already holds.
--
-- pg-test: tests/pg/pending-operations-op-type-audit.pg.test.ts asserts every
-- op type staged in server.ts or tiered in risk-tiers.ts is accepted here.
ALTER TABLE public.pending_operations
DROP CONSTRAINT IF EXISTS pending_operations_operation_type_check;
ALTER TABLE public.pending_operations
ADD CONSTRAINT pending_operations_operation_type_check
CHECK (operation_type IN (
'categorize_transaction',
'create_customer',
'create_invoice',
'mark_invoice_paid',
'send_invoice',
'mark_invoice_sent',
'match_transaction_invoice',
'close_period',
'lock_period',
'unlock_period',
'set_opening_balances',
'run_year_end',
'post_kontantmetod_cutoff',
'run_currency_revaluation',
'import_sie',
'explain_voucher_gap',
'uncategorize_transaction',
'approve_supplier_invoice',
'credit_supplier_invoice',
'credit_invoice',
'convert_invoice',
'create_transaction',
'attach_document_to_transaction',
'create_voucher',
'correct_entry',
'reverse_entry',
'create_supplier',
'create_supplier_invoice_from_inbox',
'post_annual_depreciation',
'link_invoice_voucher',
'undo_sie_import',
'match_batch_allocate',
'bulk_book_transactions',
'create_salary_run',
'generate_agi',
'link_transaction_journal_entry',
'link_supplier_invoice_voucher',
'submit_vat_declaration',
'submit_agi',
'create_article',
'update_article',
'bulk_book_inbox_items',
'create_dimension_value',
'retag_line_dimensions',
'link_document_to_voucher',
'update_payslip_line',
'register_absence',
'create_employee',
'update_employee',
'set_employee_opening_balances',
'vacation_year_close',
'create_account',
'update_account',
'set_voucher_note',
'book_salary_run',
'delete_absence',
'update_company_settings',
'update_customer',
'update_invoice',
'create_recurring_schedule',
'update_recurring_schedule',
'log_mileage_trip',
'book_mileage_period',
'link_documents_to_vouchers' -- bulk: N documents to posted verifikat in 1 staged op, addressed by series/number/fiscal_year
)) NOT VALID;
NOTIFY pgrst, 'reload schema';
@@ -0,0 +1,6 @@
-- Validate the operation type CHECK re-added in 20260817130000.
-- This separate transaction avoids a full-table scan while the preceding
-- migration holds its stronger table lock.
ALTER TABLE public.pending_operations
VALIDATE CONSTRAINT pending_operations_operation_type_check;