117 lines
4.1 KiB
PL/PgSQL
117 lines
4.1 KiB
PL/PgSQL
-- Migration 18: Audit Logging Triggers
|
|
-- Generic audit log writer with AFTER triggers on compliance-critical tables
|
|
|
|
-- =============================================================================
|
|
-- 1. Generic write_audit_log() SECURITY DEFINER function
|
|
-- Detects action type from TG_OP and state transitions
|
|
-- =============================================================================
|
|
CREATE OR REPLACE FUNCTION public.write_audit_log()
|
|
RETURNS trigger
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
AS $$
|
|
DECLARE
|
|
v_user_id uuid;
|
|
v_action text;
|
|
v_old_state jsonb;
|
|
v_new_state jsonb;
|
|
v_record_id uuid;
|
|
v_desc text;
|
|
BEGIN
|
|
-- Determine user_id from the record
|
|
IF TG_OP = 'DELETE' THEN
|
|
v_user_id := OLD.user_id;
|
|
v_record_id := OLD.id;
|
|
v_old_state := to_jsonb(OLD);
|
|
v_new_state := NULL;
|
|
v_action := 'DELETE';
|
|
v_desc := 'Deleted ' || TG_TABLE_NAME || ' record';
|
|
ELSIF TG_OP = 'INSERT' THEN
|
|
v_user_id := NEW.user_id;
|
|
v_record_id := NEW.id;
|
|
v_old_state := NULL;
|
|
v_new_state := to_jsonb(NEW);
|
|
v_action := 'INSERT';
|
|
v_desc := 'Created ' || TG_TABLE_NAME || ' record';
|
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
v_user_id := COALESCE(NEW.user_id, OLD.user_id);
|
|
v_record_id := COALESCE(NEW.id, OLD.id);
|
|
v_old_state := to_jsonb(OLD);
|
|
v_new_state := to_jsonb(NEW);
|
|
v_action := 'UPDATE';
|
|
v_desc := 'Updated ' || TG_TABLE_NAME || ' record';
|
|
|
|
-- Detect specific state transitions for journal_entries
|
|
IF TG_TABLE_NAME = 'journal_entries' THEN
|
|
IF OLD.status = 'draft' AND NEW.status = 'posted' THEN
|
|
v_action := 'COMMIT';
|
|
v_desc := 'Committed journal entry ' || NEW.voucher_series || NEW.voucher_number;
|
|
ELSIF OLD.status = 'posted' AND NEW.status = 'reversed' THEN
|
|
v_action := 'REVERSE';
|
|
v_desc := 'Reversed journal entry ' || OLD.voucher_series || OLD.voucher_number;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Detect period lock/close
|
|
IF TG_TABLE_NAME = 'fiscal_periods' THEN
|
|
IF (OLD.locked_at IS NULL AND NEW.locked_at IS NOT NULL) THEN
|
|
v_action := 'LOCK_PERIOD';
|
|
v_desc := 'Locked fiscal period "' || NEW.name || '"';
|
|
ELSIF (NOT OLD.is_closed AND NEW.is_closed) THEN
|
|
v_action := 'CLOSE_PERIOD';
|
|
v_desc := 'Closed fiscal period "' || NEW.name || '"';
|
|
END IF;
|
|
END IF;
|
|
END IF;
|
|
|
|
-- Write to audit log (bypass RLS via SECURITY DEFINER)
|
|
INSERT INTO public.audit_log (user_id, action, table_name, record_id, actor_id, old_state, new_state, description)
|
|
VALUES (v_user_id, v_action, TG_TABLE_NAME, v_record_id, v_user_id, v_old_state, v_new_state, v_desc);
|
|
|
|
-- Return appropriate value
|
|
IF TG_OP = 'DELETE' THEN
|
|
RETURN OLD;
|
|
END IF;
|
|
RETURN NEW;
|
|
END;
|
|
$$;
|
|
|
|
-- =============================================================================
|
|
-- 2. AFTER triggers on compliance-critical tables
|
|
-- =============================================================================
|
|
|
|
-- journal_entries
|
|
CREATE TRIGGER audit_journal_entries
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.journal_entries
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- journal_entry_lines
|
|
CREATE TRIGGER audit_journal_entry_lines
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.journal_entry_lines
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- chart_of_accounts
|
|
CREATE TRIGGER audit_chart_of_accounts
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.chart_of_accounts
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- document_attachments
|
|
CREATE TRIGGER audit_document_attachments
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.document_attachments
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- fiscal_periods
|
|
CREATE TRIGGER audit_fiscal_periods
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.fiscal_periods
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- company_settings
|
|
CREATE TRIGGER audit_company_settings
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.company_settings
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|
|
|
|
-- tax_codes
|
|
CREATE TRIGGER audit_tax_codes
|
|
AFTER INSERT OR UPDATE OR DELETE ON public.tax_codes
|
|
FOR EACH ROW EXECUTE FUNCTION public.write_audit_log();
|