Files
accounted/supabase/migrations/20260519154732_seed_default_cash_account.sql
Mattsson 8a6ce7093e feat: implement skattekonto drift detection and alerting (#525)
* 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.
2026-05-19 16:10:18 +02:00

104 lines
3.6 KiB
PL/PgSQL

-- Migration: seed default 1930 SEK cash_account for every company
--
-- Why this exists: the cash_accounts backfill in 20260519110000_cash_accounts.sql
-- only seeded rows from existing bank_connections. Companies that haven't
-- connected a bank via PSD2 yet, or that do their books from SIE imports + manual
-- entries only, end up with zero cash_accounts rows. Combined with the
-- ownership check on /api/reconciliation/bank/{run,unmatched-entries,status},
-- that means reconciliation became unreachable for those companies the moment we
-- removed the '1930' bypass.
--
-- Two paths covered:
-- 1. Backfill — insert (company_id, '1930', 'SEK', is_primary=true if no other
-- primary exists) for every company missing a 1930 row. Idempotent via
-- the (company_id, ledger_account) unique constraint.
-- 2. Forward-fill — extend public.create_company_with_owner() to seed the
-- same default row at company creation. Source 'manual' so it's clear
-- this isn't a PSD2-backed account; the user can re-map or remove it via
-- AccountPicker once a PSD2 connection is established.
--
-- Compliance: removes the last hidden bypass in the reconciliation routes that
-- the PR review (ASVS V8.2.1, ISO 27001:2022 A.8.3, SOC 2 CC6.6) flagged as
-- inconsistent authorization across cash accounts.
-- 1. Backfill: every company without a 1930 row gets one.
INSERT INTO public.cash_accounts (
company_id, ledger_account, currency, name, enabled, is_primary, source
)
SELECT
c.id,
'1930',
'SEK',
'Företagskonto (SEK)',
true,
-- Only flag as primary if no other primary exists for this company.
NOT EXISTS (
SELECT 1 FROM public.cash_accounts ca2
WHERE ca2.company_id = c.id AND ca2.is_primary = true
),
'manual'
FROM public.companies c
WHERE NOT EXISTS (
SELECT 1 FROM public.cash_accounts ca
WHERE ca.company_id = c.id AND ca.ledger_account = '1930'
)
ON CONFLICT (company_id, ledger_account) DO NOTHING;
-- 2. Forward-fill: bake the seed into company creation so the reconciliation
-- ownership check is always satisfiable.
CREATE OR REPLACE FUNCTION public.create_company_with_owner(
p_name text,
p_entity_type text,
p_set_active boolean DEFAULT true
)
RETURNS uuid
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_user_id uuid;
v_company_id uuid;
BEGIN
v_user_id := auth.uid();
IF v_user_id IS NULL THEN
RAISE EXCEPTION 'Not authenticated';
END IF;
IF p_entity_type NOT IN ('enskild_firma', 'aktiebolag') THEN
RAISE EXCEPTION 'Invalid entity_type: %', p_entity_type;
END IF;
INSERT INTO public.companies (name, entity_type, created_by)
VALUES (p_name, p_entity_type, v_user_id)
RETURNING id INTO v_company_id;
INSERT INTO public.company_members (company_id, user_id, role)
VALUES (v_company_id, v_user_id, 'owner');
-- Seed default 1930 SEK cash account so reconciliation routes work before
-- any PSD2 connection is established. is_primary so the __PRIMARY_SEK__
-- sentinel in skattekonto-booking resolves on day one.
INSERT INTO public.cash_accounts (
company_id, ledger_account, currency, name, enabled, is_primary, source
)
VALUES (
v_company_id, '1930', 'SEK', 'Företagskonto (SEK)', true, true, 'manual'
)
ON CONFLICT (company_id, ledger_account) DO NOTHING;
IF p_set_active THEN
INSERT INTO public.user_preferences (user_id, active_company_id)
VALUES (v_user_id, v_company_id)
ON CONFLICT (user_id)
DO UPDATE SET active_company_id = EXCLUDED.active_company_id;
END IF;
RETURN v_company_id;
END;
$$;
GRANT EXECUTE ON FUNCTION public.create_company_with_owner(text, text, boolean) TO authenticated;
NOTIFY pgrst, 'reload schema';