-- ===================================================== -- ORBIQUANT IA - FINANCIAL SCHEMA TRIGGERS -- ===================================================== -- Description: Automated triggers for data integrity and audit -- Schema: financial -- ===================================================== -- ===================================================== -- TRIGGER: Update timestamps -- ===================================================== -- DEPRECATED: Use public.update_updated_at() instead -- Migration: migrations/2026-02-03_unify_common_functions.sql -- Issue: DUP-003 -- Task: ST-2.3 -- ===================================================== CREATE OR REPLACE FUNCTION financial.update_timestamp() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$; -- Apply to all tables with updated_at CREATE TRIGGER trigger_wallets_updated_at BEFORE UPDATE ON financial.wallets FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_transactions_updated_at BEFORE UPDATE ON financial.wallet_transactions FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_subscriptions_updated_at BEFORE UPDATE ON financial.subscriptions FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_payments_updated_at BEFORE UPDATE ON financial.payments FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_invoices_updated_at BEFORE UPDATE ON financial.invoices FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_exchange_rates_updated_at BEFORE UPDATE ON financial.currency_exchange_rates FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); CREATE TRIGGER trigger_wallet_limits_updated_at BEFORE UPDATE ON financial.wallet_limits FOR EACH ROW EXECUTE FUNCTION financial.update_timestamp(); -- ===================================================== -- TRIGGER: Auto-generate invoice number -- ===================================================== CREATE OR REPLACE FUNCTION financial.generate_invoice_number() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF NEW.invoice_number IS NULL THEN NEW.invoice_number := 'INV-' || TO_CHAR(NOW(), 'YYYYMM') || '-' || LPAD(nextval('financial.invoice_number_seq')::TEXT, 6, '0'); END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_invoice_number BEFORE INSERT ON financial.invoices FOR EACH ROW EXECUTE FUNCTION financial.generate_invoice_number(); -- ===================================================== -- TRIGGER: Validate wallet balance consistency -- ===================================================== CREATE OR REPLACE FUNCTION financial.validate_wallet_balance() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN -- Validar que balance = available + pending IF NEW.balance != (NEW.available_balance + NEW.pending_balance) THEN RAISE EXCEPTION 'Balance consistency error: balance (%) != available (%) + pending (%)', NEW.balance, NEW.available_balance, NEW.pending_balance; END IF; -- Validar que no haya negativos IF NEW.balance < 0 OR NEW.available_balance < 0 OR NEW.pending_balance < 0 THEN RAISE EXCEPTION 'Negative balance detected: balance=%, available=%, pending=%', NEW.balance, NEW.available_balance, NEW.pending_balance; END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_wallet_balance_validation BEFORE INSERT OR UPDATE ON financial.wallets FOR EACH ROW EXECUTE FUNCTION financial.validate_wallet_balance(); -- ===================================================== -- TRIGGER: Audit wallet status changes -- ===================================================== CREATE OR REPLACE FUNCTION financial.audit_wallet_status_change() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN -- Solo auditar si cambió el status IF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN INSERT INTO financial.wallet_audit_log ( wallet_id, action, actor_type, old_values, new_values, reason ) VALUES ( NEW.id, 'status_changed', 'system', jsonb_build_object('status', OLD.status), jsonb_build_object('status', NEW.status), 'Status changed from ' || OLD.status::TEXT || ' to ' || NEW.status::TEXT ); -- Si se cerró, registrar timestamp IF NEW.status = 'closed' AND NEW.closed_at IS NULL THEN NEW.closed_at := NOW(); END IF; END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_wallet_status_audit BEFORE UPDATE ON financial.wallets FOR EACH ROW EXECUTE FUNCTION financial.audit_wallet_status_change(); -- ===================================================== -- TRIGGER: Prevent modification of completed transactions -- ===================================================== CREATE OR REPLACE FUNCTION financial.protect_completed_transactions() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN IF OLD.status = 'completed' AND NEW.status != 'completed' THEN RAISE EXCEPTION 'Cannot modify completed transaction %', OLD.id; END IF; IF OLD.status = 'completed' AND ( OLD.amount != NEW.amount OR OLD.wallet_id != NEW.wallet_id OR OLD.transaction_type != NEW.transaction_type ) THEN RAISE EXCEPTION 'Cannot modify core fields of completed transaction %', OLD.id; END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_protect_completed_tx BEFORE UPDATE ON financial.wallet_transactions FOR EACH ROW EXECUTE FUNCTION financial.protect_completed_transactions(); -- ===================================================== -- TRIGGER: Set payment succeeded_at timestamp -- ===================================================== CREATE OR REPLACE FUNCTION financial.set_payment_timestamps() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN -- Set succeeded_at when status changes to succeeded IF NEW.status = 'succeeded' AND OLD.status != 'succeeded' THEN NEW.succeeded_at := NOW(); END IF; -- Set failed_at when status changes to failed IF NEW.status = 'failed' AND OLD.status != 'failed' THEN NEW.failed_at := NOW(); END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_payment_timestamps BEFORE UPDATE ON financial.payments FOR EACH ROW EXECUTE FUNCTION financial.set_payment_timestamps(); -- ===================================================== -- TRIGGER: Update subscription ended_at -- ===================================================== CREATE OR REPLACE FUNCTION financial.set_subscription_ended_at() RETURNS TRIGGER LANGUAGE plpgsql AS $$ BEGIN -- Set ended_at when status changes to cancelled and cancel_at_period_end is false IF NEW.status = 'cancelled' AND OLD.status != 'cancelled' AND NOT NEW.cancel_at_period_end AND NEW.ended_at IS NULL THEN NEW.ended_at := NOW(); END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_subscription_ended_at BEFORE UPDATE ON financial.subscriptions FOR EACH ROW EXECUTE FUNCTION financial.set_subscription_ended_at(); -- ===================================================== -- TRIGGER: Validate transaction currency matches wallet -- ===================================================== CREATE OR REPLACE FUNCTION financial.validate_transaction_currency() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE v_wallet_currency financial.currency_code; BEGIN -- Get wallet currency SELECT currency INTO v_wallet_currency FROM financial.wallets WHERE id = NEW.wallet_id; IF NOT FOUND THEN RAISE EXCEPTION 'Wallet % not found', NEW.wallet_id; END IF; -- Validate currency match IF NEW.currency != v_wallet_currency THEN RAISE EXCEPTION 'Transaction currency (%) does not match wallet currency (%)', NEW.currency, v_wallet_currency; END IF; RETURN NEW; END; $$; CREATE TRIGGER trigger_transaction_currency_validation BEFORE INSERT ON financial.wallet_transactions FOR EACH ROW EXECUTE FUNCTION financial.validate_transaction_currency(); COMMENT ON FUNCTION financial.update_timestamp IS 'Auto-update updated_at timestamp'; COMMENT ON FUNCTION financial.generate_invoice_number IS 'Auto-generate invoice number with format INV-YYYYMM-XXXXXX'; COMMENT ON FUNCTION financial.validate_wallet_balance IS 'Ensure balance = available + pending'; COMMENT ON FUNCTION financial.audit_wallet_status_change IS 'Log wallet status changes to audit log'; COMMENT ON FUNCTION financial.protect_completed_transactions IS 'Prevent modification of completed transactions'; COMMENT ON FUNCTION financial.set_payment_timestamps IS 'Auto-set succeeded_at and failed_at timestamps'; COMMENT ON FUNCTION financial.set_subscription_ended_at IS 'Auto-set ended_at when subscription is cancelled'; COMMENT ON FUNCTION financial.validate_transaction_currency IS 'Ensure transaction currency matches wallet currency';