213 lines
7.3 KiB
PL/PgSQL
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;
|