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:
Mattsson
2026-06-12 16:35:30 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent 8e8b63a200
commit db8983ba9e
131 changed files with 33796 additions and 340 deletions
@@ -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.
+64 -13
View File
@@ -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,