---
id: "ET-PAY-001"
title: "Modelo de Datos Financial"
type: "Technical Specification"
status: "Done"
priority: "Alta"
epic: "OQI-005"
project: "trading-platform"
version: "1.0.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---

# ET-PAY-001: Modelo de Datos Financial

**Epic:** OQI-005 Pagos y Stripe
**Versión:** 1.0
**Fecha:** 2025-12-05
**Responsable:** Requirements-Analyst

---

## 1. Descripción

Define el modelo de datos completo para el schema `financial` en PostgreSQL 15+, incluyendo:
- Pagos (one-time y recurrentes)
- Suscripciones
- Facturas
- Transacciones de wallet
- Reembolsos
- Métodos de pago

---

## 2. Arquitectura de Base de Datos

```
┌─────────────────────────────────────────────────────────────────┐
│                     SCHEMA: financial                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                   │
│  ┌──────────────┐         ┌─────────────────┐                   │
│  │   customers  │◄────────│    payments     │                   │
│  └──────────────┘         └─────────────────┘                   │
│         │                          │                             │
│         │                          ▼                             │
│         │                  ┌─────────────────┐                   │
│         │                  │    refunds      │                   │
│         │                  └─────────────────┘                   │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐         ┌─────────────────┐                   │
│  │subscriptions │────────►│    invoices     │                   │
│  └──────────────┘         └─────────────────┘                   │
│         │                                                         │
│         ▼                                                         │
│  ┌──────────────┐                                                │
│  │payment_methods│                                               │
│  └──────────────┘                                                │
│                                                                   │
│  ┌──────────────┐                                                │
│  │wallet_transactions                                            │
│  └──────────────┘                                                │
│                                                                   │
└─────────────────────────────────────────────────────────────────┘
```

---

## 3. Especificación de Tablas

### 3.1 Tabla: `customers`

Datos de clientes en Stripe.

```sql
CREATE TABLE financial.customers (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relación con usuario
    user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Stripe
    stripe_customer_id VARCHAR(255) NOT NULL UNIQUE,

    -- Información de facturación
    email VARCHAR(255) NOT NULL,
    name VARCHAR(255),
    phone VARCHAR(50),

    -- Dirección de facturación
    billing_address JSONB, -- { "line1", "line2", "city", "state", "postal_code", "country" }

    -- Configuración
    default_payment_method_id UUID REFERENCES financial.payment_methods(id),
    currency VARCHAR(3) DEFAULT 'usd',

    -- Metadata
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_currency CHECK (currency IN ('usd', 'eur', 'gbp'))
);

-- Índices
CREATE INDEX idx_customers_user_id ON financial.customers(user_id);
CREATE INDEX idx_customers_stripe_customer_id ON financial.customers(stripe_customer_id);
CREATE INDEX idx_customers_email ON financial.customers(email);

-- Trigger
CREATE TRIGGER update_customers_updated_at BEFORE UPDATE ON financial.customers
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

### 3.2 Tabla: `payment_methods`

Métodos de pago guardados del cliente.

```sql
CREATE TABLE financial.payment_methods (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relación
    customer_id UUID NOT NULL REFERENCES financial.customers(id) ON DELETE CASCADE,

    -- Stripe
    stripe_payment_method_id VARCHAR(255) NOT NULL UNIQUE,

    -- Tipo y detalles
    type VARCHAR(50) NOT NULL, -- 'card', 'bank_account', 'paypal'

    -- Para tarjetas
    card_brand VARCHAR(50), -- 'visa', 'mastercard', 'amex'
    card_last4 VARCHAR(4),
    card_exp_month INTEGER,
    card_exp_year INTEGER,
    card_country VARCHAR(2),

    -- Para cuentas bancarias
    bank_name VARCHAR(255),
    bank_last4 VARCHAR(4),
    bank_account_type VARCHAR(20), -- 'checking', 'savings'

    -- Estado
    is_default BOOLEAN DEFAULT false,
    is_active BOOLEAN DEFAULT true,

    -- Metadata
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_payment_method_type CHECK (type IN ('card', 'bank_account', 'paypal'))
);

-- Índices
CREATE INDEX idx_payment_methods_customer_id ON financial.payment_methods(customer_id);
CREATE INDEX idx_payment_methods_stripe_id ON financial.payment_methods(stripe_payment_method_id);
CREATE INDEX idx_payment_methods_is_default ON financial.payment_methods(customer_id, is_default) WHERE is_default = true;

-- Trigger
CREATE TRIGGER update_payment_methods_updated_at BEFORE UPDATE ON financial.payment_methods
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

### 3.3 Tabla: `payments`

Registro de todos los pagos procesados.

