279 lines
8.4 KiB
PL/PgSQL
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';
|