Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import - Briox auth via account ID + application token (no app-level credentials); both tokens rotate on refresh and are persisted - New sie-fetcher pulls the general ledger as SIE through the provider API for Fortnox, Briox and Bjorn Lunden - Wizard stops on a failed SIE import and surfaces the real errors instead of proceeding to the misleading migrate-guard message - PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED; new PROVIDER_TOKEN_INVALID for rejected provider credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices Defer revenue/costs per invoice line to 29xx/17xx interim accounts with automatic monthly dissolution (nightly cron + catch-up at registration), schedule cancellation on credit, year-end auto-detect exclusion for already-scheduled invoices, invoice-inbox service-period extraction for prefill, and an MCP tool to list schedules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing Generate the annual report as iXBRL from a generated taxonomy registry (K2 element lists, taxonomy:generate/check scripts + CI guard), expose it via the fiscal-period API, and add the bolagsverket extension for digital submission to eget utrymme with webhook-driven status tracking (submissions table + pg tests, lifecycle events, year-end wizard UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): raise origin-guard test timeout to 20s The dynamic import pulls in the full server module; the parse alone flirts with the 5s default under full-suite parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new scripts and documentation for K2 AB taxonomy generation and validation - Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models. - Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle. - Included new documentation files: - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx` - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx` - `taxonomi-paket-2024-09-12_rev20250312.zip` * Add tests for bookkeeping accruals dissolution and supplier invoices - Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios. - Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions. - Introduce tests for the Arcim migration provider client, ensuring token handling and error classification. - Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings. - Add Zod schemas for Bolagsverket response payloads to ensure proper validation. - Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping. - Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly. - Introduce typed domain errors for accrual schedules to improve error handling in the service. - Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling. * fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments * fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated * feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id * feat(bokslut): enhance compliance and financial processing features with new submission details and security measures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8e8b63a200
commit
db8983ba9e
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Pin tests for gnubok_list_accrual_schedules.
|
||||
*
|
||||
* The scope mapping is load-bearing: tools missing from TOOL_SCOPE_MAP are
|
||||
* usable by ANY API key, so the reports:read pin guards against the tool
|
||||
* silently becoming scope-less again.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { tools } from '../server'
|
||||
import { TOOL_SCOPE_MAP } from '@/lib/auth/api-keys'
|
||||
|
||||
describe('gnubok_list_accrual_schedules — registration', () => {
|
||||
it('is registered and read-only', () => {
|
||||
const tool = tools.find((t) => t.name === 'gnubok_list_accrual_schedules')
|
||||
expect(tool).toBeDefined()
|
||||
expect(tool?.annotations.readOnlyHint).toBe(true)
|
||||
expect(tool?.annotations.destructiveHint).toBe(false)
|
||||
})
|
||||
|
||||
it('is mapped to reports:read scope', () => {
|
||||
expect(TOOL_SCOPE_MAP.gnubok_list_accrual_schedules).toBe('reports:read')
|
||||
})
|
||||
})
|
||||
@@ -100,6 +100,10 @@ describe('forbiddenOriginResponse', () => {
|
||||
})
|
||||
|
||||
describe('mcp-server apiRoutes origin enforcement', () => {
|
||||
// The dynamic import pulls in the full 9k-line server module; that parse
|
||||
// alone takes ~4s and flirts with the 5s default timeout under full-suite
|
||||
// parallel load. The test is import-bound, not logic-bound — give it
|
||||
// explicit headroom instead of letting machine load decide the outcome.
|
||||
it('rejects foreign-Origin requests on every /mcp method before dispatch', async () => {
|
||||
const { mcpServerExtension } = await import('../index')
|
||||
const routes = (mcpServerExtension.apiRoutes ?? []).filter((r) => r.path === '/mcp')
|
||||
@@ -114,5 +118,5 @@ describe('mcp-server apiRoutes origin enforcement', () => {
|
||||
)
|
||||
expect(res.status, `${route.method} /mcp`).toBe(403)
|
||||
}
|
||||
})
|
||||
}, 20_000)
|
||||
})
|
||||
|
||||
@@ -43,6 +43,9 @@ describe('tools/list payload size guard', () => {
|
||||
// 92 tools for Connectors Directory readiness; the ~10 longest descriptions
|
||||
// were trimmed toward 180–200 chars to partly offset. Headroom reserved for
|
||||
// the upcoming Skatteverket tools.
|
||||
// * Held at 36K when gnubok_list_accrual_schedules (add/bokslut) merged with
|
||||
// the categorize vat_amount override (#717): the combination crossed the
|
||||
// ceiling by ~75, offset by trimming the 8 longest descriptions to ~200 chars.
|
||||
// Long-term answer to growth is leaning harder on gnubok_search_tools — if this
|
||||
// fires again, prefer trimming descriptions or making a tool opt-in via search
|
||||
// before bumping further.
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
TOOL_SCOPE_MAP,
|
||||
} from '@/lib/auth/api-keys'
|
||||
import { createLogger } from '@/lib/logger'
|
||||
import { roundOre, sumOre } from '@/lib/money'
|
||||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { buildMappingResultFromCategory } from '@/lib/bookkeeping/category-mapping'
|
||||
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
|
||||
@@ -2060,7 +2061,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_get_agent_briefing',
|
||||
title: 'Get Agent Briefing',
|
||||
description: 'Bootstrap this company\'s specialized accountant context in one call: profile_summary, the atoms loaded for the company (metadata only — call gnubok_load_skill for bodies), and the top-30 active memories. Call once at session start.',
|
||||
description: 'Bootstrap this company\'s accountant context in one call: profile_summary, loaded atoms (metadata only — gnubok_load_skill for bodies), top-30 active memories. Call once at session start.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -4291,7 +4292,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_query_journal',
|
||||
title: 'Query Journal Lines',
|
||||
description: "Flexible journal-line query — replaces chained ledger calls for ad-hoc questions. Filters: accounts, date range, amount range, voucher series/number, source type, status, project, cost center, free-text. Returns lines with parent voucher metadata + totals.",
|
||||
description: "Flexible journal-line query for ad-hoc questions. Filters: account, date, amount, voucher series/number, source type, status, project, cost center, free-text. Returns lines with voucher metadata + totals.",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -4666,7 +4667,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_match_transaction_to_invoice',
|
||||
title: 'Match Transaction to Invoice',
|
||||
description: 'Match a bank transaction (income, amount>0) to a customer invoice. Confirm tx date/amount and invoice number/customer match before staging — preview mirrors what you pass. Supports partial payments and auto-storno of prior categorization.',
|
||||
description: 'Match a bank transaction (income, amount>0) to a customer invoice. Confirm tx date/amount and invoice number/customer before staging. Supports partial payments and auto-storno of prior categorization.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -5273,7 +5274,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_find_voucher_candidates_for_invoice',
|
||||
title: 'Find Voucher Candidates (Invoice)',
|
||||
description: "List posted verifikat that could be this invoice's payment (faktureringsmetoden: credit 1510; kontantmetoden: debit a bank/cash account 19xx). Call before gnubok_link_invoice_to_voucher to mark a faktura paid against an existing verifikation (no new bokföring).",
|
||||
description: "List posted verifikat that could be this invoice's payment (faktureringsmetoden: credit 1510; kontantmetoden: debit 19xx). Call before gnubok_link_invoice_to_voucher to mark the faktura paid (no new bokföring).",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -5339,7 +5340,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_link_invoice_to_voucher',
|
||||
title: 'Link Invoice to Voucher',
|
||||
description: 'Markera en faktura som betald genom att länka till en befintlig bokförd verifikation (faktureringsmetoden: krediterar 1510; kontantmetoden: debiterar likvidkonto 19xx). Ingen ny verifikation skapas. Hitta kandidater med gnubok_find_voucher_candidates_for_invoice först.',
|
||||
description: 'Markera en faktura som betald via länk till en befintlig verifikation (faktureringsmetoden: krediterar 1510; kontantmetoden: debiterar 19xx). Skapar ingen ny verifikation. Kör gnubok_find_voucher_candidates_for_invoice först.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -5426,7 +5427,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_find_voucher_candidates_for_supplier_invoice',
|
||||
title: 'Find Voucher Candidates (Supplier Invoice)',
|
||||
description: 'List posted verifikat that debit leverantörsskuld (2440) and could be the payment for this supplier invoice. Use before gnubok_link_supplier_invoice_to_voucher when marking a leverantörsfaktura paid against an existing verifikation (no new bokföring).',
|
||||
description: 'List posted verifikat that debit leverantörsskuld (2440) and could be this supplier invoice\'s payment. Call before gnubok_link_supplier_invoice_to_voucher to mark the leverantörsfaktura paid (no new bokföring).',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -5492,7 +5493,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_link_supplier_invoice_to_voucher',
|
||||
title: 'Link Supplier Invoice to Voucher',
|
||||
description: 'Markera en leverantörsfaktura som betald genom att länka till en befintlig verifikation som redan debiterar leverantörsskuld (2440). Ingen ny verifikation skapas. Hitta kandidater med gnubok_find_voucher_candidates_for_supplier_invoice först.',
|
||||
description: 'Markera en leverantörsfaktura som betald via länk till en befintlig verifikation som debiterar leverantörsskuld (2440). Skapar ingen ny verifikation. Kör gnubok_find_voucher_candidates_for_supplier_invoice först.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -5955,7 +5956,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_list_inbox_items',
|
||||
title: 'List Inbox Items',
|
||||
description: 'List document inbox items. Each has a `processed` flag covering all terminal links (transaction match, supplier invoice, or journal entry), so a booked receipt counts as done. unprocessed_only=true returns only docs still needing handling.',
|
||||
description: 'List document inbox items. `processed` covers all terminal links (transaction, supplier invoice, journal entry); booked receipts count as done. unprocessed_only=true returns docs still needing handling.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -6090,7 +6091,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_create_supplier_invoice_from_inbox',
|
||||
title: 'Create Supplier Invoice from Inbox',
|
||||
description: "Atomic: turn an OCR'd inbox item into a staged supplier invoice. Resolves supplier (matched or via org_number/name), assembles line items from extracted_data, applies VAT + FX, attaches the source document. Stages for human review; honors dry_run.",
|
||||
description: "Atomic: turn an OCR'd inbox item into a staged supplier invoice. Resolves supplier, builds lines from extracted_data, applies VAT + FX, attaches the document. Stages for human review; honors dry_run.",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -8279,7 +8280,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_undo_sie_import',
|
||||
title: 'Undo SIE Import',
|
||||
description: 'Stage undo of a completed SIE import: hard-deletes its entries (transactions + opening balance), detaches docs, resets voucher_sequences, marks the row \'undone\' so the file can be re-imported. Use after a botched import. Period must be open. HIGH risk.',
|
||||
description: 'Stage undo of a completed SIE import: hard-deletes its entries, detaches docs, resets voucher_sequences, marks the import \'undone\' for re-import. Use after a botched import. Period must be open. HIGH risk.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -8380,7 +8381,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_create_voucher',
|
||||
title: 'Create Manual Voucher (Verifikation)',
|
||||
description: 'Stage a manual verifikation with arbitrary balanced lines. Use for capitalization (1010), period-end accruals, FX adjustments, rättelseposter outside categorize_transaction. Pass inbox_item_id to book a kvitto direct — links inbox + attaches doc. HIGH risk.',
|
||||
description: 'Stage a manual verifikation with arbitrary balanced lines: capitalization (1010), accruals, FX adjustments, rättelser outside categorize_transaction. Pass inbox_item_id to book a kvitto direct (links + attaches doc). HIGH risk.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -8778,7 +8779,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_reverse_journal_entry',
|
||||
title: 'Reverse Journal Entry (Storno)',
|
||||
description: 'Stage a storno: inverts debits/credits; original stays visible per BFL 5 kap. Use only when the affärshändelse should never have been booked (duplicate, ghost, test). If booked wrong, use gnubok_correct_entry; for refunds, gnubok_credit_invoice. HIGH risk.',
|
||||
description: 'Stage a storno: inverts debits/credits; original stays visible per BFL 5 kap. Only when the affärshändelse should never have been booked (duplicate, ghost, test). Booked wrong → gnubok_correct_entry; refund → gnubok_credit_invoice. HIGH risk.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -8977,6 +8978,56 @@ export const tools: McpTool[] = [
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'gnubok_list_accrual_schedules',
|
||||
title: 'List Periodiseringar',
|
||||
description:
|
||||
'Löpande periodiseringar (17xx/29xx): monthly installments, dissolved and remaining amounts.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'completed', 'cancelled', 'all'],
|
||||
description: "Default 'active'.",
|
||||
},
|
||||
},
|
||||
},
|
||||
outputSchema: { type: 'object', additionalProperties: true },
|
||||
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
||||
async execute(args, companyId, _userId, supabase, _actor) {
|
||||
const status = (args.status as string) || 'active'
|
||||
let query = supabase
|
||||
.from('accrual_schedules')
|
||||
.select('*, installments:accrual_schedule_installments(*)')
|
||||
.eq('company_id', companyId)
|
||||
.order('created_at', { ascending: false })
|
||||
if (status !== 'all') query = query.eq('status', status)
|
||||
const { data, error } = await query
|
||||
if (error) throw new Error(error.message)
|
||||
|
||||
type InstallmentRow = { period_month: string; amount: number; status: string }
|
||||
const schedules = ((data ?? []) as Array<Record<string, unknown>>).map((schedule) => {
|
||||
const installments = ([...((schedule.installments as InstallmentRow[]) ?? [])]).sort(
|
||||
(a, b) => a.period_month.localeCompare(b.period_month),
|
||||
)
|
||||
const dissolved = sumOre(
|
||||
installments.filter((i) => i.status === 'posted').map((i) => Number(i.amount)),
|
||||
)
|
||||
const total = Number(schedule.total_amount)
|
||||
return {
|
||||
...schedule,
|
||||
installments,
|
||||
dissolved_amount: dissolved,
|
||||
remaining_amount:
|
||||
schedule.status === 'cancelled' ? 0 : roundOre(total - dissolved),
|
||||
}
|
||||
})
|
||||
return { schedules, count: schedules.length }
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'gnubok_propose_annual_depreciation',
|
||||
title: 'Propose Annual Depreciation (Avskrivning)',
|
||||
@@ -9213,7 +9264,7 @@ export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_approve_pending_operation',
|
||||
title: 'Approve Pending Operation',
|
||||
description: "Commit a staged pending_operation when the user has explicitly authorised the operation_id. risk_level=high requires confirmed=true — surface the BFL 5 kap 5§ irreversibility to the user first. The /pending web UI offers an equivalent commit path.",
|
||||
description: "Commit a staged pending_operation the user has explicitly authorised. risk_level=high requires confirmed=true — surface the BFL 5 kap 5§ irreversibility first. The /pending web UI offers an equivalent commit path.",
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
|
||||
Reference in New Issue
Block a user