feat(mcp): add create_voucher + correct_entry MCP tools (#448)

* feat(mcp): add create_voucher + correct_entry MCP tools

The MCP toolset had no way to post a journal entry outside the preset
workflows (categorize_transaction, create_invoice, …). That blocks
legitimate flows the engine already supports — K3 capitalization to BAS
1010, period-end accruals, FX adjustments, prepayments, and rättelseposter
for foreign reverse-charge VAT that landed on 2641 instead of 2614/2645.

create_voucher exposes the existing createJournalEntry() primitive:
arbitrary balanced lines, optional fiscal-period auto-resolution, staged
for human approval. correct_entry exposes correctEntry() (storno + new
corrected entry per BFL 5 kap 5§) so part of a posted verifikation can be
fixed without losing the legs that were right.

Both are HIGH risk in OPERATION_RISK_TIERS — the arbitrary account/amount/
period inputs make them compliance-critical despite being structurally
similar to uncategorize_transaction (medium). Approval flow unchanged; no
auto-commit, regardless of trust level.

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

* fix(mcp): address PR #448 review — voucher tools hardening

Greptile P1 + compliance bot findings, all in one pass.

commitCreateVoucher (commit.ts):
- Hardcode source_type to 'manual' instead of reading from params. A future
  direct-staging path or hand-inserted pending_operations row could
  otherwise inject 'bank_transaction'/'invoice_created'/etc. and corrupt
  the audit-trail origin.
- Re-validate balance defensively before reaching the engine, so a tampered
  params row surfaces a clean Swedish 400 instead of an opaque engine error.

gnubok_create_voucher (server.ts):
- Validate the explicit fiscal_period_id when supplied: confirm it exists,
  is open (is_closed = false), and that entry_date falls within its span.
  Without this, a closed/locked period was only caught at commit-time with
  a generic DB-trigger error.
- Throw at staging when any line targets an account that's missing from
  chart_of_accounts or marked inactive, rather than relying on the
  approver to spot the advisory flag.
- Remove source_type from the staged params blob entirely — the executor
  ignores it anyway, no point letting it travel through.
- Add a comment that the staging-time period-lock check is advisory and
  the executor is the authoritative guard, so future cleanup doesn't
  remove either as 'redundant'.

Descriptions:
- gnubok_correct_entry now explicitly notes that the storno + corrected
  entries land in the original period (defends against compliance bot's
  speculative "different period" concern recurring on future reviews).
- Both tools' tax_code field gets a note that the BAS account number
  drives momsdeklaration ruta mapping, not tax_code — guards against an
  LLM treating tax_code as the VAT-routing dial.

commitCorrectEntry (commit.ts):
- Add a comment pointing at storno-service.ts:99,102,195,198 to make the
  "uses original period and date" invariant explicit in this file.

Tests:
- +2 voucher-executors cases: source_type tampering is ignored, unbalanced
  params return 400.
- +10 new voucher-tools tests (MCP layer): unbalanced, closed explicit
  period, missing explicit period, entry_date outside period, unknown
  account, inactive account, happy path + correct_entry registration +
  unbalanced replacement.

720 tests pass in the impacted suites; full suite 2998/2998.

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:
Jakob Wennberg
2026-05-12 17:16:39 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent ec06a2d04d
commit eb77ad50b5
10 changed files with 1292 additions and 2 deletions
@@ -0,0 +1,59 @@
-- Phase 4: expose the bookkeeping engine's arbitrary-line primitives via MCP.
--
-- Until now the only way to produce a journal entry via MCP was through a
-- preset workflow (categorize_transaction, create_invoice, ...). That blocks
-- legitimate flows the engine already supports — capitalization to balance-
-- sheet accounts (BAS 1010 development costs under K3), period-end accruals,
-- FX adjustments outside the built-in revaluation, prepayments, manual
-- reclassifications, and rättelseposter for foreign reverse-charge VAT.
--
-- Two new op types:
-- * create_voucher — arbitrary balanced lines via createJournalEntry()
-- * correct_entry — storno + replacement via correctEntry() (BFL 5 kap 5§)
--
-- Both are routed as HIGH risk in lib/pending-operations/risk-tiers.ts because
-- they accept arbitrary account/amount/period inputs, unlike
-- uncategorize_transaction which mirrors an existing entry's shape.
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 (
-- Phase 0: original 7 op types
'categorize_transaction',
'create_customer',
'create_invoice',
'mark_invoice_paid',
'send_invoice',
'mark_invoice_sent',
'match_transaction_invoice',
-- Stream 1 Phase 1: bookkeeping period operations
'close_period',
'lock_period',
'unlock_period',
'set_opening_balances',
'run_year_end',
'run_currency_revaluation',
-- Stream 1 Phase 1: SIE import (export is read-only)
'import_sie',
-- Stream 1 Phase 1: voucher gap explanations
'explain_voucher_gap',
-- Stream 1 Phase 1: transaction reversal
'uncategorize_transaction',
-- Stream 1 Phase 1: supplier invoice lifecycle
'approve_supplier_invoice',
'credit_supplier_invoice',
-- Stream 1 Phase 1: invoice operations beyond simple create/send
'credit_invoice',
'convert_invoice',
-- Phase 3: manual transaction ingestion + document attachment
'create_transaction',
'attach_document_to_transaction',
-- Phase 4: arbitrary-line bookkeeping primitives (this migration)
'create_voucher',
'correct_entry'
));
NOTIFY pgrst, 'reload schema';