```sql
CREATE TABLE financial.payments (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relaciones
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    customer_id UUID REFERENCES financial.customers(id),

    -- Stripe
    stripe_payment_intent_id VARCHAR(255) UNIQUE,
    stripe_charge_id VARCHAR(255),

    -- Tipo de pago
    payment_type VARCHAR(50) NOT NULL, -- 'one_time', 'subscription', 'invoice', 'investment_deposit'

    -- Montos
    amount DECIMAL(15, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'usd',
    amount_refunded DECIMAL(15, 2) DEFAULT 0.00,
    net_amount DECIMAL(15, 2), -- amount - fees - refunds

    -- Fees
    application_fee DECIMAL(15, 2),
    stripe_fee DECIMAL(15, 2),

    -- Estado
    status VARCHAR(50) NOT NULL DEFAULT 'pending', -- 'pending', 'processing', 'succeeded', 'failed', 'canceled', 'refunded'

    -- Método de pago
    payment_method_id UUID REFERENCES financial.payment_methods(id),
    payment_method_type VARCHAR(50),

    -- Referencias
    subscription_id UUID REFERENCES financial.subscriptions(id),
    invoice_id UUID REFERENCES financial.invoices(id),

    -- Metadata
    description TEXT,
    metadata JSONB,
    failure_code VARCHAR(100),
    failure_message TEXT,

    -- Timestamps
    paid_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_amount_positive CHECK (amount > 0),
    CONSTRAINT chk_payment_status CHECK (status IN ('pending', 'processing', 'succeeded', 'failed', 'canceled', 'refunded')),
    CONSTRAINT chk_payment_type CHECK (payment_type IN ('one_time', 'subscription', 'invoice', 'investment_deposit'))
);

-- Índices
CREATE INDEX idx_payments_user_id ON financial.payments(user_id);
CREATE INDEX idx_payments_customer_id ON financial.payments(customer_id);
CREATE INDEX idx_payments_stripe_payment_intent ON financial.payments(stripe_payment_intent_id);
CREATE INDEX idx_payments_status ON financial.payments(status);
CREATE INDEX idx_payments_created_at ON financial.payments(created_at DESC);
CREATE INDEX idx_payments_subscription_id ON financial.payments(subscription_id);
CREATE INDEX idx_payments_invoice_id ON financial.payments(invoice_id);

-- Trigger
CREATE TRIGGER update_payments_updated_at BEFORE UPDATE ON financial.payments
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

### 3.4 Tabla: `subscriptions`

Suscripciones recurrentes de usuarios.

```sql
CREATE TABLE financial.subscriptions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relaciones
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    customer_id UUID NOT NULL REFERENCES financial.customers(id),

    -- Stripe
    stripe_subscription_id VARCHAR(255) NOT NULL UNIQUE,
    stripe_price_id VARCHAR(255) NOT NULL,
    stripe_product_id VARCHAR(255) NOT NULL,

    -- Plan
    plan_name VARCHAR(100) NOT NULL, -- 'basic', 'pro', 'enterprise'
    plan_interval VARCHAR(20) NOT NULL, -- 'month', 'year'

    -- Precio
    amount DECIMAL(15, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'usd',

    -- Estado
    status VARCHAR(50) NOT NULL DEFAULT 'active', -- 'active', 'past_due', 'canceled', 'unpaid', 'trialing'

    -- Fechas del ciclo
    current_period_start TIMESTAMP WITH TIME ZONE NOT NULL,
    current_period_end TIMESTAMP WITH TIME ZONE NOT NULL,

    -- Trial
    trial_start TIMESTAMP WITH TIME ZONE,
    trial_end TIMESTAMP WITH TIME ZONE,

    -- Cancelación
    cancel_at_period_end BOOLEAN DEFAULT false,
    canceled_at TIMESTAMP WITH TIME ZONE,
    cancellation_reason TEXT,

    -- Metadata
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_subscription_status CHECK (status IN ('active', 'past_due', 'canceled', 'unpaid', 'trialing')),
    CONSTRAINT chk_plan_interval CHECK (plan_interval IN ('month', 'year'))
);

-- Índices
CREATE INDEX idx_subscriptions_user_id ON financial.subscriptions(user_id);
CREATE INDEX idx_subscriptions_customer_id ON financial.subscriptions(customer_id);
CREATE INDEX idx_subscriptions_stripe_id ON financial.subscriptions(stripe_subscription_id);
CREATE INDEX idx_subscriptions_status ON financial.subscriptions(status);
CREATE INDEX idx_subscriptions_current_period_end ON financial.subscriptions(current_period_end);

-- Trigger
CREATE TRIGGER update_subscriptions_updated_at BEFORE UPDATE ON financial.subscriptions
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

### 3.5 Tabla: `invoices`

Facturas generadas para suscripciones y pagos.

