-- ============================================================================ -- FINANCIAL SCHEMA - Tabla: payment_methods -- ============================================================================ -- Metodos de pago guardados por usuarios para pagos recurrentes -- Integra con Stripe para almacenamiento seguro de tarjetas y cuentas -- ============================================================================ -- Enum para tipo de metodo de pago guardado DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'saved_payment_type' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'financial')) THEN CREATE TYPE financial.saved_payment_type AS ENUM ( 'card', 'bank_account', 'sepa_debit', 'crypto_wallet' ); END IF; END$$; -- Enum para estado del metodo de pago DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'payment_method_status' AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'financial')) THEN CREATE TYPE financial.payment_method_status AS ENUM ( 'pending_verification', 'active', 'expired', 'failed', 'removed' ); END IF; END$$; CREATE TABLE IF NOT EXISTS financial.payment_methods ( 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) ON DELETE SET NULL, -- Stripe integration stripe_payment_method_id VARCHAR(100) UNIQUE, stripe_fingerprint VARCHAR(100), -- Para detectar duplicados -- Tipo y estado payment_type financial.saved_payment_type NOT NULL, status financial.payment_method_status NOT NULL DEFAULT 'pending_verification', -- Informacion del metodo (datos no sensibles) -- Para tarjetas: last4, brand, exp_month, exp_year -- Para bancos: last4, bank_name, account_type display_info JSONB NOT NULL DEFAULT '{}', -- Metodo por defecto is_default BOOLEAN NOT NULL DEFAULT FALSE, -- Datos de tarjeta (solo informacion visible) card_brand VARCHAR(20), -- 'visa', 'mastercard', 'amex', etc. card_last4 VARCHAR(4), card_exp_month INTEGER, card_exp_year INTEGER, card_funding VARCHAR(20), -- 'credit', 'debit', 'prepaid' -- Datos de cuenta bancaria (solo informacion visible) bank_name VARCHAR(100), bank_last4 VARCHAR(4), bank_account_type VARCHAR(20), -- 'checking', 'savings' -- Datos de crypto wallet crypto_network VARCHAR(20), -- 'ethereum', 'bitcoin', 'polygon' crypto_address_last8 VARCHAR(8), -- Verificacion verified_at TIMESTAMPTZ, verification_method VARCHAR(50), -- 'micro_deposits', '3d_secure', 'instant' -- Billing address (para 3DS y validacion) billing_address JSONB DEFAULT '{}', -- Metadata metadata JSONB DEFAULT '{}', -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), expires_at TIMESTAMPTZ, -- Para tarjetas con fecha de expiracion removed_at TIMESTAMPTZ, -- Constraints CONSTRAINT chk_card_info CHECK ( payment_type != 'card' OR ( card_brand IS NOT NULL AND card_last4 IS NOT NULL AND card_exp_month IS NOT NULL AND card_exp_year IS NOT NULL ) ), CONSTRAINT chk_bank_info CHECK ( payment_type != 'bank_account' OR ( bank_name IS NOT NULL AND bank_last4 IS NOT NULL ) ), CONSTRAINT chk_crypto_info CHECK ( payment_type != 'crypto_wallet' OR ( crypto_network IS NOT NULL AND crypto_address_last8 IS NOT NULL ) ), CONSTRAINT chk_valid_exp_month CHECK ( card_exp_month IS NULL OR (card_exp_month >= 1 AND card_exp_month <= 12) ), CONSTRAINT chk_valid_exp_year CHECK ( card_exp_year IS NULL OR card_exp_year >= 2024 ) ); -- Indices CREATE INDEX idx_payment_methods_user ON financial.payment_methods(user_id); CREATE INDEX idx_payment_methods_customer ON financial.payment_methods(customer_id); CREATE INDEX idx_payment_methods_stripe ON financial.payment_methods(stripe_payment_method_id); CREATE INDEX idx_payment_methods_status ON financial.payment_methods(status); CREATE INDEX idx_payment_methods_default ON financial.payment_methods(user_id, is_default) WHERE is_default = TRUE; CREATE INDEX idx_payment_methods_fingerprint ON financial.payment_methods(stripe_fingerprint) WHERE stripe_fingerprint IS NOT NULL; CREATE INDEX idx_payment_methods_active ON financial.payment_methods(user_id, payment_type) WHERE status = 'active'; -- Comentarios COMMENT ON TABLE financial.payment_methods IS 'Metodos de pago guardados por usuarios con integracion Stripe'; COMMENT ON COLUMN financial.payment_methods.stripe_payment_method_id IS 'ID del PaymentMethod en Stripe'; COMMENT ON COLUMN financial.payment_methods.stripe_fingerprint IS 'Fingerprint para detectar tarjetas duplicadas'; COMMENT ON COLUMN financial.payment_methods.display_info IS 'Informacion visible del metodo para UI'; COMMENT ON COLUMN financial.payment_methods.is_default IS 'Metodo de pago por defecto del usuario'; COMMENT ON COLUMN financial.payment_methods.billing_address IS 'Direccion de facturacion para 3D Secure'; -- Trigger para asegurar un solo metodo por defecto por usuario CREATE OR REPLACE FUNCTION financial.ensure_single_default_payment_method() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_default = TRUE THEN UPDATE financial.payment_methods SET is_default = FALSE, updated_at = NOW() WHERE user_id = NEW.user_id AND id != NEW.id AND is_default = TRUE; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER tr_ensure_single_default_payment_method BEFORE INSERT OR UPDATE OF is_default ON financial.payment_methods FOR EACH ROW WHEN (NEW.is_default = TRUE) EXECUTE FUNCTION financial.ensure_single_default_payment_method(); -- Funcion para marcar tarjetas expiradas CREATE OR REPLACE FUNCTION financial.check_expired_cards() RETURNS INTEGER AS $$ DECLARE v_count INTEGER; BEGIN UPDATE financial.payment_methods SET status = 'expired', updated_at = NOW() WHERE payment_type = 'card' AND status = 'active' AND ( card_exp_year < EXTRACT(YEAR FROM CURRENT_DATE) OR (card_exp_year = EXTRACT(YEAR FROM CURRENT_DATE) AND card_exp_month < EXTRACT(MONTH FROM CURRENT_DATE)) ); GET DIAGNOSTICS v_count = ROW_COUNT; RETURN v_count; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION financial.check_expired_cards() IS 'Marca como expiradas las tarjetas vencidas. Ejecutar mensualmente.';