9.0 KiB
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— addedsru_code textfor Skatteverket SRU mappingjournal_entries— addedcommitted_at timestamptz,reversed_by_id uuid FK→self,reverses_id uuid FK→self,correction_of_id uuid FK→selfjournal_entries— expandedsource_typeCHECK to includestorno,correction,import,systemjournal_entry_lines— addedtax_code text,cost_center text,project textfiscal_periods— addedlocked_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:
enforce_journal_entry_immutability()— allows draft→draft, draft→posted, posted→reversed. Blocks all other updates/deletes on committed entries.enforce_journal_entry_line_immutability()— blocks modifications to lines of posted/reversed entries.enforce_period_lock()— rejects journal_entries writes whenis_closed=trueORlocked_at IS NOT NULL.enforce_period_lock_documents()— blocks document attachment to entries in locked periods.block_document_deletion()— blocks deletion if linked to committed entry or within retention window. Logs blocked attempts to audit_log.enforce_retention_journal_entries()— blocks journal entry deletion within 7-year retention window.set_committed_at()— auto-setscommitted_at = now()on draft→posted transition.calculate_retention_expiry()— auto-setsretention_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
TaxCodeinterface,TaxCodeIdunion typeDocumentAttachmentinterface,DocumentUploadSourcetype,CreateDocumentAttachmentInputAuditLogEntryinterface,AuditActionunion typeCostCenterinterfaceProjectinterfaceVoucherGapinterface
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 filtersgetEntityHistory(userId, tableName, recordId)— full mutation history of one recordgetCorrectionChain(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 recordcreateNewVersion(userId, originalId, file)— creates new version, marks old as superseded (WORM)linkToJournalEntry(userId, documentId, journalEntryId)— links document to entryverifyIntegrity(userId, documentId)— re-downloads, re-hashes, compares to stored hash
lib/core/tax/tax-code-service.ts
getTaxCodes(userId)— returns user codes + system codesgetTaxCodeByCode(userId, code)— single lookup, user code takes precedencecalculateMomsFromTaxCodes(userId, periodStart, periodEnd)— sums journal lines by tax_code, maps to moms boxes via tax_codes tableseedTaxCodes(userId)— callsseed_tax_codes_for_userRPC
lib/core/bookkeeping/storno-service.ts
correctEntry(userId, originalEntryId, correctedLines)— 3-step correction:- Creates storno entry with swapped debits/credits,
source_type='storno',reverses_idset - Creates corrected entry with new data,
source_type='correction',correction_of_idset - Marks original as reversed with
reversed_by_idset - Returns
{ reversal, corrected }
- Creates storno entry with swapped debits/credits,
Modified Existing Files
lib/bookkeeping/engine.ts
- New
buildLineInserts()helper that includestax_code,cost_center,projectin all line inserts - New
createDraftEntry(userId, input)— inserts as draft withvoucher_number=0, no commit - New
commitEntry(userId, entryId)— assigns voucher number vianext_voucher_numberRPC, transitions to posted (DB triggers handlecommitted_atand balance validation) - Existing
createJournalEntry()kept as convenience wrapper (create + immediate commit) reverseEntry()rewritten: now setsreverses_idon the reversal entry, setsreversed_by_idon the original, usessource_type='storno', preserves dimensions on reversed lines
lib/reports/vat-declaration.ts
- Added
TaxCodeimport - New
calculateVatDeclarationFromTaxCodes(userId, periodType, year, period)— generates momsdeklaration by querying journal_entry_lines grouped bytax_code, then mapping viatax_codestable to moms boxes - Legacy
calculateVatDeclaration()preserved for backward compatibility (invoice/transaction/receipt approach)
lib/reports/sie-export.ts
- Now fetches
cost_centersandprojectstables - Outputs
#DIM 1 "Kostnadsställe"and#DIM 6 "Projekt"dimension definitions - Outputs
#OBJEKTrecords for each cost center and project - Outputs
#SRUrecords fromchart_of_accounts.sru_codeafter each#KONTO #TRANSlines now include dimension object lists:{1 "CC01" 6 "P01"}when cost_center/project are set
Verification
npx tsc --noEmitpasses with zero errors