--- 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 | null; default_payment_method_id: string | null; currency: string; metadata: Record | 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 | 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 | 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[]; invoice_pdf_url: string | null; hosted_invoice_url: string | null; metadata: Record | 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 | 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 | 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