* feat: prompt to activate missing BAS accounts at commit
Booking to an account not in the active chart previously threw a
generic 400 "Account(s) not found: 5010" and the user had to leave
the form to enable the account via /bookkeeping > BAS-katalog.
- New AccountsNotInChartError thrown from resolveAccountIds in the
engine (and the parallel resolver in core/storno-service). The
query also now filters on is_active=true, so deactivated accounts
are treated the same as never-added ones.
- API routes that call the engine (journal-entries, reverse, correct,
transactions/book + match-invoice + match-supplier-invoice +
uncategorize, invoices/mark-paid, supplier-invoices + mark-paid +
credit, salary/runs/correct, import/opening-balance/execute,
pending-operations/commit) catch the typed error and return a
structured 400: { error: { code: ACCOUNTS_NOT_IN_CHART,
account_numbers, message } }.
- /api/bookkeeping/accounts/activate now also reactivates rows that
already exist but are is_active=false, not only INSERTs. Returns
{ activated, reactivated, skipped, unknown }.
- New GET /api/bookkeeping/accounts/bas-lookup?numbers=... resolves
BAS names client-side so the dialog can show "5010 · Lokalhyra"
without bundling the full 1,276-account catalog.
- ActivateAccountsDialog lists the missing accounts (BAS names + any
unknown non-BAS numbers) and confirms with a single action.
- useSubmitWithAccountActivation wraps an async submit: on
ACCOUNTS_NOT_IN_CHART it opens the dialog, activates on confirm,
then retries the original submit so the user never re-enters data.
- AccountCombobox accepts any 4-digit numeric value, not just items
from the active chart — the activation dialog handles the rest.
- JournalEntryForm wired to the hook + dialog. Other submit surfaces
now surface a clear Swedish message ("Följande konton behöver
aktiveras: …") via getErrorMessage; wiring the dialog into those
is an additive follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs: sync CLAUDE.md with current codebase state
Catch-up on growth since the last CLAUDE.md revision:
- Integrations list now includes AWS Bedrock, Upstash Redis,
Google Drive, Recharts, PDF.js, @react-pdf/renderer, xlsx,
fuse.js, ics.
- Extension table reflects cloud-backup enabled; adds
inbox-smart-match and example-logger; reorders to match current
extensions.config.json.
- Updated counts: 36 event types (was 30+), 35 MCP tools (was 26),
~60 tables (was ~47), 118 migrations (was 93), 19 report
endpoints (was 16), 20 report generators (was 17).
- lib/ directory table now covers salary, providers,
company-lookup, processing-history, support.ts; removes the
deleted settings/ subdir.
- App routes table adds /salary/*, /help, /settings/salary,
/settings/backup.
- API endpoints table adds /api/salary/*, /api/support/contact,
/api/account/delete, /api/audit-trail/*, /api/log,
/api/currency/rate, top-level extension routes.
- Tables section adds Salary, Third-party providers, Inbox &
Migration groups; removes salary_payments (replaced by
salary_runs + salary_line_items).
- Skills list updated to enumerate the Swedish domain skills by
name instead of the old single /swedish-bookkeeping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: address PR review feedback on account activation
Seven fixes based on Greptile + Swedish compliance review on #308.
- ActivateAccountsDialog: disable the confirm button when any
entered number isn't a valid BAS account. Previously activation
would succeed for the knowns and the retry would immediately
fail again on the unknowns, giving a confusing double-toast UX.
- pending-operations/commit: revert commitSendInvoice and
commitMarkInvoiceSent to swallow AccountsNotInChartError
silently. The prior PR upgrade made these blocking, which
regressed invoice delivery for users whose AR accounts are
inactive — and since the activation dialog isn't wired into
those flows yet, there's no one-click recovery. The silent
catches now append an InvoiceJournalEntrySkipped event to
processing_history so the missing verifikation is actionable
in audit trails rather than silently understating the
momsdeklaration (revenue / utgående moms unposted).
- engine.reverseEntry: resolve account IDs with includeInactive=true
so storno of an already-committed entry goes through even when
the user has since deactivated one of its accounts. Blocking
the reversal would leave the original entry uncorrected in
violation of BFL 5 kap 5§ (rättelse must be documented). The
default (includeInactive=false) still applies to createDraftEntry
so new bookings to inactive accounts continue to trigger the
activation dialog.
- supplier-invoices POST + credit: roll back the just-inserted
supplier_invoices row (items cascade-delete) on any JE failure,
not only AccountsNotInChartError. An orphan supplier_invoices
row without a registration / credit JE leaves leverantörsskuld
(2440) and ingående moms (2641) unposted — a silent
understatement / overstatement in the momsdeklaration (ML
2023:200 / BFL 5 kap). The catch now returns a clear Swedish
error message for non-activation failures (typically period
lock or DB error) instead of silently logging.
Test mocks for chart_of_accounts updated for the new query chain
(eq.in.eq instead of eq.eq.in after the is_active conditional).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
519 lines
33 KiB
Markdown
519 lines
33 KiB
Markdown
# CLAUDE.md — gnubok
|
||
|
||
## Project Overview
|
||
|
||
gnubok 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 (Bokforingslagen), 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 (company lookup), Anthropic SDK, AWS Bedrock (`@aws-sdk/client-bedrock-runtime` for inbox smart-match), OpenAI (embeddings), Resend (email), Sentry (error tracking), Svix (webhooks), web-push (notifications), Upstash Redis + Ratelimit, Google Drive (cloud backup via OAuth), JSZip (archive export), sharp (image processing), Framer Motion (animations), Recharts (charts), PDF.js (`pdfjs-dist`), `@react-pdf/renderer` (invoice PDFs), xlsx, fuse.js (fuzzy search), ics (iCal feeds).
|
||
|
||
**Path alias**: `@/*` maps to the project root. **Language**: All code, comments, and commit messages in English. **License**: AGPL-3.0-or-later.
|
||
|
||
---
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
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 setup:extensions # Regenerate extension registry from extensions.config.json
|
||
```
|
||
|
||
---
|
||
|
||
## Key Architectural Relationships
|
||
|
||
- **Multi-tenant model**: `companies` table owns all business data. `company_members` links users to companies with roles (owner/admin/member/viewer). `teams` group companies for consultants. Company context resolved via cookie (`gnubok-company-id`) in middleware (`lib/supabase/middleware.ts`).
|
||
- **All journal entry creation** routes through `lib/bookkeeping/engine.ts`. Lifecycle: `createDraftEntry()` → `commitEntry()` (atomic voucher assignment via `commit_journal_entry` DB RPC). Convenience: `createJournalEntry()` does both. Reversal via `reverseEntry()`. Correction via `correctEntry()` in `lib/core/bookkeeping/storno-service.ts`.
|
||
- **API routes** that emit events must call `ensureInitialized()` (from `lib/init.ts`) at module level. This loads extensions, wires event handlers, and registers the supplier invoice handler + event log handler.
|
||
- **Event bus** (`lib/events/bus.ts`) is a module-level singleton. Handlers run via `Promise.allSettled` — failing handlers never crash the emitter. 36 event types defined in `lib/events/types.ts`. The event log handler persists actionable events to `event_log` table for external automation.
|
||
- **Supabase clients**: browser (`lib/supabase/client.ts`), server with cookies (`createClient()` from `server.ts`), service role (`createServiceClient()`), cookieless service role for API key auth (`createServiceClientNoCookies()` from `lib/auth/api-keys.ts`). Pagination helper: `fetchAllRows()` in `lib/supabase/fetch-all.ts`.
|
||
- **Extension system**: Opt-in via `extensions.config.json`. Core builds and runs with zero extensions. Currently enabled: `enable-banking`, `email`, `arcim-migration`, `tic`, `mcp-server`, `cloud-backup`.
|
||
- **Core reports** (in `lib/reports/`, not extensions): balance sheet, income statement, trial balance, general ledger, AR/supplier ledger, AR/supplier reconciliation, VAT declaration, journal register, monthly breakdown, continuity check, opening balances, KPI (+ definitions), NE-bilaga, INK2 declaration, SIE export, full archive export, salary journal, vacation liability, avgifter basis (employer contributions).
|
||
- **Types**: All shared types in `types/index.ts` (~2,570 lines, single source of truth). Import via `import type { T } from '@/types'`. Event types live in `lib/events/types.ts`. Extension types in `lib/extensions/types.ts`.
|
||
- **Error messages**: `lib/errors/get-error-message.ts` maps technical errors to Swedish user messages (Zod → Postgres → HTTP → context fallback).
|
||
|
||
---
|
||
|
||
## Multi-Tenant Architecture
|
||
|
||
### Data Model
|
||
|
||
- **companies**: Business unit (name, org_number, entity_type, created_by, team_id). All business data (journal entries, invoices, transactions, etc.) has a `company_id` column.
|
||
- **company_members**: Links users to companies (company_id, user_id, role, source='direct'|'team'). Roles: `owner`, `admin`, `member`, `viewer`.
|
||
- **teams**: Consultant grouping (name, created_by). A company can belong to one team. Team members auto-sync to company_members via DB triggers.
|
||
- **team_members**: Links users to teams (team_id, user_id, role='owner'|'admin'|'member').
|
||
- **user_preferences**: Stores `active_company_id` per user.
|
||
|
||
### Company Context Resolution
|
||
|
||
Middleware (`lib/supabase/middleware.ts`) resolves the active company on every request:
|
||
1. Check `gnubok-company-id` cookie
|
||
2. Fall back to `user_preferences.active_company_id`
|
||
3. Fall back to first company membership
|
||
|
||
RLS policies use `user_company_ids()` DB helper function to filter by companies the user has access to.
|
||
|
||
### Invitations
|
||
|
||
- **company_invitations**: Email-based invites with `gnubok_inv_` prefixed tokens (SHA-256 hashed, 7-day TTL).
|
||
- **team_invitations**: Same pattern for team invites.
|
||
- Token generation: `lib/auth/invite-tokens.ts`.
|
||
|
||
---
|
||
|
||
## Authentication
|
||
|
||
Supabase Auth with **email+password** (primary) and **magic link** (fallback). MFA via TOTP is supported.
|
||
|
||
MFA is enforced **application-side** (middleware + API routes), **not** in RLS policies. Controlled by two env vars:
|
||
|
||
- `NEXT_PUBLIC_SELF_HOSTED=true` → MFA never enforced (users can enable voluntarily)
|
||
- `NEXT_PUBLIC_REQUIRE_MFA=true` (hosted/Vercel) → middleware redirects to `/mfa/enroll` or `/mfa/verify` until AAL2
|
||
|
||
**API route auth** (`lib/auth/require-auth.ts`): `requireAuth()` returns `{ user, supabase, error }` discriminated union, enforces MFA on hosted.
|
||
|
||
**API keys** (`lib/auth/api-keys.ts`): SHA-256 hashed with `gnubok_sk_` prefix. Scoped permissions (`TOOL_SCOPE_MAP`). Rate limited at 100 RPM via atomic DB RPC (`validate_and_increment_api_key`).
|
||
|
||
**Cron auth** (`lib/auth/cron.ts`): `verifyCronSecret()` with 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 assignment via `commit_journal_entry` DB RPC). Convenience: `createJournalEntry()` does both in one call. Reversal via `reverseEntry()` (storno). Correction via `correctEntry()` in `lib/core/bookkeeping/storno-service.ts`.
|
||
|
||
**Key engine files**:
|
||
- `transaction-entries.ts` — Journal entries from bank transactions
|
||
- `invoice-entries.ts` — Journal entries from customer invoices (`generatePerRateLines()` for mixed-rate)
|
||
- `supplier-invoice-entries.ts` — Journal entries from supplier invoices
|
||
- `vat-entries.ts` — VAT-related entries
|
||
- `currency-revaluation.ts` — Multi-currency revaluation
|
||
- `mapping-engine.ts` — Account mapping rules evaluation
|
||
- `booking-templates.ts` / `counterparty-templates.ts` — Reusable templates
|
||
- `propose-payment-lines.ts` / `propose-send-lines.ts` — AI-powered matching proposals
|
||
- `handlers/supplier-invoice-handler.ts` — Event handler creating registration entries on confirmation
|
||
|
||
**BAS data** (`bookkeeping/bas-data/`): Full BAS 2026 chart organized by class (1–8) + SRU mapping.
|
||
|
||
### Key BAS Accounts
|
||
|
||
`1510` Accounts receivable | `1930` Business bank account | `2013` Private withdrawals (EF) | `2440` Accounts payable | `2611`/`2621`/`2631` Output VAT 25%/12%/6% | `2641` Input VAT | `2645` Calculated input VAT (EU) | `2893` Shareholder loan (AB) | `3001`/`3002`/`3003` Revenue 25%/12%/6% | `3305`/`3308` Export/EU service revenue
|
||
|
||
### VAT Treatments
|
||
|
||
`standard_25`, `reduced_12`, `reduced_6`, `reverse_charge`, `export`, `exempt`
|
||
|
||
Invoice items support individual `vat_rate` values (mixed-rate invoices). Use `getAvailableVatRates(customerType, vatNumberValidated)` from `lib/invoices/vat-rules.ts`. VIES validation via `lib/vat/vies-client.ts`.
|
||
|
||
### VAT Declaration Rutor (SKV 4700)
|
||
|
||
The `VatDeclarationRutor` type maps to the Swedish tax authority's momsdeklaration form:
|
||
|
||
- **Ruta 05**: Momspliktig försäljning — total domestic taxable sales (all rates combined, from 3001+3002+3003)
|
||
- **Ruta 06/07**: Unused (momspliktiga uttag / vinstmarginalbeskattning), always 0
|
||
- **Ruta 10/11/12**: Utgående moms 25%/12%/6% — output VAT per rate (from 2611/2621/2631)
|
||
- **Ruta 39/40**: EU services / Export (from 3308/3305)
|
||
- **Ruta 48**: Ingående moms — input VAT (from 2641/2645)
|
||
- **Ruta 49**: Moms att betala/återfå = (ruta 10 + 11 + 12 + 30 + 31 + 32 + 60 + 61 + 62) - ruta 48
|
||
|
||
---
|
||
|
||
## Core Services (`lib/core/`)
|
||
|
||
- `bookkeeping/period-service.ts` — Fiscal period lifecycle management (open, close, lock)
|
||
- `bookkeeping/year-end-service.ts` — Year-end closing procedures
|
||
- `bookkeeping/storno-service.ts` — Reversal/correction entry generation
|
||
- `tax/tax-code-service.ts` — Tax code definitions and rates
|
||
- `audit/audit-service.ts` — Audit trail and compliance logging
|
||
- `documents/document-service.ts` — Document attachment lifecycle (WORM storage with version chains)
|
||
|
||
---
|
||
|
||
## 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 builds and runs with zero extensions. `npm run setup:extensions` generates static imports in `lib/extensions/_generated/` (runs automatically via `predev`/`prebuild`). Extensions **cannot** use dynamic imports (Next.js bundling).
|
||
|
||
### Available Extensions (12)
|
||
|
||
| Extension | Purpose | Currently Enabled |
|
||
|-----------|---------|:-:|
|
||
| `enable-banking` | PSD2 bank sync via Enable Banking | Yes |
|
||
| `email` | Email delivery via Resend | Yes |
|
||
| `arcim-migration` | Legacy ARCIM system data migration | Yes |
|
||
| `tic` | TIC Identity company lookup (org number → name, VAT, address) | Yes |
|
||
| `mcp-server` | MCP server for Claude Desktop/Code | Yes |
|
||
| `cloud-backup` | Google Drive backup of SIE + receipts + processing history | Yes |
|
||
| `inbox-smart-match` | AWS Bedrock AI matching of inbox receipts to bank transactions | No |
|
||
| `invoice-inbox` | Email-based invoice document processing | No |
|
||
| `push-notifications` | Web push notifications for events | No |
|
||
| `calendar` | Payment calendar with iCal feed | No |
|
||
| `skatteverket` | Skatteverket VAT declaration submission | No |
|
||
| `example-logger` | Reference implementation — logs events to console (not registered by default) | No |
|
||
|
||
### Extension Architecture
|
||
|
||
**Registration** (`lib/extensions/registry.ts`): Singleton registry. `register()` wires event handlers to the bus. `get(id)`, `getAll()`, `getByCapability(key)`.
|
||
|
||
**Context** (`lib/extensions/context-factory.ts`): Every handler receives `ExtensionContext` with: `userId`, `companyId`, `extensionId`, `supabase`, `emit()`, `settings` (key-value in `extension_data` table), `storage` (Supabase Storage), `log` (prefixed logger), `services` (e.g., `ingestTransactions`).
|
||
|
||
**API routes**: Dispatched via catch-all at `app/api/extensions/ext/[...path]/route.ts`. URL: `/api/extensions/ext/{extensionId}/{routePath}`. Path params extracted as `_paramName` search params.
|
||
|
||
**Service provider patterns**:
|
||
- *Interface registration* (email): Core defines noop default in `lib/email/service.ts`, extension calls `registerEmailService()`, core uses `getEmailService()`.
|
||
- *Services record* (ai-categorization): Extension exposes via `services` property, core looks up via `extensionRegistry.get('id')?.services?.method(...)`.
|
||
|
||
**Creating extensions**: `npx tsx scripts/create-extension.ts --name my-ext --sector general --category operations --description "..."`, then add to `extensions.config.json`.
|
||
|
||
---
|
||
|
||
## MCP Server & API Keys
|
||
|
||
gnubok exposes its bookkeeping engine as an MCP (Model Context Protocol) server, letting users do bookkeeping through Claude Desktop, Claude Code, or any MCP-compatible client.
|
||
|
||
**MCP extension** (`extensions/general/mcp-server/`): 35 tools — transactions, categorization, customers, suppliers, invoices, supplier invoices, accounts, fiscal periods, trial balance, general ledger, balance sheet, income statement, AR/supplier ledger, reconciliation, VAT report, KPI report, receipt matching, invoice payments/sending, inbox items, employees + salary runs (list/get/create/calculate), salary journal, AGI generation, document upload. JSON-RPC 2.0 protocol implemented directly (no SDK dependency). Endpoint: `/api/extensions/ext/mcp-server/mcp`.
|
||
|
||
**API key infrastructure** (`lib/auth/api-keys.ts`, `api_keys` table): SHA-256 hashed keys with `gnubok_sk_` prefix. Scoped permissions mapped via `TOOL_SCOPE_MAP`. Rate limited at 100 RPM via atomic DB RPC (`validate_and_increment_api_key`). `createServiceClientNoCookies()` creates a Supabase service client without cookies for API key auth — all queries filter by `company_id` (defense in depth).
|
||
|
||
**OAuth 2.1** for Claude Desktop connectors:
|
||
- `.well-known/oauth-protected-resource` and `.well-known/oauth-authorization-server` — discovery endpoints (excluded from auth middleware)
|
||
- `/api/mcp-oauth/authorize` — consent page + auth code generation
|
||
- `/api/mcp-oauth/token` — PKCE verification + API key creation
|
||
- `/api/mcp-oauth/register` — dynamic client registration
|
||
- Stateless encrypted auth codes (AES-256-GCM via `lib/auth/oauth-codes.ts`)
|
||
- Single-use enforcement via `oauth_used_codes` table
|
||
- Redirect URI allowlist: `claude.ai/api/*`, `claude.com/api/*`, `localhost`
|
||
|
||
**npm package** (`packages/gnubok-mcp`): Published as `gnubok-mcp` on npm. Stdio-to-HTTP bridge for Claude Desktop. Users configure `npx gnubok-mcp` with their API key.
|
||
|
||
---
|
||
|
||
## API Route Pattern
|
||
|
||
```typescript
|
||
import { createClient } from '@/lib/supabase/server'
|
||
import { NextResponse } from 'next/server'
|
||
import { ensureInitialized } from '@/lib/init'
|
||
import { validateBody } from '@/lib/api/validate'
|
||
import { MySchema } from '@/lib/api/schemas'
|
||
|
||
ensureInitialized() // Module-level — loads extensions for event emission
|
||
|
||
export async function POST(request: Request) {
|
||
const supabase = await createClient()
|
||
const { data: { user } } = await supabase.auth.getUser()
|
||
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||
|
||
const result = await validateBody(request, MySchema)
|
||
if (!result.success) return result.response
|
||
|
||
// Business logic... always filter by company_id (defense in depth alongside RLS)
|
||
return NextResponse.json({ data: result })
|
||
}
|
||
```
|
||
|
||
- Dynamic route params: `{ params }: { params: Promise<{ id: string }> }` (Next.js 16)
|
||
- Response shapes: `{ data }` for success, `{ error }` for failures
|
||
- Zod schemas in `lib/api/schemas.ts` — 30+ schemas with shared primitives (uuid, isoDate, accountNumber, nonNegativeAmount)
|
||
|
||
---
|
||
|
||
## Key lib/ Directories
|
||
|
||
| Directory | Purpose |
|
||
|-----------|---------|
|
||
| `bookkeeping/` | Engine, entry generators, mapping, templates, BAS data, template library + embeddings |
|
||
| `core/` | Period service, year-end, storno, tax codes, audit, documents |
|
||
| `events/` | Event bus singleton, 36 event types, event log handler |
|
||
| `auth/` | API keys, require-auth, require-write, MFA, OAuth codes, invite tokens, cron auth, BankID |
|
||
| `supabase/` | Browser/server/service clients, middleware, fetch-all pagination |
|
||
| `api/` | Zod validation (`validateBody`/`validateQuery`), schemas |
|
||
| `reports/` | 20 report generators (financial statements, ledgers, tax, exports, salary journal, vacation liability, avgifter basis) |
|
||
| `invoices/` | Invoice/supplier matching, payment match log, reminders, VAT rules, PDF template |
|
||
| `transactions/` | Multi-source ingestion (`ingest.ts`), AI category suggestions |
|
||
| `import/` | SIE parser/import, bank file import, opening balance, account mapper |
|
||
| `documents/` | Document matcher, receipt matcher, batch matching |
|
||
| `extensions/` | Registry, loader, context factory, types, generated files |
|
||
| `email/` | Service interface (noop default), Resend provider, templates (invite, invoice, reminder, consent) |
|
||
| `company/` | Company context resolution, CRUD actions, fiscal period computation |
|
||
| `company-lookup/` | Shared types for org-number → company info lookups |
|
||
| `providers/` | Third-party accounting provider adapters (Fortnox, Bokio, Briox, BL/Björn Lundén, Visma) with OAuth, rate limiting, retry, consent resolution |
|
||
| `salary/` | Payroll calculation engine, tax tables, absence/benefits/traktamente, AGI, KU, PDF payslips, löneväxling, personnummer, payment, salary entries, transaction matcher |
|
||
| `processing-history/` | Processing-history append helper for audit/inbox timelines |
|
||
| `reconciliation/` | Bank statement reconciliation |
|
||
| `tax/` | Tax calculator, deadline config/generator, expense warnings, Swedish holidays |
|
||
| `vat/` | VIES client, EU countries, MOMS box mapping |
|
||
| `deadlines/` | Deadline status engine |
|
||
| `currency/` | Riksbanken exchange rates |
|
||
| `skatteverket/` | Tax authority data formatting |
|
||
| `bankgiro/` | Luhn checksum validation |
|
||
| `calendar/` | ICS generator, calendar utilities |
|
||
| `errors/` | Swedish error message mapping (Zod → Postgres → HTTP → fallback) |
|
||
| `hooks/` | React hooks (e.g., `use-unsaved-changes`, `use-can-write`) |
|
||
| `logger.ts` | Structured logger with module prefixes, env-aware filtering |
|
||
| `support.ts` | Server-side support recipient email (used by `/api/support/contact`) |
|
||
| `utils.ts` | `cn()`, `formatCurrency()`, `formatDate()`, `formatOrgNumber()` |
|
||
|
||
---
|
||
|
||
## App Routes
|
||
|
||
### Pages
|
||
|
||
| Route | Purpose |
|
||
|-------|---------|
|
||
| `/login`, `/register`, `/reset-password` | Auth pages |
|
||
| `/mfa/enroll`, `/mfa/verify` | MFA flow |
|
||
| `/onboarding` | Multi-step company setup wizard |
|
||
| `/companies/new` | Create new company |
|
||
| `/invite/[token]` | Accept team/company invite |
|
||
| `/` | Dashboard home |
|
||
| `/transactions` | Bank transaction list & categorization |
|
||
| `/invoices`, `/invoices/new`, `/invoices/[id]`, `/invoices/[id]/credit` | Customer invoicing |
|
||
| `/supplier-invoices`, `/supplier-invoices/new`, `/supplier-invoices/[id]` | Supplier invoices |
|
||
| `/customers`, `/customers/[id]` | Customer management |
|
||
| `/suppliers`, `/suppliers/[id]` | Supplier management |
|
||
| `/expenses`, `/expenses/new`, `/expenses/[id]` | Expense tracking |
|
||
| `/receipts`, `/receipts/scan` | Receipt management |
|
||
| `/bookkeeping`, `/bookkeeping/[id]`, `/bookkeeping/year-end` | Journal entries, chart of accounts, year-end |
|
||
| `/salary`, `/salary/employees`, `/salary/runs` | Payroll: employees, salary runs, AGI, KU |
|
||
| `/reports` | Financial reports |
|
||
| `/import` | SIE and bank file import |
|
||
| `/kpi` | KPI metrics + monthly trend chart |
|
||
| `/deadlines` | Tax & business deadlines |
|
||
| `/pending` | Pending operations queue |
|
||
| `/help` | In-app help/support page |
|
||
| `/extensions`, `/extensions/[sector]/[extension]` | Extension marketplace |
|
||
| `/e/[sector]/[slug]` | Extension workspace |
|
||
| `/settings/*` | Company, invoicing, bookkeeping, tax, team, banking, templates, salary, backup, account, API settings |
|
||
| `/dpa`, `/privacy` | Legal pages |
|
||
| `/invoice-action/[token]` | Public invoice payment link |
|
||
| `/sandbox` | Test environment |
|
||
|
||
### API Endpoints (key groups)
|
||
|
||
- `/api/bookkeeping/*` — Accounts, fiscal periods (close/lock/year-end/opening-balances/currency-revaluation), journal entries (CRUD/reverse/correct/chain), mapping rules, voucher gaps
|
||
- `/api/invoices/*` — CRUD, send, mark-sent/paid, convert, PDF, reminders cron
|
||
- `/api/supplier-invoices/*` — CRUD, approve, mark-paid, credit
|
||
- `/api/transactions/*` — Categorize, uncategorize, describe, book, match-invoice, match-supplier-invoice, batch operations, AI suggestions
|
||
- `/api/customers/*`, `/api/suppliers/*` — CRUD
|
||
- `/api/documents/*` — CRUD, versions, link, verify, match-sweep, verify cron
|
||
- `/api/reports/*` — 19 report endpoints (general-ledger, trial-balance, balance-sheet, income-statement, journal-register, ar-ledger, supplier-ledger, vat-declaration, sie-export, ink2, ne-bilaga, kpi, audit-trail, continuity-check, monthly-breakdown, full-archive, salary-journal, vacation-liability, avgifter-basis)
|
||
- `/api/salary/*` — Employees, payroll-config, tax-tables, KU, salary runs (CRUD + calculate)
|
||
- `/api/import/*` — Bank file (parse/execute), SIE (parse/execute/mappings/create-accounts)
|
||
- `/api/reconciliation/bank/*` — Link, unlink, run, status, unmatched-entries
|
||
- `/api/settings/*` — Company settings, API keys, logo upload, counterparty templates, booking templates
|
||
- `/api/company/*` — Current, members (list/CRUD/invite), `[id]`
|
||
- `/api/team/*` — Accept, invite, members
|
||
- `/api/deadlines/*`, `/api/tax-deadlines/*` — Deadline CRUD and crons
|
||
- `/api/pending-operations/*` — Queue, commit, reject
|
||
- `/api/events/*` — Event log and cleanup cron
|
||
- `/api/documents/*` — CRUD, counts, match-sweep, verify cron
|
||
- `/api/calendar/feed/[token]` — iCal subscription feed
|
||
- `/api/mcp-oauth/*` — Register, authorize, token
|
||
- `/api/support/contact` — Support contact form submission
|
||
- `/api/account/delete` — User account deletion
|
||
- `/api/audit-trail/*` — Audit trail queries
|
||
- `/api/log` — Client-side log ingestion
|
||
- `/api/health` — Health check
|
||
- `/api/vat/validate` — VIES VAT validation
|
||
- `/api/currency/rate` — Riksbanken exchange rate lookup
|
||
- `/api/sandbox/*` — Seed, cleanup cron
|
||
- `/api/extensions/ext/[...path]` — Dynamic extension API routes (plus top-level `/api/extensions/enable-banking/*`, `/api/extensions/cloud-backup/*`, `/api/extensions/push-notifications/*`)
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
**Framework**: Vitest 4, `globals: true`, `environment: 'node'`. Tests colocated in `__tests__/` directories. Scope: business logic in `lib/` and API routes in `app/api/`. No component or E2E tests.
|
||
|
||
**Test helpers** (`tests/helpers.ts`): `createMockSupabase()` (chainable proxy), `createQueuedMockSupabase()` (sequential calls), `createMockRequest()`, `parseJsonResponse()`, `createMockRouteParams()`, and fixture factories: `makeTransaction()`, `makeJournalEntry()`, `makeJournalEntryLine()`, `makeInvoice()`, `makeInvoicePayment()`, `makeCustomer()`, `makeSupplier()`, `makeSupplierInvoice()`, `makeFiscalPeriod()`, `makeReceipt()`, `makeDocumentAttachment()`, `makeCompanySettings()`, `makeCompany()`, `makeCompanyMember()`, `makeInvoiceInboxItem()`, `makeTaxCode()`, `makeCategorizationTemplate()`, `makeSIEVoucher()`, `makeBankConnection()`.
|
||
|
||
**Patterns**: Always mock `@/lib/supabase/server`. Use `vi.clearAllMocks()` and `eventBus.clear()` in `beforeEach`. API route tests: mock `@/lib/init` and lib functions, test auth (401), validation (400), not found (404), errors (500), happy path.
|
||
|
||
---
|
||
|
||
## Database & Migrations
|
||
|
||
**Location**: `supabase/migrations/` — 118 files. Early migrations use sequential numbering (`20240101000001`–`20240101000038`), later ones use real timestamps.
|
||
|
||
### Key Tables (~60)
|
||
|
||
**Multi-tenant**: `companies`, `company_members`, `company_invitations`, `teams`, `team_members`, `team_invitations`, `user_preferences`, `profiles`
|
||
|
||
**Bookkeeping**: `chart_of_accounts`, `fiscal_periods`, `journal_entries`, `journal_entry_lines`, `account_balances`, `voucher_sequences`, `voucher_gap_explanations`
|
||
|
||
**Invoicing**: `customers`, `invoices`, `invoice_items`, `invoice_payments`, `invoice_inbox_items`
|
||
|
||
**Suppliers**: `suppliers`, `supplier_invoices`, `supplier_invoice_items`
|
||
|
||
**Banking**: `bank_connections`, `transactions`, `bank_file_imports`, `payment_match_log`
|
||
|
||
**Documents**: `document_attachments` (WORM), `receipts`, `receipt_line_items`
|
||
|
||
**Settings & Config**: `company_settings`, `mapping_rules`, `categorization_templates`, `booking_template_library`, `extension_data`
|
||
|
||
**Dimensions**: `cost_centers`, `projects`
|
||
|
||
**Tax & Deadlines**: `tax_rates`, `tax_table_rates`, `deadlines`, `calendar_feeds`, `skatteverket_tokens`
|
||
|
||
**API & Auth**: `api_keys` (with scopes), `oauth_used_codes`, `bankid_identities`
|
||
|
||
**Audit & Ops**: `audit_log` (immutable), `event_log` (30-day TTL), `pending_operations`, `processing_history` (+ `processing_event_types`), `ai_usage_tracking`, `voucher_gap_explanations`, `automation_webhooks`
|
||
|
||
**Inbox & Migration**: `invoice_inbox_items`, `company_inboxes`, `email_connections`
|
||
|
||
**Salary**: `employees`, `salary_runs`, `salary_run_employees`, `salary_line_items`, `salary_payroll_config`, `agi_declarations`
|
||
|
||
**Third-party providers**: `provider_consents`, `provider_consent_tokens`, `provider_otc`
|
||
|
||
**Other**: `sandbox_users`
|
||
|
||
### Key RPC Functions
|
||
|
||
- `create_company_with_owner()` — Atomic company + owner creation
|
||
- `commit_journal_entry()` — Atomic draft→posted with voucher number
|
||
- `next_voucher_number()` — Concurrent-safe voucher generation
|
||
- `detect_voucher_gaps()` — BFNAR 2013:2 gap detection
|
||
- `generate_invoice_number()`, `get_next_arrival_number()`, `generate_delivery_note_number()` — Sequence generators
|
||
- `seed_chart_of_accounts()` — BAS chart seeding per entity type
|
||
- `validate_and_increment_api_key()` — Atomic rate limiting
|
||
- `user_company_ids()` — RLS helper returning user's company IDs
|
||
- `get_unlinked_1930_lines()` — Bank reconciliation helper
|
||
- `cleanup_sandbox_user()`, `cleanup_expired_sandbox_users()` — Sandbox lifecycle
|
||
|
||
### Key Triggers
|
||
|
||
- `check_journal_entry_balance()` — Debit must equal credit
|
||
- `enforce_journal_entry_immutability()` — Posted entries cannot be modified
|
||
- `enforce_period_lock()` — No entries in closed/locked periods
|
||
- `enforce_company_lock_date()` — Company-wide bookkeeping lock date
|
||
- `block_document_deletion()` — WORM compliance
|
||
- `enforce_retention_journal_entries()` — 7-year retention
|
||
- `audit_log_immutable()` — Audit log cannot be modified
|
||
- `write_audit_log()` — Auto-audit on DML operations
|
||
- `sync_team_member_to_companies()` — Auto-sync team→company membership
|
||
|
||
### Migration Rules
|
||
|
||
1. **Always enable RLS** and create policies using `user_company_ids()` for company-scoped data
|
||
2. **Always add `updated_at` trigger** using `update_updated_at_column()`
|
||
3. **UUID primary keys**: `DEFAULT uuid_generate_v4()`
|
||
4. **Company ownership**: `company_id UUID REFERENCES companies NOT NULL` + `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL`
|
||
5. **Never modify existing migrations** — create new ones
|
||
6. **Never modify enforcement triggers** (migration 017) — legally required
|
||
7. **Apply via Supabase MCP tool**: `mcp__plugin_supabase_supabase__apply_migration`
|
||
8. **Always include `NOTIFY pgrst, 'reload schema'`** at the end of migrations that alter table structure (ADD/DROP COLUMN, CREATE TABLE, ALTER TYPE). Without this, PostgREST serves stale schema until next reload.
|
||
|
||
---
|
||
|
||
## Skills, Git & CI
|
||
|
||
**Skills**: Always use `/frontend-design` for new UI. Use `vercel:deploy` for deployment. Use `/supabase-migration` for new migrations. Use `/erp-api-route` for new API routes. Use `/create-extension` for new extensions. Use the Swedish domain skills (`swedish-sie-import-export`, `swedish-accounting-compliance`, `swedish-vat`, `swedish-invoice-compliance`, `swedish-payroll`, `swedish-year-end-closing`, `swedish-financial-reporting`, `swedish-sru-filing`, `swedish-asset-accounting`, `swedish-project-accounting`, `swedish-tax-planning`) for accounting domain questions.
|
||
|
||
**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 on main.
|
||
|
||
**Docker** (`.github/workflows/docker-publish.yml`): Pushes to GHCR (`erp-mafia/erp-base`) on main push. 4-stage Dockerfile (base → deps → builder → runner) with Node 22 Alpine. Runtime env placeholder replacement via `docker-entrypoint.sh`. Docker Compose with app + supercronic cron service.
|
||
|
||
---
|
||
|
||
## Deployment
|
||
|
||
### Vercel (Hosted)
|
||
|
||
Cron jobs defined in `vercel.json`:
|
||
|
||
| Schedule | Endpoint | Purpose |
|
||
|----------|----------|---------|
|
||
| `0 6 * * *` | `/api/deadlines/status/cron` | Update deadline statuses |
|
||
| `0 8 * * *` | `/api/invoices/reminders/cron` | Send invoice reminders |
|
||
| `0 0 2 1 *` | `/api/tax-deadlines/cron` | Generate tax deadlines |
|
||
| `0 5 * * *` | `/api/extensions/enable-banking/sync/cron` | Bank transaction sync |
|
||
| `0 3 * * *` | `/api/documents/verify/cron` | Document integrity verification (daily) |
|
||
| `0 4 * * *` | `/api/sandbox/cleanup/cron` | Sandbox user cleanup |
|
||
| `0 2 * * *` | `/api/events/cleanup/cron` | Event log cleanup (30-day TTL) |
|
||
| `0 * * * *` | `/api/extensions/cloud-backup/auto-sync/cron` | Hourly cloud backup auto-sync |
|
||
|
||
### Docker (Self-Hosted)
|
||
|
||
- `Dockerfile`: 4-stage Node 22 Alpine build with standalone output
|
||
- `docker-compose.yml`: App service + supercronic cron scheduler
|
||
- `docker-entrypoint.sh`: Validates required env vars, replaces build-time placeholders in `.next/static/` JS
|
||
- Extension presets: `docker/extensions.self-hosted.json`, `docker/extensions.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 extension is 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`
|
||
|
||
## Other
|
||
Never create a NUL/nul file: \gnubok\NUL
|
||
|
||
---
|
||
|
||
## Design Context
|
||
|
||
### Users
|
||
|
||
Swedish sole traders (enskild firma) and small business owners (aktiebolag) who need to manage their own bookkeeping. They are not accountants — they are professionals (consultants, freelancers, shop owners) who want to stay compliant without hiring one. They use gnubok in short, focused sessions: sending an invoice, categorizing bank transactions, filing a VAT declaration. Speed and clarity matter — every second spent in the app is a second away from their real work.
|
||
|
||
### Brand & Aesthetic
|
||
|
||
**Minimal. Sharp. Efficient.** The interface should feel like a well-made instrument: considered, quiet, and confident. Reference: Mercury (banking). Anti-reference: enterprise software (SAP/Oracle density).
|
||
|
||
- **Palette**: Grayscale foundation with restrained semantic colors — sage green (success/balance), terracotta (errors/overdue), ochre (warnings/attention). No loud brand color.
|
||
- **Typography**: Fraunces (serif) for display headings, Geist (sans) for body. Tabular numbers everywhere financial data appears.
|
||
- **Surfaces**: White/near-white cards on light gray backgrounds. Subtle borders (60% opacity). Soft shadows. Dark mode follows the same restraint.
|
||
- **Spacing**: Generous whitespace. Dense data (tables, ledgers) uses tighter spacing but never feels cramped.
|
||
- **Motion**: Subtle and purposeful. Stagger animations for list entry, spring easing for feedback. Never decorative.
|
||
- **Icons**: Lucide — 15px in navigation, slightly larger in empty states.
|
||
|
||
### Design Principles
|
||
|
||
1. **Clarity over cleverness.** Every element immediately understandable. Clear labels (in Swedish), obvious hierarchy.
|
||
2. **Earned minimalism.** Remove what doesn't serve the task, but don't strip context that prevents compliance errors.
|
||
3. **Numbers are first-class.** Tabular-nums, proper alignment, adequate contrast, clear positive/negative distinction.
|
||
4. **Trust through consistency.** Same patterns, spacing, and behavior everywhere.
|
||
5. **Speed is a feature.** Optimize for the 90-second session.
|
||
|
||
### Accessibility
|
||
|
||
- **WCAG AA**: 4.5:1 text contrast, 3:1 UI components
|
||
- Keyboard-navigable with visible focus rings
|
||
- Respect `prefers-reduced-motion`
|
||
- Color never sole indicator of state — always pair with icons, text, or shape
|