Files
accounted/CLAUDE.md
T
Jakob Wennberg c74b19df1b 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>
2026-06-03 10:52:01 +02:00

14 KiB
Raw Blame History

CLAUDE.md — Accounted

Project Overview

Accounted is a Swedish-focused accounting SaaS for sole traders (enskild firma) and limited companies (aktiebolag). It implements double-entry bookkeeping compliant with Swedish accounting law (Bokföringslagen), including VAT handling, tax reporting, and 7-year document retention. Multi-tenant: each user can own or be a member of multiple companies, optionally grouped into teams (for consultants).

Tech stack: Next.js 16.1.5 (App Router), React 19.2.3, TypeScript 5 (strict), Zod 4, Supabase (PostgreSQL + RLS + email/password + TOTP MFA auth), Tailwind CSS 4 + shadcn/ui, Vercel hosting, Docker (self-hosted).

Integrations: Enable Banking (PSD2), TIC Identity, Anthropic SDK, AWS Bedrock, OpenAI, Resend, Sentry, Svix, web-push, Upstash Redis, Google Drive, JSZip, sharp, Framer Motion, Recharts, PDF.js, @react-pdf/renderer, xlsx, fuse.js, ics.

Path alias: @/* maps to the project root. Language: All code, comments, and commit messages in English. License: AGPL-3.0-or-later.


Commands

npm run dev              # Start dev server (runs setup:extensions first)
npm run build            # Production build (runs setup:extensions first)
npm run lint             # ESLint
npm test                 # Run all Vitest tests
npx vitest run <dir>     # Run tests in a specific directory
npm run test:pg          # pg-real tests against real Postgres
npm run setup:extensions # Regenerate extension registry from extensions.config.json
npm run skills:generate  # Regenerate agent_atom_registry seed migration after editing an atom SKILL.md
npm run skills:check     # CI guard: fail if an atom SKILL.md changed without regenerating the seed migration

Key Architectural Relationships

  • Multi-tenant model: companies owns all business data. company_members links users to companies (owner/admin/member/viewer). teams group companies. Context resolved via gnubok-company-id cookie in lib/supabase/middleware.ts.
  • All journal entry creation routes through lib/bookkeeping/engine.ts. Lifecycle: createDraftEntry()commitEntry() (atomic voucher via commit_journal_entry RPC). createJournalEntry() does both. Reversal: reverseEntry(). Correction: correctEntry() in lib/core/bookkeeping/storno-service.ts.
  • API routes emitting events must call ensureInitialized() (lib/init.ts) at module level to load extensions and wire handlers.
  • Event bus (lib/events/bus.ts) is a module-level singleton using Promise.allSettled. 50+ event types in lib/events/types.ts. Persisted to event_log table (30-day TTL).
  • Supabase clients: browser (client.ts), server cookies (createClient()), service role (createServiceClient()), cookieless service role for API keys (createServiceClientNoCookies()). Pagination: fetchAllRows().
  • Extension system: Opt-in via extensions.config.json. Core runs with zero extensions.
  • Types: Shared types in types/index.ts (~3,100 lines). Import via import type { T } from '@/types'. Event types in lib/events/types.ts. Extension types in lib/extensions/types.ts.
  • Error messages: lib/errors/get-error-message.ts maps to Swedish (Zod → Postgres → HTTP → fallback).

Repository Map

  • lib/bookkeeping/ — Engine, entry generators, mapping, templates, BAS data
  • lib/core/ — Period, year-end, storno, tax codes, audit, documents
  • lib/events/ — Bus singleton, event types, event log handler
  • lib/auth/ — API keys, require-auth/write, MFA, OAuth codes, invite tokens, cron, BankID
  • lib/supabase/ — Clients, middleware, fetchAllRows pagination
  • lib/api/ — Zod validation (validateBody/validateQuery), schemas
  • lib/reports/ — Report generators (balance sheet, income statement, trial balance, GL, AR/supplier ledger, VAT declaration, SIE, INK2, NE-bilaga, KPI, salary, vacation, …)
  • lib/invoices/, lib/transactions/, lib/import/ (SIE/bank/opening balance), lib/documents/ (matchers)
  • lib/providers/ — Fortnox, Bokio, Briox, BL, Visma (OAuth, retry, consent)
  • lib/salary/ — Payroll engine, tax tables, AGI, KU, payslips, löneväxling, personnummer
  • lib/reconciliation/, lib/tax/, lib/vat/ (VIES, MOMS box), lib/deadlines/, lib/currency/ (Riksbanken), lib/skatteverket/, lib/bankgiro/ (Luhn), lib/calendar/ (ICS)
  • lib/utils.ts (cn(), formatCurrency(), formatDate(), formatOrgNumber()), lib/logger.ts
  • app/(dashboard)/* — pages; app/api/* — API routes; supabase/migrations/ — schema; extensions/general/* — opt-in extensions
  • Path-scoped detail lives in .claude/rules/ (see Path-scoped rules below).

Multi-Tenant Architecture

  • companies: Business unit. All business data has a company_id column.
  • company_members: Roles owner/admin/member/viewer, source direct|team.
  • teams: Consultant grouping. Team members auto-sync to company_members via DB triggers.
  • user_preferences: Stores active_company_id and locale.

Context resolution (lib/supabase/middleware.ts): cookie → user_preferences.active_company_id → first membership. RLS uses user_company_ids() helper.

Invitations: company_invitations/team_invitations with gnubok_inv_ tokens (SHA-256, 7-day TTL). See lib/auth/invite-tokens.ts.


Authentication

Supabase Auth: email+password (primary), magic link (fallback), TOTP MFA. MFA enforced application-side (middleware + API routes), not in RLS.

  • NEXT_PUBLIC_SELF_HOSTED=true → MFA never enforced
  • NEXT_PUBLIC_REQUIRE_MFA=true → middleware redirects to /mfa/enroll or /mfa/verify until AAL2

API route auth (lib/auth/require-auth.ts): requireAuth() returns { user, supabase, error }, enforces MFA on hosted. API keys (lib/auth/api-keys.ts): SHA-256 hashed, gnubok_sk_ prefix. Scoped via TOOL_SCOPE_MAP. Rate limited 100 RPM via validate_and_increment_api_key RPC. Cron auth (lib/auth/cron.ts): verifyCronSecret() constant-time comparison.


Core Bookkeeping Engine

The engine (lib/bookkeeping/engine.ts) is the most critical system. All accounting flows route through it.

Lifecycle: createDraftEntry()commitEntry() (atomic voucher via commit_journal_entry RPC). createJournalEntry() does both. reverseEntry() for storno; correctEntry() (lib/core/bookkeeping/storno-service.ts) for corrections.

Engine files: transaction-entries.ts, invoice-entries.ts (with generatePerRateLines() for mixed-rate), supplier-invoice-entries.ts, vat-entries.ts, currency-revaluation.ts, mapping-engine.ts, booking-templates.ts/counterparty-templates.ts, propose-payment-lines.ts/propose-send-lines.ts, handlers/supplier-invoice-handler.ts.

BAS data (lib/bookkeeping/bas-data/): Full BAS 2026 chart by class (18) + SRU mapping.

Key BAS accounts, VAT treatments, VAT declaration rutor, and lib/core/ services are in .claude/rules/bookkeeping.md. For accounting-law questions use the Swedish domain skills.


Accounting Guard Rails

These rules exist for legal compliance, enforced by database triggers. Never violate them.

  1. Committed entries are immutable. Once status: 'posted', cannot be edited or deleted (DB trigger).
  2. Never delete posted entries. Use reverseEntry() (storno) to cancel.
  3. Every entry must balance. sum(debits) === sum(credits), both > 0.
  4. Voucher numbers are sequential. Assigned atomically via commit_journal_entry DB RPC. Never set manually.
  5. Voucher gap documentation. BFNAR 2013:2 requires documented explanations for gaps (voucher_gap_explanations table, detect_voucher_gaps RPC).
  6. Period lock enforcement. DB trigger blocks writes to closed/locked periods. Company-wide lock date enforced via enforce_company_lock_date() trigger.
  7. 7-year document retention. DB triggers prevent deletion of documents linked to posted entries.
  8. Storno, never edit. Use correctEntry() from lib/core/bookkeeping/storno-service.ts.
  9. Use Math.round(x * 100) / 100 for monetary calculations. Never toFixed().
  10. Always use engine functions. Never insert directly into journal tables.
  11. Account numbers are strings. '1930', never 1930.

Extension System

Extensions are opt-in plugins in extensions/general/<name>/, controlled by extensions.config.json. Core runs with zero extensions. npm run setup:extensions generates static imports in lib/extensions/_generated/ (auto via predev/prebuild). Extensions cannot use dynamic imports.

Enabled (extensions.config.json): enable-banking (PSD2), email (Resend), arcim-migration, tic (org lookup), mcp-server, cloud-backup (Google Drive), skatteverket, invoice-inbox, document-extraction. Present but disabled: calendar, push-notifications, example-logger (plus the _example-branding template).

  • Registration (lib/extensions/registry.ts): Singleton. register() wires handlers. get(id), getAll(), getByCapability(key).
  • Context (lib/extensions/context-factory.ts): ExtensionContext = userId, companyId, extensionId, supabase, emit(), settings, storage, log, services.
  • Creating: use the /create-extension skill, or npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "...".

MCP Server & API Keys

Accounted exposes its bookkeeping engine as an MCP server (extensions/general/mcp-server/) for Claude Desktop/Code — 90+ tools, JSON-RPC 2.0, endpoint /api/extensions/ext/mcp-server/mcp, OAuth 2.1 for Claude connectors. npm bridge: packages/gnubok-mcp (npx gnubok-mcp).

API keys (lib/auth/api-keys.ts, api_keys table): SHA-256, gnubok_sk_ prefix, scoped via TOOL_SCOPE_MAP, 100 RPM via validate_and_increment_api_key RPC. createServiceClientNoCookies() — all queries filter by company_id (defense in depth).

Tool authoring conventions, the staged-operation completion-signal pattern, and OAuth details are in .claude/rules/mcp-server.md.


Testing

Framework: Vitest 4, node env, tests in __tests__/. Scope: lib/ and app/api/. No component/E2E tests.

Helpers (tests/helpers.ts): createMockSupabase(), createQueuedMockSupabase(), createMockRequest(), parseJsonResponse(), createMockRouteParams(), plus fixture factories (makeTransaction, makeJournalEntry, makeInvoice, makeCustomer, makeSupplier, makeSupplierInvoice, makeFiscalPeriod, etc.).

Patterns: Always mock @/lib/supabase/server. vi.clearAllMocks() + eventBus.clear() in beforeEach. Test auth (401), validation (400), 404, 500, happy path.

pg-real: Parallel Vitest project for triggers/RPCs/RLS using real Postgres. File convention *.pg.test.ts. Required: any PR touching a trigger/RPC/RLS/DEFERRABLE must include or extend a *.pg.test.ts. (Details in .claude/rules/database.md.)


Skills, Git & CI

Skills: Use /frontend-design for new UI, vercel:deploy for deployment, /supabase-migration for new migrations, /erp-api-route for new API routes, /create-extension for new extensions. Use the Swedish domain skills (swedish-vat, swedish-accounting-compliance, swedish-invoice-compliance, swedish-payroll, swedish-year-end-closing, swedish-sie-import-export, swedish-sru-filing, swedish-financial-reporting, swedish-asset-accounting, swedish-project-accounting, swedish-tax-planning, swedish-e-invoicing) for accounting domain questions. The swedish-*, industry/*, and modifier/* skills also ship as product atoms (see .claude/rules/database.md).

Git: Conventional commits (feat:, fix:, refactor:, test:, docs:). Atomic commits, branch from main.

CI:

  • .github/workflows/core-build.yml — resets extensions to empty, runs build + test, verifies no core code imports from @/extensions/ directly.
  • .github/workflows/swedish-compliance-review.yml — Swedish accounting compliance review on PRs touching bookkeeping/reports/tax logic.
  • .github/workflows/docker-publish.yml — pushes images to GHCR (erp-mafia/erp-base) on main.

Deployment

  • Vercel (hosted): Cron jobs in vercel.json (deadline status, invoice reminders, tax deadlines, enable-banking sync, document verify, sandbox cleanup, event log cleanup, cloud-backup auto-sync).
  • Docker (self-hosted): 4-stage Node 22 Alpine Dockerfile (standalone output) + docker-compose.yml (app + supercronic cron). docker-entrypoint.sh validates env vars and replaces build-time placeholders in .next/static/. Extension presets: docker/extensions.{self-hosted,hosted}.json.

Environment variables:

  • Required: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, NEXT_PUBLIC_APP_URL, CRON_SECRET
  • Auth: NEXT_PUBLIC_REQUIRE_MFA (set true on hosted), NEXT_PUBLIC_SELF_HOSTED (set true for Docker)
  • Extension-specific (only when enabled): ENABLE_BANKING_APP_ID/ENABLE_BANKING_APP_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, RESEND_API_KEY, VAPID_PUBLIC_KEY/VAPID_PRIVATE_KEY
  • Optional: SENTRY_DSN, SENTRY_AUTH_TOKEN

Path-scoped rules (.claude/rules/)

Topic detail loads automatically when Claude touches matching files:

  • design.md — design context & locked design-system tokens (app/**, components/**)
  • i18n.md — bilingual sv/en conventions + "stays Swedish" surfaces (UI + lib/email|invoices|reports|salary)
  • api-routes.md — API route pattern + endpoint map (app/api/**)
  • database.md — migration rules, key tables/RPCs/triggers, agent_atom_registry (supabase/migrations/**)
  • mcp-server.md — MCP tool authoring conventions (extensions/general/mcp-server/**)
  • bookkeeping.md — BAS accounts, VAT treatments/rutor, lib/core/ services (lib/bookkeeping|core|reports|vat|invoices|salary)

Other

Never create a NUL/nul file: \Accounted\NUL.