trading-platform-database-v2/ddl/schemas/financial/002_wallet_transactions.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

213 lines
7.3 KiB
PL/PgSQL

-- ============================================================================
-- 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;