* 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>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { getRiskLevel, isHighRisk, OPERATION_RISK_TIERS } from '../risk-tiers'
|
|
|
|
describe('risk-tiers', () => {
|
|
it('classifies all currently-staged op types', () => {
|
|
// Op types that exist in the pending_operations CHECK constraint today.
|
|
const knownOps = [
|
|
'categorize_transaction',
|
|
'create_customer',
|
|
'create_invoice',
|
|
'mark_invoice_paid',
|
|
'send_invoice',
|
|
'mark_invoice_sent',
|
|
'match_transaction_invoice',
|
|
]
|
|
for (const op of knownOps) {
|
|
expect(OPERATION_RISK_TIERS).toHaveProperty(op)
|
|
}
|
|
})
|
|
|
|
it('treats sending invoices and marking paid as high risk', () => {
|
|
expect(getRiskLevel('send_invoice')).toBe('high')
|
|
expect(getRiskLevel('mark_invoice_paid')).toBe('high')
|
|
expect(getRiskLevel('mark_invoice_sent')).toBe('high')
|
|
})
|
|
|
|
it('treats period close, year-end, and SIE import as high risk', () => {
|
|
expect(getRiskLevel('close_period')).toBe('high')
|
|
expect(getRiskLevel('lock_period')).toBe('high')
|
|
expect(getRiskLevel('run_year_end')).toBe('high')
|
|
expect(getRiskLevel('import_sie')).toBe('high')
|
|
expect(getRiskLevel('set_opening_balances')).toBe('high')
|
|
})
|
|
|
|
it('treats customer creation as low risk (no booking impact)', () => {
|
|
expect(getRiskLevel('create_customer')).toBe('low')
|
|
})
|
|
|
|
it('treats reversible bookings as medium risk', () => {
|
|
expect(getRiskLevel('categorize_transaction')).toBe('medium')
|
|
expect(getRiskLevel('match_transaction_invoice')).toBe('medium')
|
|
expect(getRiskLevel('create_invoice')).toBe('medium')
|
|
expect(getRiskLevel('uncategorize_transaction')).toBe('medium')
|
|
})
|
|
|
|
it('defaults unknown op types to high (fail-safe)', () => {
|
|
expect(getRiskLevel('totally_unknown_op')).toBe('high')
|
|
expect(isHighRisk('totally_unknown_op')).toBe(true)
|
|
})
|
|
|
|
it('isHighRisk returns true only for high-risk ops', () => {
|
|
expect(isHighRisk('send_invoice')).toBe(true)
|
|
expect(isHighRisk('create_customer')).toBe(false)
|
|
expect(isHighRisk('categorize_transaction')).toBe(false)
|
|
})
|
|
|
|
// Phase 4: arbitrary-line bookkeeping primitives. These accept any account
|
|
// and any amount from the caller, so they're HIGH despite being structurally
|
|
// similar to uncategorize_transaction (which is medium).
|
|
it('treats arbitrary-line voucher primitives as high risk', () => {
|
|
expect(getRiskLevel('create_voucher')).toBe('high')
|
|
expect(getRiskLevel('correct_entry')).toBe('high')
|
|
expect(isHighRisk('create_voucher')).toBe(true)
|
|
expect(isHighRisk('correct_entry')).toBe(true)
|
|
})
|
|
})
|