Accounted rebrand + swarm-skill cleanup + bank-reconciliation fixes (#643)
* feat(reconciliation): close the bank-feed loop on voucher links and re-tag mis-typed opening balances
Two related fixes to bank reconciliation correctness:
1. Auto-reconcile on voucher link. Linking an invoice or supplier invoice to
an existing voucher previously advanced only the invoice — the bank
transaction that paid it kept sitting in the Transactions inbox with a null
journal_entry_id. linkInvoiceToVoucher / linkSupplierInvoiceToVoucher now
call autoReconcileTransactionForLinkedVoucher (lib/reconciliation), which
links the bank transaction to the same verifikat when exactly one unbooked
line matches it. Best-effort and post-commit: a failure here never fails the
link. The result surfaces reconciledTransactionId; the inbox row leaves the
list and the UI shows link_success_tx_reconciled.
2. Re-tag mis-typed opening balances. getReconciliationStatus and the GL-line
matching RPCs identify a cash account's ingående balans solely by
journal_entries.source_type='opening_balance'. Companies migrated from other
systems often booked the bank IB as an ordinary voucher (source_type
'import' or 'manual'), so it was never excluded and surfaced as a phantom
reconciliation difference equal to the opening balance. Adds:
- migration mark_entry_as_opening_balance: a GUC-gated carve-out in the
immutability trigger plus a SECURITY DEFINER RPC that validates the entry
(balance-sheet lines only, dated on a fiscal-period boundary), flips the
source_type, and writes an audit row — no blanket data sweep.
- POST /api/reconciliation/bank/mark-opening-balance + MarkOpeningBalanceSchema.
- BankReconciliationView action to trigger it from the IB diff.
The gnubok_create_voucher executor now accepts a typed is_opening_balance flag
and derives source_type='opening_balance' only after validating class 1/2 lines
on the period start, so new IBs land correctly typed.
Covered by lib/reconciliation auto-reconcile tests, voucher-executors tests,
and a mark-entry-as-opening-balance pg-real test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: rebrand gnubok → Accounted and prune swarm agent skills
Product rebrand and skills housekeeping. No runtime behaviour change.
Rebrand: replace user-visible "gnubok" with "Accounted" across docs, READMEs,
in-code comments, doc-site content, MCP skill/resource prose, and the
gnubok-mcp package description. The MCP resource URI scheme is moved gnubok://
→ Accounted:// consistently across resource registrations, the event-type
comment, and the resource/skill tests. Deliberately preserved as stable
identifiers (NOT rebranded): the gnubok-company-id cookie, gnubok_sk_ / gnubok_inv_
token prefixes, the gnubok-mcp npm bridge name, and the AGI <gem:Programnamn>
value (kept 'gnubok' per its source comment — it is the software identifier sent
to Skatteverket and must not churn across visual rebrands).
Skills: remove the 27 swarm-* agent SKILL.md atoms (no longer used; already
absent from the agent_atom_registry in prod), refresh the remaining skill docs,
add the .claude/rules/ path-scoped rule set, and regenerate the
seed_agent_atom_bodies migration + .skill-body-manifest.json via
`npm run skills:generate` so the DB-backed skill bodies match the trimmed set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
331ae11867
commit
c74b19df1b
@@ -3,7 +3,7 @@ import { registerBrandingService } from '@/lib/branding/service'
|
||||
|
||||
// Register the whitelabel branding values immediately when this extension is loaded.
|
||||
// Edit the values below to match your brand. Any field omitted falls back to the
|
||||
// gnubok default (and to whatever you've set via env vars). See WHITELABEL.md.
|
||||
// Accounted default (and to whatever you've set via env vars). See WHITELABEL.md.
|
||||
registerBrandingService({
|
||||
// appName: 'YourBrand',
|
||||
// appDescription: 'Bokföring & redovisning',
|
||||
|
||||
@@ -63,7 +63,7 @@ function translateOAuthError(error: string, description: string | null): string
|
||||
* Provider Migration extension
|
||||
*
|
||||
* Migrates bookkeeping data from external Swedish accounting systems
|
||||
* (Fortnox, Visma, Bokio, Björn Lundén, Briox) into gnubok by talking
|
||||
* (Fortnox, Visma, Bokio, Björn Lundén, Briox) into Accounted by talking
|
||||
* directly to each provider's API.
|
||||
*
|
||||
* Bookkeeping data (accounts, balances, vouchers) is imported via SIE
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**
|
||||
* Maps Arcim Sync canonical DTOs to gnubok internal types.
|
||||
* Maps Arcim Sync canonical DTOs to Accounted internal types.
|
||||
*
|
||||
* These mappers transform the normalized data from any Swedish accounting
|
||||
* provider into the exact shapes gnubok expects for database insertion.
|
||||
* provider into the exact shapes Accounted expects for database insertion.
|
||||
*/
|
||||
|
||||
import type { CustomerType, SupplierType, VatTreatment } from '@/types'
|
||||
@@ -275,11 +275,11 @@ export function mapSalesInvoice(
|
||||
const primaryTaxPercent = dto.lines.find(l => l.taxPercent != null)?.taxPercent
|
||||
const vatTreatment = inferVatTreatment(primaryTaxPercent, dto.currencyCode)
|
||||
|
||||
// Map Arcim status to gnubok status
|
||||
// Map Arcim status to Accounted status
|
||||
const statusMap: Record<string, string> = {
|
||||
draft: 'draft',
|
||||
sent: 'sent',
|
||||
booked: 'sent', // gnubok has no 'booked' status — treat as sent
|
||||
booked: 'sent', // Accounted has no 'booked' status — treat as sent
|
||||
paid: 'paid',
|
||||
overdue: 'overdue',
|
||||
cancelled: 'cancelled',
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function executeMigration(options: MigrationOptions): Promise<Migra
|
||||
}
|
||||
|
||||
// ── Step 2: Customers (bulk) ──────────────────────────────────
|
||||
// customerIdMap: provider customer id → gnubok customer id.
|
||||
// customerIdMap: provider customer id → Accounted customer id.
|
||||
// orgNumberToCustomerId / nameToCustomerId speed up invoice lookup
|
||||
// without extra queries later.
|
||||
const customerIdMap = new Map<string, string>()
|
||||
|
||||
@@ -236,7 +236,7 @@ export default function CloudBackupCard() {
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground leading-relaxed">
|
||||
Koppla ditt Google-konto för att ladda upp säkerhetsbackupen till din egen Drive.
|
||||
gnubok får bara tillgång till filer som appen själv skapar (scope{' '}
|
||||
Accounted får bara tillgång till filer som appen själv skapar (scope{' '}
|
||||
<span className="font-mono text-xs">drive.file</span>).
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dataPattern": "manual",
|
||||
"hasOwnData": true,
|
||||
"description": "Synka säkerhetsbackup till din egen molnlagring",
|
||||
"longDescription": "Koppla ditt Google Drive-konto och ladda upp en fullständig säkerhetsbackup med ett klick. Gnubok skapar en ZIP med SIE-filer, kvitton och behandlingshistorik och laddar upp till en egen mapp i din Drive. Perfekt för att uppfylla egna krav på redundans.",
|
||||
"longDescription": "Koppla ditt Google Drive-konto och ladda upp en fullständig säkerhetsbackup med ett klick. Accounted skapar en ZIP med SIE-filer, kvitton och behandlingshistorik och laddar upp till en egen mapp i din Drive. Perfekt för att uppfylla egna krav på redundans.",
|
||||
"subscriptionNotice": "Kräver ett Google-konto. Uppladdningar sker direkt till din Drive — ingen data lagras hos tredje part utöver Google."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export const ExtractionSchema = z.object({
|
||||
unitPrice: z.number().nullable(),
|
||||
lineTotal: z.number(),
|
||||
// Sane range for any real-world VAT rate. We allow non-Swedish rates
|
||||
// (UK 20, DE 19, NO 25, ...) since gnubok stores foreign invoices
|
||||
// (UK 20, DE 19, NO 25, ...) since Accounted stores foreign invoices
|
||||
// for reference; the strict Swedish allowlist applies later when the
|
||||
// user converts to a supplier invoice.
|
||||
vatRate: z.number().min(0).max(100).nullable(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# gnubok MCP server
|
||||
# Accounted MCP server
|
||||
|
||||
JSON-RPC 2.0 server exposing the gnubok bookkeeping engine to MCP clients (Claude Desktop, Claude Code, etc.). Endpoint: `/api/extensions/ext/mcp-server/mcp`. OAuth and stdio bridge live alongside the API surface — see `app/api/mcp-oauth/` and `packages/gnubok-mcp/`.
|
||||
JSON-RPC 2.0 server exposing the Accounted bookkeeping engine to MCP clients (Claude Desktop, Claude Code, etc.). Endpoint: `/api/extensions/ext/mcp-server/mcp`. OAuth and stdio bridge live alongside the API surface — see `app/api/mcp-oauth/` and `packages/gnubok-mcp/`.
|
||||
|
||||
## Tool authoring contract
|
||||
|
||||
@@ -17,7 +17,7 @@ Enforced by tests in `__tests__/` — these are not style preferences, they're g
|
||||
|
||||
Tool definitions (name, description, inputSchema, outputSchema, annotations) are declared as static object literals at module load — no timestamps, no UUIDs, no Date/Math.random in the definition layer. This makes the `tools/list` JSON payload byte-stable across requests, which lets agent-side prompt caches stay warm. **Do not introduce per-request non-determinism into the definitions block.** Anything time-bound or random belongs inside `execute()`.
|
||||
|
||||
For internal Anthropic API usage (today only `extensions/general/invoice-inbox/lib/extract-invoice-fields.ts`): annotate stable prefixes with `cache_control: { type: 'ephemeral' }` and log `usage.cache_read_input_tokens` for hit-ratio observability. The 1h TTL from the agent-native API plan (item 10) requires the direct Anthropic API; gnubok's Bedrock path defaults to a shorter TTL.
|
||||
For internal Anthropic API usage (today only `extensions/general/invoice-inbox/lib/extract-invoice-fields.ts`): annotate stable prefixes with `cache_control: { type: 'ephemeral' }` and log `usage.cache_read_input_tokens` for hit-ratio observability. The 1h TTL from the agent-native API plan (item 10) requires the direct Anthropic API; Accounted's Bedrock path defaults to a shorter TTL.
|
||||
|
||||
## Payload-size watchdog
|
||||
|
||||
@@ -27,7 +27,7 @@ For internal Anthropic API usage (today only `extensions/general/invoice-inbox/l
|
||||
|
||||
- `server.ts` — the tools array + JSON-RPC dispatcher
|
||||
- `tool-result.ts` — `withNext()`, `toToolError()` response helpers
|
||||
- `resources/` — read-only `gnubok://` URIs (active company, period, recent activity, capabilities, attention items, voucher gaps, chart of accounts, VAT treatments)
|
||||
- `resources/` — read-only `Accounted://` URIs (active company, period, recent activity, capabilities, attention items, voucher gaps, chart of accounts, VAT treatments)
|
||||
- `widgets/` — inline HTML widgets (receipt-matcher, vat-review)
|
||||
- `prompts/` — slash-command-style prompts
|
||||
- `skills/` — domain-knowledge skill bodies served via `gnubok_load_skill`
|
||||
|
||||
@@ -56,7 +56,7 @@ function enqueueEmpty(enqueue: (r: { data?: unknown; error?: unknown; count?: nu
|
||||
enqueue({ data: null })
|
||||
}
|
||||
|
||||
describe('gnubok://attention', () => {
|
||||
describe('Accounted://attention', () => {
|
||||
it('returns empty summary for a brand-new company', async () => {
|
||||
const { supabase, enqueue } = createQueuedMockSupabase()
|
||||
enqueueEmpty(enqueue)
|
||||
|
||||
@@ -226,8 +226,8 @@ describe('MCP Receipt Matcher', () => {
|
||||
|
||||
// Data resources (added in Stream 3 Phase 1) should also be listed.
|
||||
const uris = result.resources.map((r: { uri: string }) => r.uri)
|
||||
expect(uris).toContain('gnubok://company/current')
|
||||
expect(uris).toContain('gnubok://capabilities')
|
||||
expect(uris).toContain('Accounted://company/current')
|
||||
expect(uris).toContain('Accounted://capabilities')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -6,13 +6,13 @@ describe('mcp resource registry', () => {
|
||||
expect(dataResources).toHaveLength(7)
|
||||
const uris = dataResources.map((r) => r.uri).sort()
|
||||
expect(uris).toEqual([
|
||||
'gnubok://attention',
|
||||
'gnubok://capabilities',
|
||||
'gnubok://chart-of-accounts',
|
||||
'gnubok://company/current',
|
||||
'gnubok://period/active',
|
||||
'gnubok://recent-activity',
|
||||
'gnubok://settings/vat-treatments',
|
||||
'Accounted://attention',
|
||||
'Accounted://capabilities',
|
||||
'Accounted://chart-of-accounts',
|
||||
'Accounted://company/current',
|
||||
'Accounted://period/active',
|
||||
'Accounted://recent-activity',
|
||||
'Accounted://settings/vat-treatments',
|
||||
])
|
||||
|
||||
for (const r of dataResources) {
|
||||
@@ -24,28 +24,28 @@ describe('mcp resource registry', () => {
|
||||
})
|
||||
|
||||
it('matches base URI ignoring query string', () => {
|
||||
const r = findResource('gnubok://recent-activity?limit=5')
|
||||
expect(r?.uri).toBe('gnubok://recent-activity')
|
||||
const r = findResource('Accounted://recent-activity?limit=5')
|
||||
expect(r?.uri).toBe('Accounted://recent-activity')
|
||||
})
|
||||
|
||||
it('returns null for unknown URI', () => {
|
||||
expect(findResource('gnubok://does-not-exist')).toBeNull()
|
||||
expect(findResource('Accounted://does-not-exist')).toBeNull()
|
||||
})
|
||||
|
||||
it('parses query params from URI', () => {
|
||||
const q = parseResourceQuery('gnubok://recent-activity?limit=5&offset=10')
|
||||
const q = parseResourceQuery('Accounted://recent-activity?limit=5&offset=10')
|
||||
expect(q?.get('limit')).toBe('5')
|
||||
expect(q?.get('offset')).toBe('10')
|
||||
})
|
||||
|
||||
it('returns undefined when no query', () => {
|
||||
expect(parseResourceQuery('gnubok://capabilities')).toBeUndefined()
|
||||
expect(parseResourceQuery('Accounted://capabilities')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('vat-treatments resource', () => {
|
||||
it('returns matrix for all customer types without DB access', async () => {
|
||||
const r = findResource('gnubok://settings/vat-treatments')!
|
||||
const r = findResource('Accounted://settings/vat-treatments')!
|
||||
const result = (await r.read({
|
||||
// Pure-function resource: no DB calls
|
||||
supabase: undefined as never,
|
||||
|
||||
@@ -144,10 +144,10 @@ describe('Skills registry', () => {
|
||||
expect(await findSkill('does-not-exist', supabase as never)).toBeNull()
|
||||
})
|
||||
|
||||
it('skillUri uses the gnubok://skill/ prefix; atom slugs are URL-encoded', () => {
|
||||
expect(skillUri('foo')).toBe('gnubok://skill/foo')
|
||||
expect(skillUri('vertical/konsult-it')).toBe('gnubok://skill/vertical%2Fkonsult-it')
|
||||
expect(SKILL_URI_PREFIX).toBe('gnubok://skill/')
|
||||
it('skillUri uses the Accounted://skill/ prefix; atom slugs are URL-encoded', () => {
|
||||
expect(skillUri('foo')).toBe('Accounted://skill/foo')
|
||||
expect(skillUri('vertical/konsult-it')).toBe('Accounted://skill/vertical%2Fkonsult-it')
|
||||
expect(SKILL_URI_PREFIX).toBe('Accounted://skill/')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -440,7 +440,7 @@ describe('Skills via MCP protocol', () => {
|
||||
__resetAtomCache()
|
||||
})
|
||||
|
||||
it('resources/list includes one entry per skill at gnubok://skill/<slug>', async () => {
|
||||
it('resources/list includes one entry per skill at Accounted://skill/<slug>', async () => {
|
||||
const res = await handleMcpRequest(mcpRequest('resources/list'))
|
||||
const result = await parseResult(res)
|
||||
const uris = result.resources.map((r: { uri: string }) => r.uri)
|
||||
@@ -463,18 +463,18 @@ describe('Skills via MCP protocol', () => {
|
||||
|
||||
it('resources/read returns the Markdown body for a skill URI', async () => {
|
||||
const res = await handleMcpRequest(
|
||||
mcpRequest('resources/read', { uri: 'gnubok://skill/quarterly-vat-review' })
|
||||
mcpRequest('resources/read', { uri: 'Accounted://skill/quarterly-vat-review' })
|
||||
)
|
||||
const result = await parseResult(res)
|
||||
expect(result.contents).toHaveLength(1)
|
||||
expect(result.contents[0].uri).toBe('gnubok://skill/quarterly-vat-review')
|
||||
expect(result.contents[0].uri).toBe('Accounted://skill/quarterly-vat-review')
|
||||
expect(result.contents[0].mimeType).toBe('text/markdown')
|
||||
expect(result.contents[0].text).toContain('# Quarterly VAT Review')
|
||||
})
|
||||
|
||||
it('resources/read returns Resource not found for unknown skill slug', async () => {
|
||||
const res = await handleMcpRequest(
|
||||
mcpRequest('resources/read', { uri: 'gnubok://skill/does-not-exist' })
|
||||
mcpRequest('resources/read', { uri: 'Accounted://skill/does-not-exist' })
|
||||
)
|
||||
const json = await res.json()
|
||||
expect(json.error).toBeDefined()
|
||||
|
||||
@@ -316,11 +316,11 @@ describe('mcp.resource_read telemetry', () => {
|
||||
const eventPromise = captureNextResourceReadEvent()
|
||||
|
||||
await handleMcpRequest(
|
||||
mcpRequest('resources/read', { uri: 'gnubok://skill/quarterly-vat-review' })
|
||||
mcpRequest('resources/read', { uri: 'Accounted://skill/quarterly-vat-review' })
|
||||
)
|
||||
|
||||
const event = await eventPromise
|
||||
expect(event.uri).toBe('gnubok://skill/quarterly-vat-review')
|
||||
expect(event.uri).toBe('Accounted://skill/quarterly-vat-review')
|
||||
expect(event.kind).toBe('skill')
|
||||
expect(event.success).toBe(true)
|
||||
expect(event.errorCode).toBeNull()
|
||||
@@ -330,11 +330,11 @@ describe('mcp.resource_read telemetry', () => {
|
||||
const eventPromise = captureNextResourceReadEvent()
|
||||
|
||||
await handleMcpRequest(
|
||||
mcpRequest('resources/read', { uri: 'gnubok://nonexistent/whatever' })
|
||||
mcpRequest('resources/read', { uri: 'Accounted://nonexistent/whatever' })
|
||||
)
|
||||
|
||||
const event = await eventPromise
|
||||
expect(event.uri).toBe('gnubok://nonexistent/whatever')
|
||||
expect(event.uri).toBe('Accounted://nonexistent/whatever')
|
||||
expect(event.kind).toBe('unknown')
|
||||
expect(event.success).toBe(false)
|
||||
expect(event.errorCode).toBe('RESOURCE_NOT_FOUND')
|
||||
@@ -346,7 +346,7 @@ describe('mcp.resource_read telemetry', () => {
|
||||
// The dispatcher only matches kind=skill when findSkill returns a hit;
|
||||
// unknown slugs fall through and end up as kind=unknown.
|
||||
await handleMcpRequest(
|
||||
mcpRequest('resources/read', { uri: 'gnubok://skill/does-not-exist' })
|
||||
mcpRequest('resources/read', { uri: 'Accounted://skill/does-not-exist' })
|
||||
)
|
||||
|
||||
const event = await eventPromise
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { McpPrompt } from './types'
|
||||
|
||||
/**
|
||||
* Single-action prompts. Each one is a Swedish slash-shortcut that directs
|
||||
* the model to call exactly one gnubok tool and report a short answer.
|
||||
* the model to call exactly one Accounted tool and report a short answer.
|
||||
*/
|
||||
export const prompts: McpPrompt[] = [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MCP prompt — a server-defined chat template the user picks from a slash menu
|
||||
* in their MCP client. Selecting a prompt sends the message text to the model,
|
||||
* which then calls the relevant gnubok tools to satisfy the request.
|
||||
* which then calls the relevant Accounted tools to satisfy the request.
|
||||
*
|
||||
* These prompts are intentionally argument-less and single-action: each one
|
||||
* maps to one read tool, returning one short answer in Swedish.
|
||||
|
||||
@@ -25,7 +25,7 @@ function daysBetween(fromIso: string, toIso: string): number {
|
||||
}
|
||||
|
||||
export const attentionResource: McpResource = {
|
||||
uri: 'gnubok://attention',
|
||||
uri: 'Accounted://attention',
|
||||
name: 'What Needs Attention',
|
||||
description:
|
||||
'One-shot summary of outstanding work for the active company: unbooked transactions, overdue invoices, pending approvals, voucher gaps, upcoming deadlines, bank consent expiry, and period-lock alerts. Each category includes a count, up to 5 sample rows, and a suggested next tool call. Use this at session start to orient before chaining read tools.',
|
||||
@@ -180,7 +180,7 @@ export const attentionResource: McpResource = {
|
||||
samples: overdueAll.slice(0, SAMPLE_LIMIT),
|
||||
next: {
|
||||
description: 'Granska förfallna fakturor och skicka påminnelser.',
|
||||
resource: 'gnubok://recent-activity?limit=20',
|
||||
resource: 'Accounted://recent-activity?limit=20',
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -360,7 +360,7 @@ export const attentionResource: McpResource = {
|
||||
],
|
||||
next: {
|
||||
description: 'Slutför obokfört arbete innan lock_date.',
|
||||
resource: 'gnubok://period/active',
|
||||
resource: 'Accounted://period/active',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Capability {
|
||||
}
|
||||
|
||||
export const capabilitiesResource: McpResource = {
|
||||
uri: 'gnubok://capabilities',
|
||||
uri: 'Accounted://capabilities',
|
||||
name: 'Capabilities',
|
||||
description: 'What the current API key can actually do given (a) its granted scopes and (b) the current company state. Surfaces blockers like locked periods so the agent knows ahead of time why an action would fail.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -11,7 +11,7 @@ interface AccountSummary {
|
||||
}
|
||||
|
||||
export const chartOfAccountsResource: McpResource = {
|
||||
uri: 'gnubok://chart-of-accounts',
|
||||
uri: 'Accounted://chart-of-accounts',
|
||||
name: 'Chart of Accounts (BAS)',
|
||||
description: 'The active BAS chart of accounts for the current company, grouped by account class (1=assets, 2=liabilities/equity, 3=revenue, 4=COGS, 5-7=expenses, 8=financial). Use to look up account numbers before booking entries.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { McpResource } from './types'
|
||||
* Read-only and per-request; no caching. Target payload <8 KB.
|
||||
*/
|
||||
export const companyCurrentResource: McpResource = {
|
||||
uri: 'gnubok://company/current',
|
||||
uri: 'Accounted://company/current',
|
||||
name: 'Active Company',
|
||||
description: 'Per-company working memory: identity, active fiscal period, lock dates, entity counts, voucher series state, recent activity, approaching Swedish filing deadlines. Read this first when starting work on a company.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { McpResource } from './types'
|
||||
|
||||
export const periodActiveResource: McpResource = {
|
||||
uri: 'gnubok://period/active',
|
||||
uri: 'Accounted://period/active',
|
||||
name: 'Active Fiscal Period',
|
||||
description: 'The fiscal period that the current date falls within: lock state, opening-balance status, retention deadline. Use to check whether new entries can be posted.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { McpResource } from './types'
|
||||
|
||||
export const recentActivityResource: McpResource = {
|
||||
uri: 'gnubok://recent-activity',
|
||||
uri: 'Accounted://recent-activity',
|
||||
name: 'Recent Activity',
|
||||
description: 'Most recent journal entries, invoices, and bank transactions for the current company. Optional ?limit=N (default 20, max 100). Use to orient on the latest state without burning tool calls.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { CustomerType } from '@/types'
|
||||
const CUSTOMER_TYPES: CustomerType[] = ['individual', 'swedish_business', 'eu_business', 'non_eu_business']
|
||||
|
||||
export const vatTreatmentsResource: McpResource = {
|
||||
uri: 'gnubok://settings/vat-treatments',
|
||||
uri: 'Accounted://settings/vat-treatments',
|
||||
name: 'VAT Treatments',
|
||||
description: 'Available VAT treatments and rates per customer type, and the resulting moms ruta on the VAT declaration. Use before creating invoices to pick the right VAT rate.',
|
||||
mimeType: 'application/json',
|
||||
|
||||
@@ -837,7 +837,7 @@ const VAT_REPORT_OUTPUT_SCHEMA = {
|
||||
* buyer's VAT number is invalid).
|
||||
*
|
||||
* Companies using non-standard charts must either book to one of these
|
||||
* or extend the list — gnubok's BAS chart only ships 3001/3002/3003/3004
|
||||
* or extend the list — Accounted's BAS chart only ships 3001/3002/3003/3004
|
||||
* by default, but 30xx alternates are common in custom charts. */
|
||||
const RUTA_05_ACCOUNTS = [
|
||||
// Domestic sales by VAT rate (canonical BAS)
|
||||
@@ -1399,7 +1399,7 @@ function previousPeriodArgs(
|
||||
export const tools: McpTool[] = [
|
||||
{
|
||||
name: 'gnubok_search_tools',
|
||||
description: 'Search gnubok MCP tools by keyword and return their schemas at a chosen detail level. Call this first when looking for a capability — avoids loading every tool schema upfront.',
|
||||
description: 'Search Accounted MCP tools by keyword and return their schemas at a chosen detail level. Call this first when looking for a capability — avoids loading every tool schema upfront.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -7713,6 +7713,7 @@ export const tools: McpTool[] = [
|
||||
fiscal_period_id: { type: 'string', description: 'UUID of fiscal period. If omitted, resolved from entry_date.' },
|
||||
voucher_series: { type: 'string', description: 'Single letter A–Z. Defaults to A.' },
|
||||
notes: { type: 'string', description: 'Internal notes (max 2000 chars) — visible on the verifikation but not on reports.' },
|
||||
is_opening_balance: { type: 'boolean', description: 'Set true ONLY for a migrated ingående balans (IB). Marks the entry source_type=opening_balance so bank reconciliation excludes it from period movement. Requires every line to be a balance-sheet account (class 1/2) and entry_date = fiscal period start, else rejected. Defaults false.' },
|
||||
inbox_item_id: { type: 'string', description: 'Optional inbox item UUID to book directly. On confirm, the inbox item is linked to the new verifikat and its OCR document is attached to the journal entry. Fails if the inbox item is already booked (as voucher) or converted (to supplier invoice).' },
|
||||
lines: {
|
||||
type: 'array',
|
||||
@@ -7889,16 +7890,23 @@ export const tools: McpTool[] = [
|
||||
}
|
||||
|
||||
// NOTE: source_type is intentionally NOT included in the staged params.
|
||||
// The executor hardcodes 'manual' so a tampered or future direct-staged
|
||||
// pending_operations row can't misrepresent the entry's origin.
|
||||
// The executor derives it: 'opening_balance' when the typed
|
||||
// is_opening_balance flag is set AND the executor re-validates the entry
|
||||
// genuinely looks like an IB (all class-1/2 lines, dated on the period
|
||||
// start); otherwise 'manual'. We never accept a raw source_type string —
|
||||
// a tampered or future direct-staged pending_operations row can't
|
||||
// misrepresent the entry's origin, only assert "this is an IB" via a
|
||||
// boolean the executor independently verifies.
|
||||
const isOpeningBalance = args.is_opening_balance === true
|
||||
return stagePendingOperation(supabase, companyId, userId, 'create_voucher',
|
||||
`Manuell verifikation: ${description}`,
|
||||
`${isOpeningBalance ? 'Ingående balans' : 'Manuell verifikation'}: ${description}`,
|
||||
{
|
||||
entry_date: entryDate,
|
||||
description,
|
||||
fiscal_period_id: fiscalPeriodId,
|
||||
voucher_series: (args.voucher_series as string) || undefined,
|
||||
notes: (args.notes as string) || undefined,
|
||||
is_opening_balance: isOpeningBalance,
|
||||
inbox_item_id: inboxItemId,
|
||||
document_id: inboxDocumentId,
|
||||
lines,
|
||||
@@ -8190,7 +8198,7 @@ export const tools: McpTool[] = [
|
||||
// If the original touches output/input VAT accounts (2610–2670), a storno
|
||||
// is correct ONLY if the moms period covering entry_date has not yet been
|
||||
// filed with Skatteverket. For filed periods the legal path is an
|
||||
// omprövning (rättelse-omprövning per ML 2023:200, SFL 22 kap). gnubok
|
||||
// omprövning (rättelse-omprövning per ML 2023:200, SFL 22 kap). Accounted
|
||||
// doesn't track per-VAT-period filing status today, so we surface a
|
||||
// soft warning rather than block — the human approver decides.
|
||||
const vatAccounts = originalLines
|
||||
@@ -8727,7 +8735,7 @@ export const tools: McpTool[] = [
|
||||
// ── Bring-your-own-extraction for inbox items ────────────────
|
||||
{
|
||||
name: 'gnubok_set_inbox_extracted_data',
|
||||
description: 'Replace extracted_data on an inbox item with agent-supplied fields (bring-your-own-extraction). Use when your own pipeline parses the document better than gnubok\'s OCR. Follow with gnubok_create_supplier_invoice_from_inbox to stage.',
|
||||
description: 'Replace extracted_data on an inbox item with agent-supplied fields (bring-your-own-extraction). Use when your own pipeline parses the document better than Accounted\'s OCR. Follow with gnubok_create_supplier_invoice_from_inbox to stage.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
@@ -9167,7 +9175,7 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
|
||||
},
|
||||
serverInfo: SERVER_INFO,
|
||||
instructions: [
|
||||
'gnubok — Swedish double-entry bookkeeping via conversation.',
|
||||
'Accounted — Swedish double-entry bookkeeping via conversation.',
|
||||
'',
|
||||
'Discovery:',
|
||||
'• tools/list returns the full schema for every tool. To narrow a large catalog, call gnubok_search_tools(query="…") — it ranks tools by relevance; pass detail="name"|"summary"|"full" to control payload size.',
|
||||
@@ -9423,7 +9431,7 @@ export async function handleMcpRequest(request: Request): Promise<Response> {
|
||||
)
|
||||
}
|
||||
|
||||
// Skills exposed at gnubok://skill/<slug> — Markdown bodies, forward-compatible
|
||||
// Skills exposed at Accounted://skill/<slug> — Markdown bodies, forward-compatible
|
||||
// with a future native MCP skills/list primitive. Atom slugs (slash-bearing
|
||||
// registry ids) are URL-encoded in the URI; skillSlugFromUri decodes.
|
||||
if (uri.startsWith(SKILL_URI_PREFIX)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Bank Reconciliation — gnubok
|
||||
const body = `# Bank Reconciliation — Accounted
|
||||
|
||||
Reconcile the company's bank statements against the bookkeeping ledger so that the cash position in the books matches the bank. Run at month-end and before VAT close.
|
||||
|
||||
@@ -58,7 +58,7 @@ and matching balances. If the balances still disagree:
|
||||
|
||||
### Step 5 — Document the reconciliation
|
||||
|
||||
For each fiscal period, gnubok stores the reconciliation state automatically.
|
||||
For each fiscal period, Accounted stores the reconciliation state automatically.
|
||||
For a printed audit trail (BFL 8 kap), generate the supplier ledger and AR
|
||||
ledger after reconciliation:
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Customer Onboarding — gnubok
|
||||
const body = `# Customer Onboarding — Accounted
|
||||
|
||||
Adding a customer correctly the first time avoids a long tail of wrong-VAT
|
||||
invoices. This skill covers the decision tree for picking \`customer_type\`,
|
||||
@@ -30,7 +30,7 @@ later — much cheaper to ask the customer once at onboarding.
|
||||
|
||||
**Special cases:**
|
||||
|
||||
- **B2G (Swedish municipality/agency)**: still \`swedish_business\`, but the invoice must be Peppol-formatted (legal requirement since 2019-04-01). gnubok handles Peppol when the customer record has a Peppol endpoint configured.
|
||||
- **B2G (Swedish municipality/agency)**: still \`swedish_business\`, but the invoice must be Peppol-formatted (legal requirement since 2019-04-01). Accounted handles Peppol when the customer record has a Peppol endpoint configured.
|
||||
- **Consumer customer in another EU country (B2C distance sale)**: \`eu_business\` does NOT apply. Charge Swedish VAT (25/12/6 %) below the OSS threshold; above the threshold the company must register for OSS. This is rare for sole traders — flag the user if turnover suggests they're approaching the threshold.
|
||||
|
||||
## Workflow
|
||||
@@ -55,12 +55,12 @@ the VAT number is invalid (or unverifiable), reverse charge does NOT apply —
|
||||
you must charge Swedish VAT instead. The wrong call here means you under-collect
|
||||
VAT and Skatteverket charges interest + penalty later.
|
||||
|
||||
gnubok's \`gnubok_create_customer\` runs VIES automatically when \`vat_number\`
|
||||
Accounted's \`gnubok_create_customer\` runs VIES automatically when \`vat_number\`
|
||||
is provided. The result is stored in \`vat_number_validated\` on the customer.
|
||||
Subsequent invoices read this field via \`getAvailableVatRates\` to determine
|
||||
which VAT rates are legal.
|
||||
|
||||
If VIES is temporarily down (intermittent), gnubok marks the customer as
|
||||
If VIES is temporarily down (intermittent), Accounted marks the customer as
|
||||
\`vat_number_validated = false\` and you'll see standard 25 % rates instead
|
||||
of reverse charge. Re-validate by updating the customer once VIES is back.
|
||||
|
||||
@@ -83,7 +83,7 @@ appears in \`gnubok_list_customers\` and can be invoiced via \`gnubok_create_inv
|
||||
\`gnubok_list_customers\` — confirm the customer appears, customer_type is right.
|
||||
Read \`vat_number_validated\` (for EU); if false, double-check the VAT number.
|
||||
|
||||
When you eventually call \`gnubok_create_invoice\`, gnubok uses the
|
||||
When you eventually call \`gnubok_create_invoice\`, Accounted uses the
|
||||
\`getAvailableVatRates\` helper to constrain the dropdown of legal rates per
|
||||
customer type. If you see "VAT rate X% is not allowed for customer type Y",
|
||||
the customer_type is wrong — re-onboard correctly via the agent flow, do NOT
|
||||
@@ -91,7 +91,7 @@ hand-pick a different rate.
|
||||
|
||||
## Common errors
|
||||
|
||||
- *"VAT rate 25% is not allowed for customer type eu_business"*: the customer's VAT number is validated → reverse charge required. Use 0 % on line items, and gnubok will add the "Omvänd betalningsskyldighet" notation automatically.
|
||||
- *"VAT rate 25% is not allowed for customer type eu_business"*: the customer's VAT number is validated → reverse charge required. Use 0 % on line items, and Accounted will add the "Omvänd betalningsskyldighet" notation automatically.
|
||||
- *"VAT rate 0% is not allowed for customer type swedish_business"*: domestic customers always get 25/12/6 %. Pick the right rate.
|
||||
- *"Customer not found"* on \`gnubok_create_invoice\`: the customer wasn't created (or its pending_operation wasn't approved). Run \`gnubok_list_customers\` to confirm.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Invoicing Rules — gnubok
|
||||
const body = `# Invoicing Rules — Accounted
|
||||
|
||||
How to send a Swedish-compliant invoice from start to finish.
|
||||
|
||||
@@ -67,7 +67,7 @@ If the customer doesn't exist:
|
||||
|
||||
When in doubt for an invoice issued on or after 1 April 2026, classify the supply per the above rather than defaulting to one rate for "restaurang/hotell".
|
||||
|
||||
Use \`getAvailableVatRates(customerType, vatNumberValidated)\` semantics — gnubok handles this. Per-line override possible via \`vat_rate\` on each item.
|
||||
Use \`getAvailableVatRates(customerType, vatNumberValidated)\` semantics — Accounted handles this. Per-line override possible via \`vat_rate\` on each item.
|
||||
|
||||
### Step 3 — Create the invoice
|
||||
|
||||
@@ -108,16 +108,16 @@ For consumer-targeted services (RUT: städning, RUT) or construction (ROT):
|
||||
- **2025 May–Dec**: enhanced 50 % rate (still 50 000 SEK ceiling). Reverts to 30 % from 2026 unless extended.
|
||||
- When in doubt for an invoice issued in May 2025 or later, default to the current Skatteverket-published rate rather than the 30 % baseline.
|
||||
|
||||
This data goes on the invoice; gnubok's invoice template renders it automatically when set on the customer.
|
||||
This data goes on the invoice; Accounted's invoice template renders it automatically when set on the customer.
|
||||
|
||||
## Peppol / e-invoicing (B2G)
|
||||
|
||||
Swedish authorities require e-invoices via Peppol BIS Billing 3.0 (Lag 2018:1277). For private B2B, the buyer's preference governs but Peppol is preferred. gnubok renders an EN 16931-compliant XML on demand.
|
||||
Swedish authorities require e-invoices via Peppol BIS Billing 3.0 (Lag 2018:1277). For private B2B, the buyer's preference governs but Peppol is preferred. Accounted renders an EN 16931-compliant XML on demand.
|
||||
|
||||
## Critical rules
|
||||
|
||||
- **Invoice numbers are sequential and gap-free.** Allocated atomically at approval. If you change your mind, use \`gnubok_credit_invoice\`, never delete or skip a number — Skatteverket will audit.
|
||||
- **F-skatt notation is mandatory** for B2B services. gnubok adds it automatically when company settings have F-skatt = true.
|
||||
- **F-skatt notation is mandatory** for B2B services. Accounted adds it automatically when company settings have F-skatt = true.
|
||||
- **Currency:** SEK is default but the invoice itself can be issued in any of SEK/EUR/USD/GBP/NOK/DKK. The bookkeeping JE is always in SEK at issue-date Riksbanken rate.
|
||||
|
||||
## Common errors
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Kreditfaktura (Credit Note) Process — gnubok
|
||||
const body = `# Kreditfaktura (Credit Note) Process — Accounted
|
||||
|
||||
When a sent invoice needs to be partially or fully reversed, Swedish law
|
||||
(BFL 5 kap. 6-7 §, ML 17 kap. 30 §) requires a kreditfaktura — not a delete,
|
||||
@@ -78,7 +78,7 @@ If the original was unpaid, no refund — the credit just zeroes the AR balance.
|
||||
|
||||
The customer receives a PDF marked "KREDITFAKTURA" with reference to the
|
||||
original invoice number. ML 17 kap. 30 § requires the kreditfaktura to
|
||||
reference the document being credited — gnubok does this automatically.
|
||||
reference the document being credited — Accounted does this automatically.
|
||||
|
||||
### Step 5 — Verify
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Month-End Close — gnubok
|
||||
const body = `# Month-End Close — Accounted
|
||||
|
||||
Run this at the end of each calendar month to ensure books are clean before locking the period.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Monthly Payroll — gnubok
|
||||
const body = `# Monthly Payroll — Accounted
|
||||
|
||||
Salary run + AGI filing for one calendar month.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Quarterly VAT Review — gnubok
|
||||
const body = `# Quarterly VAT Review — Accounted
|
||||
|
||||
End-to-end review of momsdeklaration (SKV 4700) before filing to Skatteverket.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*
|
||||
* - **workflow** skills are static Markdown documents the server ships
|
||||
* alongside tools (month-end close, VAT review, year-end, invoicing,
|
||||
* payroll). They document *how* to compose gnubok tools for a real-world
|
||||
* payroll). They document *how* to compose Accounted tools for a real-world
|
||||
* workflow.
|
||||
* - **horizontal / vertical / modifier** atoms are loaded dynamically from
|
||||
* `agent_atom_registry` — the same atoms the in-app composer assembles for
|
||||
@@ -64,7 +64,7 @@ export interface Skill {
|
||||
export const SKILL_MIME_TYPE = 'text/markdown' as const
|
||||
|
||||
/** Resource URI prefix for skills exposed via resources/read. */
|
||||
export const SKILL_URI_PREFIX = 'gnubok://skill/' as const
|
||||
export const SKILL_URI_PREFIX = 'Accounted://skill/' as const
|
||||
|
||||
/** Build a resource URI for a skill. Slugs with slashes (atom ids) are
|
||||
* URL-encoded so the URI remains well-formed. */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Skill } from './types'
|
||||
|
||||
const body = `# Year-End Close (Bokslut) — gnubok
|
||||
const body = `# Year-End Close (Bokslut) — Accounted
|
||||
|
||||
The annual close. Irreversible. Legally significant. Always staged for human approval.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export const RECEIPT_MATCHER_HTML = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Kvittomatchning — gnubok</title>
|
||||
<title>Kvittomatchning — Accounted</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
|
||||
@@ -11,7 +11,7 @@ export const VAT_REVIEW_HTML = `<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Momsdeklaration — gnubok</title>
|
||||
<title>Momsdeklaration — Accounted</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fafafa;
|
||||
|
||||
@@ -1907,7 +1907,7 @@ export const skatteverketExtension: Extension = {
|
||||
|
||||
/**
|
||||
* Parse and validate declaration request body.
|
||||
* Computes momsuppgift from gnubok's VAT calculation if not provided directly.
|
||||
* Computes momsuppgift from Accounted's VAT calculation if not provided directly.
|
||||
*/
|
||||
async function parseDeclarationRequest(
|
||||
request: Request,
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { SkatteverketMomsuppgift } from '../types'
|
||||
export { formatRedovisare, formatRedovisningsperiod } from '@/lib/skatteverket/format'
|
||||
|
||||
/**
|
||||
* Convert gnubok VatDeclarationRutor to Skatteverket's momsuppgift payload.
|
||||
* Convert Accounted VatDeclarationRutor to Skatteverket's momsuppgift payload.
|
||||
*
|
||||
* Fields with value 0 are omitted (Skatteverket treats absent fields as 0).
|
||||
* This keeps the payload clean and avoids sending unnecessary data.
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function handleSkattekontoDriftDetected(
|
||||
const lines = [
|
||||
`Vi har upptäckt en differens mellan ditt skattekonto och bokföringen per ${fetchedAt}.`,
|
||||
'',
|
||||
'Logga in på gnubok för att se beloppen och granska skattekonto-raderna:',
|
||||
'Logga in på Accounted för att se beloppen och granska skattekonto-raderna:',
|
||||
dashboardLink,
|
||||
'',
|
||||
'Vanliga orsaker att differensen syns redan innan en åtgärd behövs:',
|
||||
@@ -76,7 +76,7 @@ export async function handleSkattekontoDriftDetected(
|
||||
|
||||
const html = `
|
||||
<p>Vi har upptäckt en differens mellan ditt skattekonto och bokföringen per ${escapeHtml(fetchedAt)}.</p>
|
||||
<p><a href="${escapeHtml(dashboardLink)}">Logga in på gnubok</a> för att se beloppen och granska skattekonto-raderna.</p>
|
||||
<p><a href="${escapeHtml(dashboardLink)}">Logga in på Accounted</a> för att se beloppen och granska skattekonto-raderna.</p>
|
||||
<p><strong>Vanliga orsaker att differensen syns redan innan en åtgärd behövs:</strong></p>
|
||||
<ul>
|
||||
<li>Anstånd — saldot förskjuts hos Skatteverket men bokföringen påverkas inte.</li>
|
||||
|
||||
@@ -136,7 +136,7 @@ export interface EnrichmentData {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Complete endpoint request/response (gnubok internal)
|
||||
// Complete endpoint request/response (Accounted internal)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface BankIdCompleteRequest {
|
||||
|
||||
@@ -183,7 +183,7 @@ export async function getCompanyDocuments(companyId: number): Promise<TICDocumen
|
||||
|
||||
/**
|
||||
* Get fiscal-year configurations for a company. v2 endpoint with no v1
|
||||
* equivalent — used to auto-fill fiscal-year selection during gnubok
|
||||
* equivalent — used to auto-fill fiscal-year selection during Accounted
|
||||
* onboarding so the user doesn't have to enter MM-DD manually.
|
||||
*/
|
||||
export async function getFiscalYears(companyId: number): Promise<TICFiscalYear[] | null> {
|
||||
|
||||
Reference in New Issue
Block a user