trading-platform-database/ddl/schemas/financial/functions/03-triggers.sql

279 lines
8.4 KiB
PL/PgSQL

-- =====================================================
-- ORBIQUANT IA - FINANCIAL SCHEMA TRIGGERS
-- =====================================================
-- Description: Automated triggers for data integrity and audit
-- Schema: financial
-- =====================================================
-- =====================================================
-- TRIGGER: Update timestamps
-- =====================================================
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';