-- ============================================================================ -- SCHEMA: financial -- TABLE: wallet_transactions -- DESCRIPTION: Registro de todas las transacciones de wallet -- VERSION: 1.0.0 -- CREATED: 2026-01-10 -- ============================================================================ -- Enum para tipo de transaccion DO $$ BEGIN CREATE TYPE financial.transaction_type AS ENUM ( 'CREDIT_PURCHASE', -- Compra de creditos con tarjeta 'AGENT_FUNDING', -- Fondeo a Money Manager 'AGENT_WITHDRAWAL', -- Retiro de Money Manager 'AGENT_PROFIT', -- Ganancia de Money Manager 'AGENT_LOSS', -- Perdida de Money Manager 'PRODUCT_PURCHASE', -- Compra de producto (one-time) 'SUBSCRIPTION_CHARGE', -- Cargo de suscripcion 'PREDICTION_PURCHASE', -- Compra de prediccion individual 'REFUND', -- Devolucion 'PROMO_CREDIT', -- Creditos promocionales agregados 'PROMO_EXPIRY', -- Expiracion de creditos promo 'ADJUSTMENT', -- Ajuste manual (admin) 'TRANSFER_IN', -- Transferencia recibida 'TRANSFER_OUT', -- Transferencia enviada 'FEE' -- Comision de plataforma ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para estado de transaccion DO $$ BEGIN CREATE TYPE financial.transaction_status AS ENUM ( 'pending', -- Pendiente de confirmacion 'completed', -- Completada exitosamente 'failed', -- Fallida 'cancelled', -- Cancelada 'reversed' -- Revertida ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de transacciones CREATE TABLE IF NOT EXISTS financial.wallet_transactions ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), wallet_id UUID NOT NULL REFERENCES financial.wallets(id) ON DELETE RESTRICT, tenant_id UUID NOT NULL, -- Tipo y estado type financial.transaction_type NOT NULL, status financial.transaction_status NOT NULL DEFAULT 'completed', -- Montos amount DECIMAL(15, 4) NOT NULL, balance_before DECIMAL(15, 4) NOT NULL, balance_after DECIMAL(15, 4) NOT NULL, -- Descripcion description TEXT, -- Referencias a entidades relacionadas reference_type VARCHAR(50), reference_id UUID, -- Para compras de creditos (Stripe) stripe_payment_intent_id VARCHAR(255), stripe_charge_id VARCHAR(255), payment_method VARCHAR(50), -- Para transferencias entre wallets related_wallet_id UUID REFERENCES financial.wallets(id), related_transaction_id UUID, -- Metadata adicional metadata JSONB DEFAULT '{}', -- Auditoria ip_address INET, user_agent TEXT, created_by UUID, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), processed_at TIMESTAMPTZ, -- Indices parciales para busquedas frecuentes CONSTRAINT valid_amount CHECK ( (type IN ('CREDIT_PURCHASE', 'AGENT_PROFIT', 'PROMO_CREDIT', 'REFUND', 'TRANSFER_IN') AND amount > 0) OR (type IN ('AGENT_FUNDING', 'PRODUCT_PURCHASE', 'SUBSCRIPTION_CHARGE', 'PREDICTION_PURCHASE', 'PROMO_EXPIRY', 'TRANSFER_OUT', 'FEE', 'AGENT_LOSS') AND amount < 0) OR (type IN ('ADJUSTMENT', 'AGENT_WITHDRAWAL')) ) ); COMMENT ON TABLE financial.wallet_transactions IS 'Registro inmutable de todas las transacciones de wallet. Cada operacion crea un registro.'; -- Indices CREATE INDEX IF NOT EXISTS idx_wallet_tx_wallet_id ON financial.wallet_transactions(wallet_id); CREATE INDEX IF NOT EXISTS idx_wallet_tx_tenant_id ON financial.wallet_transactions(tenant_id); CREATE INDEX IF NOT EXISTS idx_wallet_tx_type ON financial.wallet_transactions(type); CREATE INDEX IF NOT EXISTS idx_wallet_tx_status ON financial.wallet_transactions(status); CREATE INDEX IF NOT EXISTS idx_wallet_tx_created_at ON financial.wallet_transactions(created_at DESC); CREATE INDEX IF NOT EXISTS idx_wallet_tx_reference ON financial.wallet_transactions(reference_type, reference_id) WHERE reference_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_wallet_tx_stripe ON financial.wallet_transactions(stripe_payment_intent_id) WHERE stripe_payment_intent_id IS NOT NULL; -- Funcion para crear transaccion y actualizar wallet atomicamente CREATE OR REPLACE FUNCTION financial.create_wallet_transaction( p_wallet_id UUID, p_type financial.transaction_type, p_amount DECIMAL(15, 4), p_description TEXT DEFAULT NULL, p_reference_type VARCHAR(50) DEFAULT NULL, p_reference_id UUID DEFAULT NULL, p_metadata JSONB DEFAULT '{}' ) RETURNS UUID AS $$ DECLARE v_wallet financial.wallets%ROWTYPE; v_transaction_id UUID; v_new_balance DECIMAL(15, 4); BEGIN -- Lock wallet row SELECT * INTO v_wallet FROM financial.wallets WHERE id = p_wallet_id FOR UPDATE; IF NOT FOUND THEN RAISE EXCEPTION 'Wallet not found: %', p_wallet_id; END IF; IF v_wallet.status != 'active' THEN RAISE EXCEPTION 'Wallet is not active: %', v_wallet.status; END IF; -- Calcular nuevo balance v_new_balance := v_wallet.balance + p_amount; -- Validar que no quede negativo IF v_new_balance < 0 THEN RAISE EXCEPTION 'Insufficient balance. Current: %, Required: %', v_wallet.balance, ABS(p_amount); END IF; -- Validar limites si es debito IF p_amount < 0 THEN IF ABS(p_amount) > v_wallet.single_transaction_limit THEN RAISE EXCEPTION 'Exceeds single transaction limit: %', v_wallet.single_transaction_limit; END IF; IF v_wallet.daily_spent + ABS(p_amount) > v_wallet.daily_spend_limit THEN RAISE EXCEPTION 'Exceeds daily spend limit: %', v_wallet.daily_spend_limit; END IF; END IF; -- Crear transaccion INSERT INTO financial.wallet_transactions ( wallet_id, tenant_id, type, amount, balance_before, balance_after, description, reference_type, reference_id, metadata ) VALUES ( p_wallet_id, v_wallet.tenant_id, p_type, p_amount, v_wallet.balance, v_new_balance, p_description, p_reference_type, p_reference_id, p_metadata ) RETURNING id INTO v_transaction_id; -- Actualizar wallet UPDATE financial.wallets SET balance = v_new_balance, total_credited = CASE WHEN p_amount > 0 THEN total_credited + p_amount ELSE total_credited END, total_debited = CASE WHEN p_amount < 0 THEN total_debited + ABS(p_amount) ELSE total_debited END, daily_spent = CASE WHEN p_amount < 0 THEN daily_spent + ABS(p_amount) ELSE daily_spent END, monthly_spent = CASE WHEN p_amount < 0 THEN monthly_spent + ABS(p_amount) ELSE monthly_spent END, last_transaction_at = NOW() WHERE id = p_wallet_id; RETURN v_transaction_id; END; $$ LANGUAGE plpgsql; -- RLS Policy ALTER TABLE financial.wallet_transactions ENABLE ROW LEVEL SECURITY; CREATE POLICY wallet_tx_tenant_isolation ON financial.wallet_transactions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT ON financial.wallet_transactions TO trading_app; GRANT SELECT ON financial.wallet_transactions TO trading_readonly;