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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
8ed943198f
commit
44082ff845
@@ -0,0 +1,137 @@
|
||||
-- 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';
|
||||
@@ -0,0 +1,12 @@
|
||||
-- Add the missing BEFORE DELETE immutability trigger on processing_history.
|
||||
--
|
||||
-- Mirrors the pattern from migration 014 (audit_log) where both UPDATE and
|
||||
-- DELETE are blocked via audit_log_immutable(). Without this trigger the
|
||||
-- service role could silently remove rows, contradicting BFNAR 2013:2 kap 8
|
||||
-- behandlingshistorik immutability requirements.
|
||||
|
||||
CREATE TRIGGER processing_history_no_delete
|
||||
BEFORE DELETE ON public.processing_history
|
||||
FOR EACH ROW EXECUTE FUNCTION public.audit_log_immutable();
|
||||
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user