* feat: implement skattekonto drift detection and alerting - Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum. - Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming. - Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows. feat: create own account transfer detection - Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN. - Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs. feat: establish cash accounts as a first-class entity - Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures. - Implement functions for listing, upserting, and managing cash accounts, including primary account designation. feat: enhance GL line reconciliation functionality - Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies. - Update related functions to ensure compatibility with the new cash_accounts structure. feat: capture counterparty IBAN in transactions - Add counterparty_iban column to transactions table to facilitate intra-account transfer detection. - Create index for efficient lookups based on counterparty IBAN. * feat: Enhance cash account handling and reconciliation processes - Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'. - Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes. - Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy. - Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731). - Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities. - Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates. - Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one. - Updated email notifications for drift detection to avoid exposing sensitive financial data. - Enhanced bank reconciliation logic to handle multi-currency transactions correctly. - Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage. - Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards.
133 lines
6.4 KiB
SQL
133 lines
6.4 KiB
SQL
-- Migration: skattekonto_rules — extensible counter-account rules for skattekontot
|
|
--
|
|
-- Why this exists: skattekonto-booking previously hardcoded an 8-entry array of
|
|
-- (substring → counter-account) rules in TypeScript. Each new SKV transaktionstext
|
|
-- pattern (omprövning, skattetillägg, förseningsavgift, ...) required a code change.
|
|
-- This table lets users (and migrations) add rules without redeploys, mirroring the
|
|
-- Fortnox "Regelverk" model.
|
|
--
|
|
-- Sign convention is handled by the caller (skattekonto-booking.ts): a positive
|
|
-- belopp_skatteverket debits 1630 and credits counter_account; a negative belopp
|
|
-- does the reverse. Rules only resolve the counter-account.
|
|
--
|
|
-- System seeds (company_id IS NULL): read-only to all companies; cannot be mutated
|
|
-- via RLS. Per-company rules override system seeds via lower numeric priority.
|
|
--
|
|
-- Special sentinel: counter_account = '__PRIMARY_SEK__' resolves at runtime via
|
|
-- the cash_accounts.is_primary lookup. Used so the inbetalning / utbetalning rules
|
|
-- don't have to assume 1930 is the bank account for every company.
|
|
--
|
|
-- Account 8314 (Skattefria ränteintäkter) is used for intäktsränta instead of 8313
|
|
-- (Ränteintäkter från bankgiro etc., taxable) because skattekontoräntan is skattefri
|
|
-- per IL 8 kap 7 §.
|
|
--
|
|
-- Account 6992 (Övriga externa kostnader, ej avdragsgilla) catches skattetillägg
|
|
-- and förseningsavgift — both are non-deductible penalties on SKV charges.
|
|
--
|
|
-- "Omprövning" deliberately has NO system rule: it's a re-assessment, not a
|
|
-- penalty. The underlying tax (moms, F-skatt, AGI) is what changes — the existing
|
|
-- moms/preliminärskatt/AGI rules cover those cases. Routing "omprövning" to 6992
|
|
-- by keyword alone would mis-book a moms re-assessment as a non-deductible cost.
|
|
--
|
|
-- Anstånd is intentionally NOT given a rule. Anstånd is an SKV-side deferral
|
|
-- (the saldo changes but no underlying tax is restated), so the GL doesn't move.
|
|
-- The resolver returns null and the booking flow surfaces NO_COUNTER_ACCOUNT for
|
|
-- the user to handle manually if a rare anstånd-across-closed-period case appears.
|
|
|
|
CREATE TABLE public.skattekonto_rules (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
-- NULL = system default seed, readable by every company.
|
|
company_id UUID REFERENCES public.companies(id) ON DELETE CASCADE,
|
|
-- Lower priority wins. Per-company rules typically use 1-49 to override system
|
|
-- seeds (10-30).
|
|
priority INTEGER NOT NULL,
|
|
-- Comma-separated lowercase substrings; ANY match wins.
|
|
pattern TEXT NOT NULL CHECK (length(pattern) > 0),
|
|
amount_min NUMERIC,
|
|
amount_max NUMERIC,
|
|
company_type TEXT NOT NULL DEFAULT 'all'
|
|
CHECK (company_type IN ('aktiebolag','enskild_firma','all')),
|
|
-- BAS counter-account, or the literal sentinel '__PRIMARY_SEK__'.
|
|
counter_account TEXT NOT NULL CHECK (length(counter_account) > 0),
|
|
-- Override for enskild_firma when the same rule needs a different account for EF
|
|
-- (e.g. preliminärskatt: AB → 2510, EF → 2012). NULL means use counter_account.
|
|
counter_account_ef TEXT,
|
|
label TEXT,
|
|
active BOOLEAN NOT NULL DEFAULT true,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX idx_skattekonto_rules_lookup
|
|
ON public.skattekonto_rules (company_id, priority)
|
|
WHERE active = true;
|
|
|
|
ALTER TABLE public.skattekonto_rules ENABLE ROW LEVEL SECURITY;
|
|
|
|
-- System seeds are visible to all authenticated users; per-company rules only to
|
|
-- members of that company.
|
|
CREATE POLICY "skattekonto_rules_select" ON public.skattekonto_rules
|
|
FOR SELECT USING (
|
|
company_id IS NULL
|
|
OR company_id IN (SELECT public.user_company_ids())
|
|
);
|
|
|
|
-- Writes are scoped to companies the user belongs to. System seeds (company_id IS NULL)
|
|
-- cannot be mutated via RLS — the WITH CHECK forces a non-NULL company_id.
|
|
CREATE POLICY "skattekonto_rules_insert" ON public.skattekonto_rules
|
|
FOR INSERT WITH CHECK (
|
|
company_id IS NOT NULL
|
|
AND company_id IN (SELECT public.user_company_ids())
|
|
);
|
|
|
|
CREATE POLICY "skattekonto_rules_update" ON public.skattekonto_rules
|
|
FOR UPDATE USING (
|
|
company_id IS NOT NULL
|
|
AND company_id IN (SELECT public.user_company_ids())
|
|
)
|
|
WITH CHECK (
|
|
company_id IS NOT NULL
|
|
AND company_id IN (SELECT public.user_company_ids())
|
|
);
|
|
|
|
CREATE POLICY "skattekonto_rules_delete" ON public.skattekonto_rules
|
|
FOR DELETE USING (
|
|
company_id IS NOT NULL
|
|
AND company_id IN (SELECT public.user_company_ids())
|
|
);
|
|
|
|
CREATE TRIGGER skattekonto_rules_updated_at
|
|
BEFORE UPDATE ON public.skattekonto_rules
|
|
FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
|
|
|
|
-- ============================================================
|
|
-- System seeds
|
|
-- ============================================================
|
|
--
|
|
-- counter_account = '__PRIMARY_SEK__' on the in-/utbetalning rules so the resolver
|
|
-- looks up cash_accounts.is_primary = true for SEK rather than assuming 1930.
|
|
|
|
INSERT INTO public.skattekonto_rules (
|
|
company_id, priority, pattern, company_type, counter_account, counter_account_ef, label
|
|
) VALUES
|
|
(NULL, 10, 'inbetalning bokförd,inbetalning,överföring från bank', 'all',
|
|
'__PRIMARY_SEK__', NULL, 'Inbetalning till skattekonto'),
|
|
(NULL, 10, 'utbetalning,återbetalning', 'all',
|
|
'__PRIMARY_SEK__', NULL, 'Utbetalning från skattekonto'),
|
|
(NULL, 20, 'debiterad preliminärskatt,preliminärskatt,f-skatt,fskatt', 'all',
|
|
'2510', '2012', 'Preliminär skatt'),
|
|
(NULL, 20, 'arbetsgivaravgift,sociala avgifter,agi', 'all',
|
|
'2731', NULL, 'Arbetsgivaravgifter'),
|
|
(NULL, 20, 'avdragen skatt,personalskatt,a-skatt', 'all',
|
|
'2710', NULL, 'Avdragen skatt anställda'),
|
|
(NULL, 20, 'mervärdesskatt,moms,momsdeklaration', 'all',
|
|
'2650', NULL, 'Redovisningskonto för moms'),
|
|
(NULL, 25, 'skattetillägg,förseningsavgift', 'all',
|
|
'6992', NULL, 'Ej avdragsgilla skatteavgifter'),
|
|
(NULL, 30, 'kostnadsränta', 'all',
|
|
'8423', NULL, 'Kostnadsränta skattekonto'),
|
|
(NULL, 30, 'intäktsränta', 'all',
|
|
'8314', NULL, 'Intäktsränta skattekonto');
|
|
|
|
NOTIFY pgrst, 'reload schema';
|