Files
accounted/supabase/migrations/20260418130000_processing_history.sql
T
Jakob Wennberg 44082ff845 Fix SIE multi-year P&L accumulation, Fortnox supplier filter, processing_history (#267)
* fix: prevent P&L accumulation when importing multi-year SIE files

The opening-balance fallback summed all prior journal lines without
distinguishing balance sheet (class 1-2) from P&L (class 3-8). When
users imported one SIE file per year without running year-end closing
between them, resultatkonton accumulated across years instead of
resetting at each räkenskapsårsskifte. Reported by a customer.

Skip class 3-8 in the fallback path. P&L accounts must reset to zero
each fiscal year (årets resultat → 2099 → equity).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: only import unpaid supplier invoices from Fortnox

Fortnox's /supplierinvoices list endpoint doesn't reliably expose
FullyPaid, which caused historic paid invoices to be imported as
unpaid. Switch to the ?filter=unpaid query and surface that scope
in the migration options UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: add processing_history table for behandlingshistorik

Append-only event log per BFNAR 2013:2 kap 8. Includes:
- processing_history table with seq, correlation/causation chaining,
  aggregate (Document/BankTransaction/MatchProposal/Verifikation/etc.),
  open event_type validated against processing_event_types registry.
- Immutability via audit_log_immutable trigger (no UPDATE/DELETE).
- RLS scoped to user_company_ids; writes via service role only.
- appendProcessingHistory() helper with PII guard rejecting payloads
  containing personnummer/orgnr patterns.
- Shared TS types in types/index.ts.

No consumers wired yet — this is the persistence layer only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: add swedish-project-accounting skill

Reference skill covering projektredovisning: dimensional tagging,
WIP accounting, K2/K3 revenue recognition (successiv vinstavräkning,
färdigställandemetoden), entreprenadavtal, BAS patterns (1470,
1620, 2420, 2450, 4970), and SIE4 #DIM 6 encoding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: rename processing_history migration to avoid timestamp collision

Main already has 20260418120000_allow_retroactive_first_fiscal_year.sql
from #265. Bumping this migration's timestamp to 20260418130000 to
keep schema_migrations.version unique.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address Greptile review on processing_history

- Add BEFORE DELETE immutability trigger so the service role can't
  silently remove rows. Mirrors the pattern from migration 014
  (audit_log_no_update + audit_log_no_delete) and satisfies the
  immutability claim in BFNAR 2013:2 kap 8. Delivered as a follow-up
  migration since the original was already applied in some envs.

- Tighten PII patterns with \b word boundaries to avoid false
  positives on Bankgiro numbers (123456-7890) and invoice references
  like 202312-1234.

- Extend PII validation to actor.label, which previously bypassed
  the payload guard despite the docblock explicitly forbidding
  names/emails/personnummer there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 13:26:41 +02:00

138 lines
5.2 KiB
SQL

-- Processing history (behandlingshistorik)
-- Append-only event log recording every legally significant act per BFNAR 2013:2 kap 8.
-- 7-year retention enforced at application layer (not DB trigger) to allow GDPR redaction.
-- Immutable: UPDATE blocked via audit_log_immutable() trigger.
--
-- seq ordering: BIGSERIAL assigns at statement time, not commit time.
-- Two concurrent transactions may produce seq values out of commit order.
-- For legally meaningful ordering, use occurred_at. seq is a cursor for polling.
-- =============================================================
-- 1. Event type reference table
-- =============================================================
-- Adding a new event type = INSERT, no ALTER TABLE required.
CREATE TABLE public.processing_event_types (
event_type TEXT PRIMARY KEY
);
ALTER TABLE public.processing_event_types ENABLE ROW LEVEL SECURITY;
-- Readable by all authenticated users (reference data)
CREATE POLICY "processing_event_types_select" ON public.processing_event_types
FOR SELECT USING (true);
-- Seed the v0.2 event catalog (28 types)
INSERT INTO public.processing_event_types (event_type) VALUES
-- Document stream
('DocumentIngested'),
('DocumentExtractionAttempted'),
('DocumentClassified'),
('DocumentRejected'),
('DocumentArchived'),
('DocumentSupersededByDuplicate'),
-- BankTransaction stream
('BankTransactionIngested'),
('BankTransactionEnriched'),
-- MatchProposal stream
('MatchAttemptedDeterministic'),
('MatchAttemptedLlm'),
('MatchConfirmed'),
('MatchRejected'),
-- Verifikation stream
('ForslagCreated'),
('ForslagMutated'),
('VerifikationCommitted'),
('RättelseverifikationIssued'),
('ForslagAbandoned'),
-- CounterpartyTemplate stream
('CounterpartyObserved'),
('TemplateStrengthened'),
('TemplateBroken'),
-- Period stream
('PeriodCloseInitiated'),
('PeriodClosed'),
('PeriodReopened'),
-- Migration stream
('MigrationInitiated'),
('MigrationValidated'),
('MigrationCommitted'),
-- System stream
('TimingCeilingTriggered'),
('RubricVersionPublished');
-- =============================================================
-- 2. Processing history table
-- =============================================================
CREATE TABLE public.processing_history (
event_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
seq BIGSERIAL UNIQUE NOT NULL,
company_id UUID NOT NULL REFERENCES public.companies(id) ON DELETE RESTRICT,
correlation_id UUID NOT NULL,
causation_id UUID REFERENCES public.processing_history(event_id),
aggregate_type TEXT NOT NULL CHECK (aggregate_type IN (
'Document',
'BankTransaction',
'MatchProposal',
'Verifikation',
'CounterpartyTemplate',
'Period',
'Migration',
'System'
)),
aggregate_id UUID NOT NULL,
event_type TEXT NOT NULL REFERENCES public.processing_event_types(event_type),
payload JSONB NOT NULL DEFAULT '{}',
payload_schema_version SMALLINT NOT NULL DEFAULT 1,
actor JSONB NOT NULL,
rubric_version TEXT,
occurred_at TIMESTAMPTZ NOT NULL,
appended_at TIMESTAMPTZ NOT NULL DEFAULT now()
-- No updated_at: append-only table
);
-- =============================================================
-- 3. Row-level security
-- =============================================================
ALTER TABLE public.processing_history ENABLE ROW LEVEL SECURITY;
-- SELECT: users can read their companies' processing history
CREATE POLICY "processing_history_select" ON public.processing_history
FOR SELECT USING (company_id IN (SELECT public.user_company_ids()));
-- No INSERT/UPDATE/DELETE policies for authenticated users.
-- Writes via service role client only.
-- =============================================================
-- 4. Indexes
-- =============================================================
-- Aggregate history: "full history of this document/transaction/verifikation"
CREATE INDEX idx_ph_aggregate
ON public.processing_history (company_id, aggregate_type, aggregate_id, seq);
-- Event type filtering: "all VerifikationCommitted events for this company"
CREATE INDEX idx_ph_company_event_type
ON public.processing_history (company_id, event_type, seq);
-- Time range: "all events for this company in March 2026" + retention cleanup
CREATE INDEX idx_ph_company_occurred
ON public.processing_history (company_id, occurred_at);
-- =============================================================
-- 5. Immutability trigger
-- =============================================================
-- Reuses audit_log_immutable() which unconditionally blocks all UPDATE operations.
CREATE TRIGGER processing_history_no_update
BEFORE UPDATE ON public.processing_history
FOR EACH ROW EXECUTE FUNCTION public.audit_log_immutable();
-- =============================================================
-- 6. Schema reload
-- =============================================================
NOTIFY pgrst, 'reload schema';