```sql
CREATE TABLE financial.invoices (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relaciones
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
    customer_id UUID NOT NULL REFERENCES financial.customers(id),
    subscription_id UUID REFERENCES financial.subscriptions(id),

    -- Stripe
    stripe_invoice_id VARCHAR(255) NOT NULL UNIQUE,

    -- Número de factura
    invoice_number VARCHAR(100),

    -- Montos
    subtotal DECIMAL(15, 2) NOT NULL,
    tax DECIMAL(15, 2) DEFAULT 0.00,
    discount DECIMAL(15, 2) DEFAULT 0.00,
    total DECIMAL(15, 2) NOT NULL,
    amount_paid DECIMAL(15, 2) DEFAULT 0.00,
    amount_due DECIMAL(15, 2) NOT NULL,

    currency VARCHAR(3) NOT NULL DEFAULT 'usd',

    -- Estado
    status VARCHAR(50) NOT NULL DEFAULT 'draft', -- 'draft', 'open', 'paid', 'void', 'uncollectible'

    -- Fechas
    due_date TIMESTAMP WITH TIME ZONE,
    paid_at TIMESTAMP WITH TIME ZONE,

    -- Items
    line_items JSONB NOT NULL, -- Array de items de la factura

    -- PDF
    invoice_pdf_url TEXT,
    hosted_invoice_url TEXT,

    -- Metadata
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_invoice_status CHECK (status IN ('draft', 'open', 'paid', 'void', 'uncollectible'))
);

-- Índices
CREATE INDEX idx_invoices_user_id ON financial.invoices(user_id);
CREATE INDEX idx_invoices_customer_id ON financial.invoices(customer_id);
CREATE INDEX idx_invoices_subscription_id ON financial.invoices(subscription_id);
CREATE INDEX idx_invoices_stripe_id ON financial.invoices(stripe_invoice_id);
CREATE INDEX idx_invoices_status ON financial.invoices(status);
CREATE INDEX idx_invoices_created_at ON financial.invoices(created_at DESC);

-- Trigger
CREATE TRIGGER update_invoices_updated_at BEFORE UPDATE ON financial.invoices
    FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```

### 3.6 Tabla: `refunds`

Registro de reembolsos procesados.

```sql
CREATE TABLE financial.refunds (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relaciones
    payment_id UUID NOT NULL REFERENCES financial.payments(id) ON DELETE CASCADE,
    user_id UUID NOT NULL REFERENCES auth.users(id),

    -- Stripe
    stripe_refund_id VARCHAR(255) NOT NULL UNIQUE,

    -- Monto
    amount DECIMAL(15, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'usd',

    -- Razón
    reason VARCHAR(50), -- 'duplicate', 'fraudulent', 'requested_by_customer'
    description TEXT,

    -- Estado
    status VARCHAR(50) NOT NULL DEFAULT 'pending', -- 'pending', 'succeeded', 'failed', 'canceled'

    -- Metadata
    metadata JSONB,
    processed_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_refund_amount_positive CHECK (amount > 0),
    CONSTRAINT chk_refund_status CHECK (status IN ('pending', 'succeeded', 'failed', 'canceled')),
    CONSTRAINT chk_refund_reason CHECK (reason IN ('duplicate', 'fraudulent', 'requested_by_customer', 'other'))
);

-- Índices
CREATE INDEX idx_refunds_payment_id ON financial.refunds(payment_id);
CREATE INDEX idx_refunds_user_id ON financial.refunds(user_id);
CREATE INDEX idx_refunds_stripe_id ON financial.refunds(stripe_refund_id);
CREATE INDEX idx_refunds_status ON financial.refunds(status);

```

### 3.7 Tabla: `wallet_transactions`

Transacciones de wallet interno (créditos, bonos).

```sql
CREATE TABLE financial.wallet_transactions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

    -- Relación
    user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,

    -- Tipo
    type VARCHAR(50) NOT NULL, -- 'credit', 'debit', 'bonus', 'refund'

    -- Monto
    amount DECIMAL(15, 2) NOT NULL,
    currency VARCHAR(3) NOT NULL DEFAULT 'usd',

    -- Balance
    balance_before DECIMAL(15, 2) NOT NULL,
    balance_after DECIMAL(15, 2) NOT NULL,

    -- Referencia
    reference_type VARCHAR(50), -- 'payment', 'refund', 'admin_credit'
    reference_id UUID,

    -- Descripción
    description TEXT,

    -- Metadata
    metadata JSONB,
    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),

    -- Constraints
    CONSTRAINT chk_wallet_amount_positive CHECK (amount > 0),
    CONSTRAINT chk_wallet_type CHECK (type IN ('credit', 'debit', 'bonus', 'refund'))
);

-- Índices
CREATE INDEX idx_wallet_transactions_user_id ON financial.wallet_transactions(user_id);
CREATE INDEX idx_wallet_transactions_type ON financial.wallet_transactions(type);
CREATE INDEX idx_wallet_transactions_created_at ON financial.wallet_transactions(created_at DESC);
```

