* feat(nav): interaction-mode sidebar grouping — Arbeta/Analys/Data/Skatt & bokslut Nav IA redesign phase 0 (dev_docs/nav_ia_redesign.md): same routes, regrouped by what the user is doing. CLAUDE.md restructured around Hard Rules (doc references updated); pending-page explainer removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): correctness + feedback batch for Bankavstämning (phase 0) Engine: fetchAllRows pagination on status/run/RPC fetches (silent 1000-row cap corrupted totals), optimistic-lock guards on manualLink + apply, unlink audit rows attributed to the acting user (was: company UUID), selected_matches partial apply intersected with a fresh match run. View: silent in-place refresh instead of a full-page skeleton per action, checkbox-gated apply with confidence badges (fuzzy unticked) in chunks of 500, honest result toasts, dry-run errors surfaced, ranked per-row picker candidates pinned to the applied date window, currency-correct amounts (bank side in account currency, GL side SEK), voucher links, translated source types, colored differens, dirty-date-filter guard. Discovery: year-end preflight 404 href fixed (/reconciliation/bank never existed), ⌘K palette entry, real links from the transactions page. v1: status registry schema now matches the actual ReconciliationStatus payload, errors documented as a count, false ~0.85-threshold pitfall replaced, route test mocks the real shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
9.3 KiB
CLAUDE.md — Accounted
Swedish accounting SaaS: double-entry bookkeeping under Swedish accounting law (Bokföringslagen) for sole traders (enskild firma) and limited companies (aktiebolag). Multi-tenant: users belong to companies via company_members; teams group companies for consultants.
Stack: Next.js 16 (App Router), React 19, TypeScript 5 strict, Zod 4, Supabase (Postgres + RLS + auth), Tailwind 4 + shadcn/ui. Vercel-hosted is the primary target; Docker self-hosted must keep working but never at hosted's expense. Path alias @/* = repo root. All code, comments, and commits in English.
Hard Rules
The accounting rules are Swedish law, enforced by DB triggers. Code that violates them fails at runtime; code that works around the triggers breaks legal compliance. Never do either.
- Never edit or delete a posted journal entry. Committed vouchers are immutable. Cancel with
reverseEntry(); correct withcorrectEntry()(lib/core/bookkeeping/storno-service.ts). Storno, never edit. - All journal writes go through
lib/bookkeeping/engine.ts. Never insert into journal tables directly: voucher numbers are assigned atomically by thecommit_journal_entryRPC and must stay sequential, and gaps require documented explanations (BFNAR 2013:2 —voucher_gap_explanations). - Every entry balances:
sum(debits) === sum(credits), both> 0. - Respect period locks. DB triggers block writes to closed/locked periods and behind the company lock date. Don't work around them — fix the flow that tried to write there.
- Never delete documents linked to posted entries — 7-year retention is a legal requirement.
- Money math is
Math.round(x * 100) / 100. NevertoFixed()— it returns strings and rounds incorrectly, causing öre-level drift that breaks entry balance. - Account numbers are strings (
'1930', never1930). They are identifiers, not quantities; arithmetic on them is always a bug.
General prohibitions:
- Never modify an existing migration — schemas already shipped; create a new migration. Never touch the enforcement triggers (migration 017); they are legally required.
- Core code must never import from
@/extensions/. CI builds core with zero extensions enabled; a direct import breaks that build. Extensions cannot use dynamic imports (the registry generates static imports viasetup:extensions). - Don't add dependencies without asking. This is an AGPL-3.0 project; license compatibility matters, and the dependency surface is audited.
- Don't "finish" the gnubok → Accounted rename. Wire-format identifiers keep the old name on purpose:
gnubok-company-idcookie,gnubok_sk_/gnubok_inv_prefixes,gnubok-mcpnpm package. Renaming them breaks live sessions, API keys, and invites. - Treat
.env.localas pointing at the production database. Never run seed/cleanup/repair scripts against it without explicit confirmation. - Keep the diff scoped to the request. No drive-by refactors of untouched code.
- Never create a NUL/nul file:
\Accounted\NUL.
When Uncertain
- Stop and ask; do not guess. Especially for anything touching posted entries, the production database, money math, or Swedish tax law.
- Swedish domain questions are never answered from training data. Load the matching
swedish-*skill (vat, accounting-compliance, invoice-compliance, payroll, year-end-closing, sie-import-export, sru-filing, financial-reporting, asset-accounting, project-accounting, tax-planning, e-invoicing). - Scaffolding has skills — use them instead of improvising:
/erp-api-route(API routes),/supabase-migration(migrations),/create-extension(extensions),/frontend-design(new UI),vercel:deploy(deployment).
Definition of Done
A change is done when all of these hold — iterate until they do:
npm run lintis clean andnpm testpasses (npx vitest run <dir>while iterating).- New or changed logic in
lib/orapp/api/has tests: auth 401, validation 400, 404, happy path; mock@/lib/supabase/server. - Any change to a trigger, RPC, RLS policy, or DEFERRABLE constraint ships with a
*.pg.test.ts(npm run test:pg). - New UI strings exist in both
messages/sv.jsonandmessages/en.json. - If you edited an atom
SKILL.md,npm run skills:generatewas run (CI'sskills:checkfails otherwise). npm run check:guardspasses if you touched API routes.- Commit is conventional (
feat:/fix:/refactor:/test:/docs:), atomic, branched frommain.
Commands
npm run dev # Dev server (runs setup:extensions first)
npm run build # Production build (runs setup:extensions first)
npm run lint # ESLint
npm test # All Vitest tests
npx vitest run <dir> # Tests in one directory
npm run test:pg # pg-real tests against real Postgres
npm run check:guards # Ratchet guard (e.g. no hand-rolled route auth)
npm run setup:extensions # Regenerate extension registry from extensions.config.json
npm run skills:generate # Regenerate agent_atom_registry seed after editing an atom SKILL.md
Architecture
- Journal entry lifecycle:
createDraftEntry()→commitEntry()(atomic voucher viacommit_journal_entryRPC);createJournalEntry()does both. Everything accounting-shaped routes through this engine. - Tenancy: every business table has
company_id. Active company resolves inlib/supabase/middleware.ts:gnubok-company-idcookie →user_preferences.active_company_id→ first membership. RLS usesuser_company_ids(); queries still filter bycompany_idexplicitly (defense in depth — service-role paths have no RLS). - Auth: Supabase email+password + TOTP MFA, enforced application-side, not in RLS.
NEXT_PUBLIC_REQUIRE_MFA=trueon hosted;NEXT_PUBLIC_SELF_HOSTED=truedisables MFA. API routes wrapwithRouteContext— it is the only path that enforces MFA, so never hand-rollsupabase.auth.getUser()in a route. - Events:
lib/events/bus.tsis a module-level singleton. Any route that emits events must callensureInitialized()(lib/init.ts) at module level — otherwise extension handlers are never wired and events silently go nowhere. - Supabase clients: browser
client.ts, servercreateClient(), service rolecreateServiceClient(), cookieless service rolecreateServiceClientNoCookies()(lives inlib/auth/api-keys.ts; for API-key/MCP paths). Paginate withfetchAllRows()— PostgREST silently caps at 1000 rows. - Extensions: opt-in plugins in
extensions/general/<name>/;extensions.config.jsonis the source of truth for what's enabled. Core must run with zero extensions. - MCP server: the bookkeeping engine is exposed as 100+ MCP tools (
extensions/general/mcp-server/), authenticated bygnubok_sk_API keys (SHA-256, scoped, default 100 RPM per key). - Types: import from
@/types(types/index.ts); event types inlib/events/types.ts. - User-facing errors are Swedish: map through
lib/errors/get-error-message.ts. - Cron: hosted cron jobs live in
vercel.json, authenticated viaverifyCronSecret()(lib/auth/cron.ts).
Repository Map
lib/bookkeeping/— engine, entry generators, mapping, templates, BAS 2026 data (bas-data/)lib/core/— period, year-end, storno, tax codes, audit, documentslib/events/,lib/auth/,lib/supabase/,lib/api/(ZodvalidateBody/validateQuery)lib/reports/— balance sheet, income statement, trial balance, GL, ledgers, VAT, SIE, INK2, NE-bilaga, salary, …lib/invoices/,lib/transactions/,lib/import/,lib/documents/,lib/salary/,lib/reconciliation/,lib/tax/,lib/vat/,lib/providers/(Fortnox/Bokio/Briox/BL/Visma),lib/skatteverket/,lib/currency/,lib/bankgiro/,lib/deadlines/,lib/calendar/lib/utils.ts—cn(),formatCurrency(),formatDate(),formatOrgNumber();lib/logger.tsapp/(dashboard)/*pages;app/api/*routes;supabase/migrations/schema;extensions/general/*plugins
Testing
Vitest 4, node env, tests in __tests__/, scope lib/ + app/api/ (no component/E2E tests). Helpers in tests/helpers.ts: createMockSupabase(), createQueuedMockSupabase(), createMockRequest(), parseJsonResponse(), plus fixture factories (makeTransaction, makeJournalEntry, makeInvoice, …). vi.clearAllMocks() + eventBus.clear() in beforeEach. Trigger/RPC/RLS behavior is tested in *.pg.test.ts against real Postgres, not with mocks.
Detail Loads On Demand
Don't duplicate these here — they auto-load when you touch matching paths:
.claude/rules/design.md— design system, locked tokens (app/**,components/**).claude/rules/i18n.md— sv/en conventions, "stays Swedish" surfaces.claude/rules/api-routes.md—withRouteContextroute pattern, endpoint map (app/api/**).claude/rules/database.md— migration rules, key tables/RPCs/triggers, pg-real (supabase/migrations/**).claude/rules/mcp-server.md— MCP tool authoring, staged-operation pattern.claude/rules/bookkeeping.md— BAS accounts, VAT treatments/rutor,lib/core/services
Decision Log
When you make a non-obvious choice — picked approach A over B, declined a dependency, stopped because a rule here forbade something — append one line to dev_docs/DECISIONS.md: [YYYY-MM-DD] <decision> — <why>. Check that file before re-litigating a past decision.