Files
accounted/lib/pending-operations/risk-tiers.ts
T
MattssonandClaude Opus 4.8 f6ee0c2a82 Bug/customer invoice bug (#628)
* fix(supplier-invoices): self-assess reverse-charge VAT + link payments to vouchers

Reverse-charge supplier invoices now carry a per-item reverse_charge_rate (0.06/0.12/0.25). Under omvänd skattskyldighet the supplier charges 0% VAT, so the line vat_rate stays 0 and the buyer self-assesses fiktiv moms at the statutory rate. Centralizes rate resolution (resolveReverseChargeRate) and the ruta 20-24 basis-account guard (isReverseChargeBasisAccount) in vat-entries so the booking engine and review-dialog preview can no longer drift.

Adds the link_supplier_invoice_voucher pending operation: mark a leverantorsfaktura paid by linking an existing posted verifikat that debits 2440, with no new journal entry. Exposes find-candidates/link MCP tools and the bulk-reconcile helper, scoped under suppliers:read/write.

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

* fix(vat): report yearly VAT over the rakenskapsar, not the calendar year

Annual VAT (helarsmoms) is filed per beskattningsar/rakenskapsar (SFL 26 kap), which can be extended or shortened up to 18 months. The previous Jan-Dec calendar span silently dropped part of an extended first year. calculateVatDeclaration now accepts a fiscalPeriodId and resolves the period's actual bounds for yearly; monthly/quarterly stay calendar. The reports UI passes the selected fiscal period, defaults the periodicity from the company's moms_period setting, and carries the period into the ruta drill-down. full-archive export threads the period id through too.

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

* fix(migration): resolve supplier invoice status from payment amounts

The provider's lifecycle status and its payment status are computed independently upstream and can contradict each other (e.g. a Fortnox invoice marked booked but fully paid). Both the arcim entity-mapper and the Fortnox mapper now let payment state win: fully paid -> paid, partial -> partially_paid, otherwise the mapped lifecycle status, with credit notes forced terminal. Balance is compared numerically (never strict === 0) so float drift or a residual ore resolves cleanly, and an absent Balance is treated as unpaid.

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

* fix(enable-banking): only ingest booked transactions to stop re-import drift

Pending entries are skipped during sync: a pending row is unstable across syncs (a later 'synka nu' returns it still pending or finally booked, often with a different effective date). Because both the dedup external_id and the content-dedup key are date-derived, that drift minted a new id and re-imported a transaction that already existed - observed in production as the same amount+description landing twice with different dates. Gating the import set on a stable booking_date removes the drift at the source and leaves booked rows' ids byte-identical.

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

* chore(gitignore): ignore local SIE test fixtures

tests/fixtures/sie/ may contain real or scrubbed company data and must never be committed.

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

* fix(invoice): handle errors during registration journal entry creation and ensure invoice rollback
feat(tests): add test for reverse charge rate handling on supplier invoice line items
feat(fortnox): ensure paid status reflects zero balance for fully paid invoices
chore(migrations): add reverse_charge_rate to supplier_invoice_items and backfill link_supplier_invoice_voucher

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 13:25:48 +02:00

130 lines
6.6 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',
// Link an existing posted verifikat as payment for an invoice. Reversible by
// deleting the invoice_payments row and reverting invoice status; no journal
// entry is created or modified. Sits next to match_transaction_invoice
// semantically — both attach an existing booking to an invoice.
link_invoice_voucher: 'medium',
// Supplier-side mirror of link_invoice_voucher: link an existing posted
// verifikat (Dr 2440) as payment for a leverantörsfaktura. Reversible by
// deleting the supplier_invoice_payments row and reverting status; no journal
// entry is created or modified.
link_supplier_invoice_voucher: 'medium',
create_invoice: 'medium', // creates as draft; sending is a separate op
create_transaction: 'medium', // ingests an uncategorized row; reversible by delete
// Supplier master data carries payment-routing fields (IBAN, BIC, bankgiro,
// bank_account) that drive outgoing payment files and supplier invoice
// postings. A wrong account or org_number can enable supplier-fraud / BEC
// (silently rerouting payment), so always require explicit human approval
// rather than auto-commit.
create_supplier: 'medium',
// 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',
// Planenlig avskrivning: one journal entry per asset, each independently
// reversible (storno). Mid-stakes bokslut posting — staged and human-reviewed,
// but not the irreversible tier that year-end close / period lock occupy.
post_annual_depreciation: 'medium',
import_sie: 'high',
// Hard-deletes the import's journal entries + resets voucher sequences.
// Same destructive reach as replace_sie_import; never auto-commit.
undo_sie_import: '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',
reverse_entry: 'high',
// ── Payroll ────────────────────────────────────────────────────────
// Salary run creation materialises a draft + per-employee base lines. The
// run is reversible while still draft, so 'medium' aligns with other
// create-draft operations. AGI generation produces the Skatteverket
// underlag (XML, BFL 7-year retention) — statutory artifact, always
// staged.
create_salary_run: 'medium',
generate_agi: 'high',
// ── Multi-tx flows (PRs #603/#606/#608/#610) ───────────────────────
// Allocate 1 bank tx across N customer or supplier invoices into one
// combined verifikat. Reversible via storno + invoice_payments delete,
// so 'medium' (same tier as match_transaction_invoice — its single-
// invoice counterpart).
match_batch_allocate: 'medium',
// Bulk-book N bank txs into 1 verifikat. The create-new branch posts
// a verifikat with caller-supplied lines (template-expanded or manual),
// the same compliance-critical surface as create_voucher. 'high'.
bulk_book_transactions: 'high',
// Link a single bank tx to an already-posted verifikat (no new JE created).
// Reversible by clearing transactions.journal_entry_id and deleting any
// invoice_payments row — sits next to link_invoice_voucher semantically;
// both attach an existing booking to a different entity.
link_transaction_journal_entry: 'medium',
}
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'
}