Files
accounted/supabase/migrations/20260409140000_fix_delete_user_account_timeout.sql
T
Jakob WennbergandClaude Opus 4.6 bca00cc5bd fix: BankID fallback, bank import fixes, account deletion (#216)
* fix: BankID graceful fallback, bank import fixes, and account deletion

- BankID: surface service_unavailable state in login/register with password
  fallback messaging; structured error codes in tic extension; poll failure
  counter in BankIdAuth avoids infinite retry when TIC API is down.
- Transactions: allow deleting unbooked bank-synced and imported transactions
  (only posted entries remain protected); detect reconnect duplicates by also
  checking unbooked bank-synced rows in content-based dedup.
- Enable Banking: key external_id by account iban/uid instead of connection id
  so reconnects don't create duplicates.
- Bank import: detect SEB privatbanken CSV variant (Bokföringsdatum /
  Valutadatum headers) via regex.
- Banking settings: replace full-screen sync loader with toast notifications.
- delete_user_account: raise statement_timeout, pre-clear NO ACTION FK
  references, and disable audit/immutability triggers during CASCADE.

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

* fix: address Greptile review — ingest dedup scoping and migration EXCEPTION handler

- ingest: split buildExistingTransactionMap into two maps. Booked rows
  (any source) remain consumed by any incoming raw transaction, but
  unbooked enable_banking slots are only consumed when the incoming raw
  transaction is also enable_banking. This preserves reconnect dedup
  while preventing false positives where a pending bank-synced row
  silently blocks a legitimately separate CSV row with the same
  date/amount.
- delete_user_account: add EXCEPTION WHEN OTHERS handler that re-enables
  every legally required enforcement trigger before re-raising. Postgres
  transactional DDL already rolls back on abort, but the explicit guard
  makes the intent unambiguous and covers sub-transaction edge cases so
  enforcement triggers are never left disabled.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 13:02:50 +02:00

117 lines
6.4 KiB
PL/PgSQL

-- Fix delete_user_account: timeout + cascade blockers.
--
-- Problem 1: ALTER TABLE DDL requires ACCESS EXCLUSIVE locks. With the
-- authenticated role's 8s statement_timeout, any concurrent read causes
-- error 57014. Fixed via function-level SET statement_timeout = '60s'.
--
-- Problem 2: Several NO ACTION FKs block the CASCADE chain from
-- auth.users → companies → children:
-- audit_log.company_id → companies (NO ACTION)
-- fiscal_periods.previous_period_id → fiscal_periods (NO ACTION, self-ref)
-- fiscal_periods.closing_entry_id → journal_entries (NO ACTION)
-- fiscal_periods.opening_balance_entry_id → journal_entries (NO ACTION)
-- Fixed by explicitly clearing these before the CASCADE fires.
CREATE OR REPLACE FUNCTION public.delete_user_account(target_user_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
SET statement_timeout = '60s'
SET lock_timeout = '10s'
AS $$
DECLARE
v_company_ids uuid[];
BEGIN
-- Only allow users to delete their own account
IF auth.uid() IS DISTINCT FROM target_user_id THEN
RAISE EXCEPTION 'Can only delete your own account';
END IF;
-- Collect companies owned by this user (CASCADE will delete these)
SELECT array_agg(id) INTO v_company_ids
FROM public.companies
WHERE created_by = target_user_id;
-- Clear active_company_id to avoid FK conflicts during CASCADE
DELETE FROM public.user_preferences WHERE user_id = target_user_id;
-- Explicitly delete from extension_data (missing DELETE RLS policy
-- causes CASCADE from auth.users to fail even with ON DELETE CASCADE)
DELETE FROM public.extension_data WHERE user_id = target_user_id;
-- Disable BEFORE DELETE triggers that block deletion
ALTER TABLE audit_log DISABLE TRIGGER audit_log_no_delete;
ALTER TABLE payment_match_log DISABLE TRIGGER payment_match_log_no_delete;
ALTER TABLE document_attachments DISABLE TRIGGER block_document_deletion;
ALTER TABLE journal_entries DISABLE TRIGGER enforce_journal_entry_immutability;
ALTER TABLE journal_entries DISABLE TRIGGER enforce_retention_journal_entries;
ALTER TABLE journal_entry_lines DISABLE TRIGGER enforce_journal_entry_line_immutability;
-- Disable AFTER DELETE audit triggers (they INSERT into audit_log during
-- CASCADE, which would create orphaned rows after the user is gone)
ALTER TABLE api_keys DISABLE TRIGGER audit_api_keys;
ALTER TABLE chart_of_accounts DISABLE TRIGGER audit_chart_of_accounts;
ALTER TABLE company_settings DISABLE TRIGGER audit_company_settings;
ALTER TABLE document_attachments DISABLE TRIGGER audit_document_attachments;
ALTER TABLE extension_data DISABLE TRIGGER audit_extension_data;
ALTER TABLE fiscal_periods DISABLE TRIGGER audit_fiscal_periods;
ALTER TABLE journal_entries DISABLE TRIGGER audit_journal_entries;
ALTER TABLE supplier_invoices DISABLE TRIGGER audit_supplier_invoices;
-- Clear NO ACTION FK references that block the CASCADE chain
IF v_company_ids IS NOT NULL THEN
-- audit_log.company_id → companies (NO ACTION)
DELETE FROM public.audit_log
WHERE company_id = ANY(v_company_ids);
-- fiscal_periods self-ref and cross-refs to journal_entries (NO ACTION)
UPDATE public.fiscal_periods
SET previous_period_id = NULL,
closing_entry_id = NULL,
opening_balance_entry_id = NULL
WHERE company_id = ANY(v_company_ids);
END IF;
-- Delete from auth.users — ON DELETE CASCADE handles all public tables
DELETE FROM auth.users WHERE id = target_user_id;
-- Re-enable all triggers
ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete;
ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete;
ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion;
ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability;
ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries;
ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability;
ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys;
ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts;
ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings;
ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments;
ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data;
ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods;
ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries;
ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices;
EXCEPTION WHEN OTHERS THEN
-- PostgreSQL transactional DDL already rolls back the DISABLE TRIGGER
-- statements if the function aborts, but re-enable explicitly as a
-- defensive guard against sub-transaction edge cases so enforcement
-- triggers are never left disabled on the live tables.
BEGIN ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries; EXCEPTION WHEN OTHERS THEN NULL; END;
BEGIN ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices; EXCEPTION WHEN OTHERS THEN NULL; END;
RAISE;
END;
$$;