Files
accounted/lib/pending-operations/risk-tiers.ts
T
Jakob WennbergandClaude Opus 4.7 eb77ad50b5 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>
2026-05-12 17:16:39 +02:00

81 lines
3.7 KiB
TypeScript

/**
* Risk tier classification for pending_operations.
*
* Used by lib/pending-operations/should-auto-commit.ts to decide whether a
* staged proposal from a trusted agent can be auto-committed without human
* review.
*
* Tiering principles:
* - **low**: no booking impact, no external side-effects, no audit risk.
* A reasonable bookkeeper would never want to manually approve these.
* - **medium**: reversible booking impact (drafts, transaction
* categorization that can be uncategorized). Auto-commit is allowed for
* trusted agents under a configurable monetary threshold.
* - **high**: irreversible or compliance-critical. Sends external messages,
* locks/closes periods, or affects tax filings. NEVER auto-committed,
* regardless of company opt-in or trust level.
*/
export type RiskLevel = 'low' | 'medium' | 'high'
export const OPERATION_RISK_TIERS: Record<string, RiskLevel> = {
// ── Low: pure data, no booking impact ─────────────────────────────
create_customer: 'low',
// ── Medium: reversible booking ─────────────────────────────────────
categorize_transaction: 'medium',
match_transaction_invoice: 'medium',
create_invoice: 'medium', // creates as draft; sending is a separate op
create_transaction: 'medium', // ingests an uncategorized row; reversible by delete
// Pinning a doc to a tx is reversible while pre-categorization, but the link
// becomes part of the verifikation underlag (BFL 5 kap 6 §) once categorize
// propagates it. A wrong attachment requires a rättelse, so require human
// approval rather than auto-commit.
attach_document_to_transaction: 'medium',
// ── High: irreversible, compliance-critical, or external side-effects
send_invoice: 'high', // emails the customer
mark_invoice_paid: 'high', // posts payment journal entry
mark_invoice_sent: 'high', // assigns invoice number, accrual JE
// ── Stream 1 Phase 1 ops (added when those tools land) ─────────────
close_period: 'high',
lock_period: 'high',
unlock_period: 'high',
set_opening_balances: 'high',
run_year_end: 'high',
run_currency_revaluation: 'high',
import_sie: 'high',
explain_voucher_gap: 'medium',
uncategorize_transaction: 'medium',
approve_supplier_invoice: 'high',
credit_supplier_invoice: 'high',
// Create supplier invoice from inbox: stages a `registered` supplier invoice
// + its line items + document attachment. Reversible until approved (the
// approval is a separate high-risk op) but creates a leverantörsskuld row,
// so we route it through human review at medium tier.
create_supplier_invoice_from_inbox: 'medium',
credit_invoice: 'high',
convert_invoice: 'medium',
// ── Phase 4: arbitrary-line bookkeeping primitives ─────────────────
// Both accept caller-supplied account/amount/period — unlike
// uncategorize_transaction (medium), which mirrors an existing entry.
// The arbitrary-line capability is what makes these compliance-critical.
create_voucher: 'high',
correct_entry: 'high',
}
export function getRiskLevel(operationType: string): RiskLevel {
// Default to 'high' for unknown ops — fail-safe: unknown means human review.
return OPERATION_RISK_TIERS[operationType] ?? 'high'
}
/**
* High-risk operations are NEVER auto-committed, regardless of company opt-in
* or actor trust. Encoded here (not in DB config) so it can't be bypassed.
*/
export function isHighRisk(operationType: string): boolean {
return getRiskLevel(operationType) === 'high'
}