---

## 4. Interfaces TypeScript

### 4.1 Types del Modelo

```typescript
// src/types/financial.types.ts

export type PaymentStatus = 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled' | 'refunded';
export type PaymentType = 'one_time' | 'subscription' | 'invoice' | 'investment_deposit';
export type SubscriptionStatus = 'active' | 'past_due' | 'canceled' | 'unpaid' | 'trialing';
export type InvoiceStatus = 'draft' | 'open' | 'paid' | 'void' | 'uncollectible';
export type RefundReason = 'duplicate' | 'fraudulent' | 'requested_by_customer' | 'other';
export type WalletTransactionType = 'credit' | 'debit' | 'bonus' | 'refund';

export interface Customer {
  id: string;
  user_id: string;
  stripe_customer_id: string;
  email: string;
  name: string | null;
  phone: string | null;
  billing_address: Record<string, any> | null;
  default_payment_method_id: string | null;
  currency: string;
  metadata: Record<string, any> | null;
  created_at: string;
  updated_at: string;
}

export interface PaymentMethod {
  id: string;
  customer_id: string;
  stripe_payment_method_id: string;
  type: string;
  card_brand: string | null;
  card_last4: string | null;
  card_exp_month: number | null;
  card_exp_year: number | null;
  card_country: string | null;
  bank_name: string | null;
  bank_last4: string | null;
  bank_account_type: string | null;
  is_default: boolean;
  is_active: boolean;
  created_at: string;
  updated_at: string;
}

export interface Payment {
  id: string;
  user_id: string;
  customer_id: string | null;
  stripe_payment_intent_id: string | null;
  stripe_charge_id: string | null;
  payment_type: PaymentType;
  amount: number;
  currency: string;
  amount_refunded: number;
  net_amount: number | null;
  application_fee: number | null;
  stripe_fee: number | null;
  status: PaymentStatus;
  payment_method_id: string | null;
  payment_method_type: string | null;
  subscription_id: string | null;
  invoice_id: string | null;
  description: string | null;
  metadata: Record<string, any> | null;
  failure_code: string | null;
  failure_message: string | null;
  paid_at: string | null;
  created_at: string;
  updated_at: string;
}

export interface Subscription {
  id: string;
  user_id: string;
  customer_id: string;
  stripe_subscription_id: string;
  stripe_price_id: string;
  stripe_product_id: string;
  plan_name: string;
  plan_interval: string;
  amount: number;
  currency: string;
  status: SubscriptionStatus;
  current_period_start: string;
  current_period_end: string;
  trial_start: string | null;
  trial_end: string | null;
  cancel_at_period_end: boolean;
  canceled_at: string | null;
  cancellation_reason: string | null;
  metadata: Record<string, any> | null;
  created_at: string;
  updated_at: string;
}

export interface Invoice {
  id: string;
  user_id: string;
  customer_id: string;
  subscription_id: string | null;
  stripe_invoice_id: string;
  invoice_number: string | null;
  subtotal: number;
  tax: number;
  discount: number;
  total: number;
  amount_paid: number;
  amount_due: number;
  currency: string;
  status: InvoiceStatus;
  due_date: string | null;
  paid_at: string | null;
  line_items: Record<string, any>[];
  invoice_pdf_url: string | null;
  hosted_invoice_url: string | null;
  metadata: Record<string, any> | null;
  created_at: string;
  updated_at: string;
}

export interface Refund {
  id: string;
  payment_id: string;
  user_id: string;
  stripe_refund_id: string;
  amount: number;
  currency: string;
  reason: RefundReason | null;
  description: string | null;
  status: PaymentStatus;
  metadata: Record<string, any> | null;
  processed_at: string | null;
  created_at: string;
}

export interface WalletTransaction {
  id: string;
  user_id: string;
  type: WalletTransactionType;
  amount: number;
  currency: string;
  balance_before: number;
  balance_after: number;
  reference_type: string | null;
  reference_id: string | null;
  description: string | null;
  metadata: Record<string, any> | null;
  created_at: string;
}
```

---

## 5. Views Útiles

### 5.1 Vista: Payment Summary por Usuario

```sql
CREATE VIEW financial.user_payment_summary AS
SELECT
    user_id,
    COUNT(*) as total_payments,
    SUM(CASE WHEN status = 'succeeded' THEN 1 ELSE 0 END) as successful_payments,
    SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as total_amount_paid,
    MAX(created_at) as last_payment_date
FROM financial.payments
GROUP BY user_id;
```

---

## 6. Configuración

### 6.1 Variables de Entorno

```bash
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/trading

# Financial Schema
FINANCIAL_DEFAULT_CURRENCY=usd
```

---

## 7. Referencias

- Stripe API Objects Documentation
- PostgreSQL JSONB Best Practices
- Payment Gateway Database Design
