Files
accounted/.claude/rules/database.md
T
533df34369 fix(payments): make supplier payment batch creation atomic via create_supplier_payment_batch RPC (#1989)
createSupplierPaymentBatch wrote the batch header and its items as two
separate PostgREST inserts, and the active-batch recheck ran app-side
before either. Two concurrent creates selecting the same invoice could
both pass that check and both land an active batch without
confirm_already_batched, and an item-insert failure after the header
landed could leave an empty 'created' batch behind when the best-effort
cancel also failed.

The new SECURITY DEFINER RPC is now the single write path: it locks the
selected invoices FOR UPDATE in id order, re-checks payability, amounts
and active batches inside the transaction, and inserts header + items
together so a constraint violation rolls both back. TypeScript keeps the
shared eligibility evaluation and the msg_id minting (branding lives in
TS); the service result union is unchanged so the route and UI are
untouched.

Closes #1503


Claude-Session: https://claude.ai/code/session_01FkUfWtuFCUkNtRAgMQCse2

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-27 22:24:36 +02:00

82 lines
6.2 KiB
Markdown

---
paths:
- "supabase/migrations/**"
- "tests/pg/**"
---
# Database & Migrations
Use the `/supabase-migration` skill for new migrations.
**Location**: `supabase/migrations/`: 680+ files. Early migrations use sequential numbering (`20240101000001`-`20240101000038`), later ones use real timestamps.
## Migration Rules
1. Enable RLS + policies using `user_company_ids()` for company-scoped data
2. Add `updated_at` trigger via `update_updated_at_column()`
3. UUID PKs: `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 `apply_migration`
8. Always end with `NOTIFY pgrst, 'reload schema'` when altering table structure
**pg-real tests**: any PR touching a trigger/RPC/RLS/DEFERRABLE must include or extend a `*.pg.test.ts`. Parallel Vitest project against real Postgres (CI: `supabase/postgres:15`, migrations replayed). Local: `npm run test:pg`. Helpers: `tests/pg/setup.ts` (`getPool()`, `withUserContext()`), `tests/pg/fixtures.ts` (`seedCompany()`, `insertDraftJournalEntry()`, etc.).
## Key Tables (~170 live tables; the ones that matter)
- **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`, `voucher_sequences`, `voucher_gap_explanations`, `journal_entry_rattelse_log` (immutable who/when log for inline rättelse)
- **Invoicing**: `customers`, `invoices`, `invoice_items`, `invoice_payments`, `invoice_inbox_items`
- **Suppliers**: `suppliers`, `supplier_invoices`, `supplier_invoice_items`, `supplier_payment_batches`, `supplier_payment_batch_items` (betalfil, pain.001)
- **Peppol**: `peppol_registrations`, `peppol_deliveries`, `peppol_delivery_events`, `peppol_delivery_evidence`, `peppol_inbound_documents`, `peppol_access`
- **Banking**: `bank_connections`, `transactions`, `bank_file_imports`, `payment_match_log`
- **Reconciliation**: `account_reconciliations`, `account_reconciliation_attachments`
- **Documents**: `document_attachments` (WORM), `receipts`, `receipt_line_items`
- **Settings**: `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`
- **Skattekonto**: `skattekonto_transactions`, `skattekonto_file_imports`, `skattekonto_rules`, `tax_assessment_notices`
- **API/Auth**: `api_keys`, `oauth_used_codes`, `bankid_identities`
- **Audit/Ops**: `audit_log` (immutable), `event_log` (30d TTL), `pending_operations`, `processing_history`, `ai_usage_tracking`, `automation_webhooks`, `company_migration_resets`
- **Inbox**: `invoice_inbox_items`, `company_inboxes`, `email_connections`
- **WhatsApp**: `whatsapp_phone_links`, `whatsapp_link_codes`, `whatsapp_conversations`, `whatsapp_messages`, `whatsapp_sender_rate_counters`
- **Webshop**: `webshop_orders` (Shopify/WooCommerce orders)
- **Salary**: `employees`, `salary_runs`, `salary_run_employees`, `salary_line_items`, `salary_payroll_config`, `agi_declarations`, `employee_vacation_balances`, `vacation_year_closures`
- **Annual report**: `annual_report_profiles`, `annual_report_versions`, `annual_report_validation_runs`
- **Providers**: `provider_consents`, `provider_consent_tokens`, `provider_otc`
- **Agent**: `agent_atom_registry` (inlined skill bodies, see below), `mcp_tasks`
## Key RPC Functions
- `create_company_with_owner()`: Atomic company + owner creation
- `commit_journal_entry()`: Atomic draft→posted with voucher number
- `correct_entry_metadata()`, `correct_entry_lines_inline()`: Inline rättelse (BFL 5 kap. 5 §) inside the same voucher; audited SECURITY DEFINER, refused in locked/closed periods, logged to `journal_entry_rattelse_log`
- `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
- `current_active_company_id()`: RLS-side read of `user_preferences.active_company_id`; the same value the middleware resolves, so Next.js and RLS agree
- `claim_due_webhook_deliveries()`: Concurrent-safe claim of due `automation_webhooks` deliveries for the cron sender
- `create_supplier_payment_batch()`: Atomic betalfil batch creation (locks invoices, rechecks active batches in-transaction, header + items together)
- `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
## Agent skill bodies (`agent_atom_registry`)
Skill content is authored in `.claude/skills/**/SKILL.md` and inlined into the DB `body` column at runtime (not read from disk: that doesn't bundle on Vercel/Docker). After editing any atom SKILL.md, run `npm run skills:generate` to emit a new `*_seed_agent_atom_bodies.sql` migration and commit it; `npm run skills:check` (wired into CI) fails the build if you forget. Only the curated tiers become atoms: `swedish-*` (horizontal), `industry/<slug>` (vertical), `modifier/<slug>` (modifier); other Claude Code skills never become atoms. The MCP server exposes only atoms with `mcp_exposed = true`.