170 lines
9.0 KiB
Markdown
170 lines
9.0 KiB
Markdown
# Part 1: Database Foundation & Compliance Core — Implementation Record
|
|
|
|
## What was implemented
|
|
|
|
8 Supabase migrations, TypeScript type updates, 4 new service files, and modifications to 3 existing files. No UI changes.
|
|
|
|
---
|
|
|
|
## Migrations
|
|
|
|
### Migration 11: ALTER Existing Tables
|
|
`supabase/migrations/20240101000011_alter_existing_tables.sql`
|
|
|
|
- `chart_of_accounts` — added `sru_code text` for Skatteverket SRU mapping
|
|
- `journal_entries` — added `committed_at timestamptz`, `reversed_by_id uuid FK→self`, `reverses_id uuid FK→self`, `correction_of_id uuid FK→self`
|
|
- `journal_entries` — expanded `source_type` CHECK to include `storno`, `correction`, `import`, `system`
|
|
- `journal_entry_lines` — added `tax_code text`, `cost_center text`, `project text`
|
|
- `fiscal_periods` — added `locked_at timestamptz`, `retention_expires_at date`
|
|
|
|
### Migration 12: Tax Code Engine
|
|
`supabase/migrations/20240101000012_tax_codes.sql`
|
|
|
|
New table `tax_codes` with columns: `id`, `user_id`, `code`, `description`, `rate`, `moms_basis_boxes text[]`, `moms_tax_boxes text[]`, `moms_input_boxes text[]`, flags (`is_output_vat`, `is_reverse_charge`, `is_eu`, `is_export`, `is_oss`, `is_system`).
|
|
|
|
RLS: select own + system (user_id IS NULL), insert/update/delete own only.
|
|
|
|
Seeded 12 system tax codes: MP1 (25%), MP2 (12%), MP3 (6%), MPI, MPI12, MPI6, IV (intra-EU), EUS (EU sale), IP (import), EXP (export), OSS, NONE.
|
|
|
|
New function `seed_tax_codes_for_user(p_user_id)` copies system codes to user scope.
|
|
|
|
### Migration 13: Document Archive
|
|
`supabase/migrations/20240101000013_document_archive.sql`
|
|
|
|
New table `document_attachments` with: storage fields (`storage_path`, `file_name`, `file_size_bytes`, `mime_type`), integrity (`sha256_hash NOT NULL`), version chain (`version`, `original_id FK→self`, `superseded_by_id FK→self`, `is_current_version`), digitization metadata (`uploaded_by`, `upload_source`, `digitization_date`), linkage (`journal_entry_id FK ON DELETE RESTRICT`, `journal_entry_line_id FK ON DELETE RESTRICT`).
|
|
|
|
No DELETE RLS policy — deletion handled by trigger in migration 17.
|
|
|
|
### Migration 14: Audit Log
|
|
`supabase/migrations/20240101000014_audit_log.sql`
|
|
|
|
New table `audit_log`: `user_id uuid NOT NULL` (no FK cascade — survives user deletion), `action text` with CHECK constraint, `table_name`, `record_id`, `actor_id`, `old_state jsonb`, `new_state jsonb`, `description`. No `updated_at` — append-only.
|
|
|
|
BEFORE UPDATE and BEFORE DELETE triggers raise exception to enforce immutability.
|
|
|
|
### Migration 15: Dimensions
|
|
`supabase/migrations/20240101000015_dimensions.sql`
|
|
|
|
Two new tables:
|
|
- `cost_centers` (`user_id`, `code`, `name`, `is_active`) with UNIQUE(user_id, code)
|
|
- `projects` (`user_id`, `code`, `name`, `is_active`, `start_date`, `end_date`) with UNIQUE(user_id, code)
|
|
|
|
Both with standard RLS and updated_at triggers.
|
|
|
|
### Migration 16: Voucher Sequence Hardening
|
|
`supabase/migrations/20240101000016_voucher_sequences.sql`
|
|
|
|
New table `voucher_sequences` (`user_id`, `fiscal_period_id`, `voucher_series`, `last_number`) for tracking sequence state.
|
|
|
|
Replaced `next_voucher_number()` with concurrent-safe version using `INSERT ON CONFLICT DO UPDATE RETURNING` (row-level lock instead of MAX+1).
|
|
|
|
New DEFERRABLE constraint trigger `check_balance_on_post` validates debit==credit when an entry transitions from draft to posted.
|
|
|
|
New function `detect_voucher_gaps(p_user_id, p_fiscal_period_id, p_series)` returns gap ranges for compliance reporting.
|
|
|
|
### Migration 17: Enforcement Triggers
|
|
`supabase/migrations/20240101000017_enforcement_triggers.sql`
|
|
|
|
8 trigger functions:
|
|
|
|
1. **`enforce_journal_entry_immutability()`** — allows draft→draft, draft→posted, posted→reversed. Blocks all other updates/deletes on committed entries.
|
|
2. **`enforce_journal_entry_line_immutability()`** — blocks modifications to lines of posted/reversed entries.
|
|
3. **`enforce_period_lock()`** — rejects journal_entries writes when `is_closed=true` OR `locked_at IS NOT NULL`.
|
|
4. **`enforce_period_lock_documents()`** — blocks document attachment to entries in locked periods.
|
|
5. **`block_document_deletion()`** — blocks deletion if linked to committed entry or within retention window. Logs blocked attempts to audit_log.
|
|
6. **`enforce_retention_journal_entries()`** — blocks journal entry deletion within 7-year retention window.
|
|
7. **`set_committed_at()`** — auto-sets `committed_at = now()` on draft→posted transition.
|
|
8. **`calculate_retention_expiry()`** — auto-sets `retention_expires_at = period_end + 7 years`. Backfills existing rows.
|
|
|
|
### Migration 18: Audit Logging Triggers
|
|
`supabase/migrations/20240101000018_audit_triggers.sql`
|
|
|
|
SECURITY DEFINER function `write_audit_log()` that detects action type from TG_OP and state transitions (draft→posted = COMMIT, posted→reversed = REVERSE, locked_at set = LOCK_PERIOD, is_closed set = CLOSE_PERIOD). Captures old_state/new_state as JSONB.
|
|
|
|
AFTER triggers on: `journal_entries`, `journal_entry_lines`, `chart_of_accounts`, `document_attachments`, `fiscal_periods`, `company_settings`, `tax_codes`.
|
|
|
|
---
|
|
|
|
## TypeScript Changes
|
|
|
|
### Modified types in `types/index.ts`
|
|
|
|
| Type | Change |
|
|
|------|--------|
|
|
| `JournalEntrySourceType` | Added `'storno' \| 'correction' \| 'import' \| 'system'` |
|
|
| `JournalEntry` | Added `committed_at`, `reversed_by_id`, `reverses_id`, `correction_of_id` |
|
|
| `JournalEntryLine` | Added `tax_code`, `cost_center`, `project` |
|
|
| `CreateJournalEntryLineInput` | Added optional `tax_code`, `cost_center`, `project` |
|
|
| `FiscalPeriod` | Added `locked_at`, `retention_expires_at` |
|
|
| `BASAccount` | Added `sru_code` |
|
|
|
|
### New types added to `types/index.ts`
|
|
|
|
- `TaxCode` interface, `TaxCodeId` union type
|
|
- `DocumentAttachment` interface, `DocumentUploadSource` type, `CreateDocumentAttachmentInput`
|
|
- `AuditLogEntry` interface, `AuditAction` union type
|
|
- `CostCenter` interface
|
|
- `Project` interface
|
|
- `VoucherGap` interface
|
|
|
|
---
|
|
|
|
## New Service Files
|
|
|
|
### `lib/core/audit/audit-service.ts`
|
|
Read-only service (audit log is written by DB triggers):
|
|
- `getAuditLog(userId, filters)` — paginated query with action/table/date filters
|
|
- `getEntityHistory(userId, tableName, recordId)` — full mutation history of one record
|
|
- `getCorrectionChain(userId, journalEntryId)` — traces original→storno→corrected via linked IDs
|
|
|
|
### `lib/core/documents/document-service.ts`
|
|
- `uploadDocument(userId, file, metadata)` — computes SHA-256 via Web Crypto, uploads to Supabase Storage, creates record
|
|
- `createNewVersion(userId, originalId, file)` — creates new version, marks old as superseded (WORM)
|
|
- `linkToJournalEntry(userId, documentId, journalEntryId)` — links document to entry
|
|
- `verifyIntegrity(userId, documentId)` — re-downloads, re-hashes, compares to stored hash
|
|
|
|
### `lib/core/tax/tax-code-service.ts`
|
|
- `getTaxCodes(userId)` — returns user codes + system codes
|
|
- `getTaxCodeByCode(userId, code)` — single lookup, user code takes precedence
|
|
- `calculateMomsFromTaxCodes(userId, periodStart, periodEnd)` — sums journal lines by tax_code, maps to moms boxes via tax_codes table
|
|
- `seedTaxCodes(userId)` — calls `seed_tax_codes_for_user` RPC
|
|
|
|
### `lib/core/bookkeeping/storno-service.ts`
|
|
- `correctEntry(userId, originalEntryId, correctedLines)` — 3-step correction:
|
|
1. Creates storno entry with swapped debits/credits, `source_type='storno'`, `reverses_id` set
|
|
2. Creates corrected entry with new data, `source_type='correction'`, `correction_of_id` set
|
|
3. Marks original as reversed with `reversed_by_id` set
|
|
4. Returns `{ reversal, corrected }`
|
|
|
|
---
|
|
|
|
## Modified Existing Files
|
|
|
|
### `lib/bookkeeping/engine.ts`
|
|
|
|
- New `buildLineInserts()` helper that includes `tax_code`, `cost_center`, `project` in all line inserts
|
|
- New `createDraftEntry(userId, input)` — inserts as draft with `voucher_number=0`, no commit
|
|
- New `commitEntry(userId, entryId)` — assigns voucher number via `next_voucher_number` RPC, transitions to posted (DB triggers handle `committed_at` and balance validation)
|
|
- Existing `createJournalEntry()` kept as convenience wrapper (create + immediate commit)
|
|
- `reverseEntry()` rewritten: now sets `reverses_id` on the reversal entry, sets `reversed_by_id` on the original, uses `source_type='storno'`, preserves dimensions on reversed lines
|
|
|
|
### `lib/reports/vat-declaration.ts`
|
|
|
|
- Added `TaxCode` import
|
|
- New `calculateVatDeclarationFromTaxCodes(userId, periodType, year, period)` — generates momsdeklaration by querying journal_entry_lines grouped by `tax_code`, then mapping via `tax_codes` table to moms boxes
|
|
- Legacy `calculateVatDeclaration()` preserved for backward compatibility (invoice/transaction/receipt approach)
|
|
|
|
### `lib/reports/sie-export.ts`
|
|
|
|
- Now fetches `cost_centers` and `projects` tables
|
|
- Outputs `#DIM 1 "Kostnadsställe"` and `#DIM 6 "Projekt"` dimension definitions
|
|
- Outputs `#OBJEKT` records for each cost center and project
|
|
- Outputs `#SRU` records from `chart_of_accounts.sru_code` after each `#KONTO`
|
|
- `#TRANS` lines now include dimension object lists: `{1 "CC01" 6 "P01"}` when cost_center/project are set
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
- `npx tsc --noEmit` passes with zero errors
|