-- ===================================================== -- SCHEMA: billing -- PROPÓSITO: Suscripciones SaaS, planes, pagos, facturación -- MÓDULOS: MGN-015 (Billing y Suscripciones) -- FECHA: 2025-11-24 -- ===================================================== -- NOTA: Este schema permite que el sistema opere como SaaS multi-tenant -- o como instalación single-tenant (on-premise). En modo single-tenant, -- las tablas de este schema pueden ignorarse o tener un único plan "unlimited". -- ===================================================== -- Crear schema CREATE SCHEMA IF NOT EXISTS billing; -- ===================================================== -- TYPES (ENUMs) -- ===================================================== CREATE TYPE billing.subscription_status AS ENUM ( 'trialing', -- En período de prueba 'active', -- Suscripción activa 'past_due', -- Pago atrasado 'paused', -- Suscripción pausada 'cancelled', -- Cancelada por usuario 'suspended', -- Suspendida por falta de pago 'expired' -- Expirada ); CREATE TYPE billing.billing_cycle AS ENUM ( 'monthly', 'quarterly', 'semi_annual', 'annual' ); CREATE TYPE billing.payment_method_type AS ENUM ( 'card', 'bank_transfer', 'paypal', 'oxxo', -- México 'spei', -- México 'other' ); CREATE TYPE billing.invoice_status AS ENUM ( 'draft', 'open', 'paid', 'void', 'uncollectible' ); CREATE TYPE billing.payment_status AS ENUM ( 'pending', 'processing', 'succeeded', 'failed', 'cancelled', 'refunded' ); -- ===================================================== -- TABLES -- ===================================================== -- Tabla: subscription_plans (Planes disponibles - global, no por tenant) -- Esta tabla no tiene tenant_id porque los planes son globales del sistema SaaS CREATE TABLE billing.subscription_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, -- Precios price_monthly DECIMAL(12,2) NOT NULL DEFAULT 0, price_yearly DECIMAL(12,2) NOT NULL DEFAULT 0, currency_code VARCHAR(3) NOT NULL DEFAULT 'MXN', -- Límites max_users INTEGER DEFAULT 10, max_companies INTEGER DEFAULT 1, max_storage_gb INTEGER DEFAULT 5, max_api_calls_month INTEGER DEFAULT 10000, -- Características incluidas (JSON para flexibilidad) features JSONB DEFAULT '{}'::jsonb, -- Ejemplo: {"inventory": true, "sales": true, "financial": true, "purchase": true, "crm": false} -- Metadata is_active BOOLEAN NOT NULL DEFAULT true, is_public BOOLEAN NOT NULL DEFAULT true, -- Visible en página de precios is_default BOOLEAN NOT NULL DEFAULT false, -- Plan por defecto para nuevos tenants trial_days INTEGER DEFAULT 14, sort_order INTEGER DEFAULT 0, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, updated_at TIMESTAMP, updated_by UUID, CONSTRAINT chk_plans_price_monthly CHECK (price_monthly >= 0), CONSTRAINT chk_plans_price_yearly CHECK (price_yearly >= 0), CONSTRAINT chk_plans_max_users CHECK (max_users > 0 OR max_users IS NULL), CONSTRAINT chk_plans_trial_days CHECK (trial_days >= 0) ); -- Tabla: tenant_owners (Propietarios/Contratantes de tenant) -- Usuario(s) que contratan y pagan por el tenant CREATE TABLE billing.tenant_owners ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, ownership_type VARCHAR(20) NOT NULL DEFAULT 'owner', -- owner: Propietario principal (puede haber solo 1) -- billing_admin: Puede gestionar facturación -- Contacto de facturación (puede diferir del usuario) billing_email VARCHAR(255), billing_phone VARCHAR(50), billing_name VARCHAR(255), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, CONSTRAINT uq_tenant_owners UNIQUE (tenant_id, user_id), CONSTRAINT chk_ownership_type CHECK (ownership_type IN ('owner', 'billing_admin')) ); -- Tabla: subscriptions (Suscripciones activas de cada tenant) CREATE TABLE billing.subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, plan_id UUID NOT NULL REFERENCES billing.subscription_plans(id), -- Estado status billing.subscription_status NOT NULL DEFAULT 'trialing', billing_cycle billing.billing_cycle NOT NULL DEFAULT 'monthly', -- Fechas importantes trial_start_at TIMESTAMP, trial_end_at TIMESTAMP, current_period_start TIMESTAMP NOT NULL, current_period_end TIMESTAMP NOT NULL, cancelled_at TIMESTAMP, cancel_at_period_end BOOLEAN NOT NULL DEFAULT false, paused_at TIMESTAMP, -- Descuentos/Cupones discount_percent DECIMAL(5,2) DEFAULT 0, coupon_code VARCHAR(50), -- Integración pasarela de pago stripe_subscription_id VARCHAR(255), stripe_customer_id VARCHAR(255), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, updated_at TIMESTAMP, updated_by UUID, CONSTRAINT uq_subscriptions_tenant UNIQUE (tenant_id), -- Solo 1 suscripción activa por tenant CONSTRAINT chk_subscriptions_discount CHECK (discount_percent >= 0 AND discount_percent <= 100), CONSTRAINT chk_subscriptions_period CHECK (current_period_end > current_period_start) ); -- Tabla: payment_methods (Métodos de pago por tenant) CREATE TABLE billing.payment_methods ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, type billing.payment_method_type NOT NULL, is_default BOOLEAN NOT NULL DEFAULT false, -- Información de tarjeta (solo últimos 4 dígitos por seguridad) card_last_four VARCHAR(4), card_brand VARCHAR(20), -- visa, mastercard, amex card_exp_month INTEGER, card_exp_year INTEGER, -- Dirección de facturación billing_name VARCHAR(255), billing_email VARCHAR(255), billing_address_line1 VARCHAR(255), billing_address_line2 VARCHAR(255), billing_city VARCHAR(100), billing_state VARCHAR(100), billing_postal_code VARCHAR(20), billing_country VARCHAR(2), -- ISO 3166-1 alpha-2 -- Integración pasarela stripe_payment_method_id VARCHAR(255), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, updated_at TIMESTAMP, deleted_at TIMESTAMP, -- Soft delete CONSTRAINT chk_payment_methods_card_exp CHECK ( (type != 'card') OR (card_exp_month BETWEEN 1 AND 12 AND card_exp_year >= EXTRACT(YEAR FROM CURRENT_DATE)) ) ); -- Tabla: billing_invoices (Facturas de suscripción) CREATE TABLE billing.invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, subscription_id UUID REFERENCES billing.subscriptions(id), -- Número de factura invoice_number VARCHAR(50) NOT NULL, -- Estado y fechas status billing.invoice_status NOT NULL DEFAULT 'draft', period_start TIMESTAMP, period_end TIMESTAMP, due_date DATE NOT NULL, paid_at TIMESTAMP, voided_at TIMESTAMP, -- Montos subtotal DECIMAL(12,2) NOT NULL DEFAULT 0, tax_amount DECIMAL(12,2) NOT NULL DEFAULT 0, discount_amount DECIMAL(12,2) NOT NULL DEFAULT 0, total DECIMAL(12,2) NOT NULL DEFAULT 0, amount_paid DECIMAL(12,2) NOT NULL DEFAULT 0, amount_due DECIMAL(12,2) NOT NULL DEFAULT 0, currency_code VARCHAR(3) NOT NULL DEFAULT 'MXN', -- Datos fiscales del cliente customer_name VARCHAR(255), customer_tax_id VARCHAR(50), customer_email VARCHAR(255), customer_address TEXT, -- PDF y CFDI (México) pdf_url VARCHAR(500), cfdi_uuid VARCHAR(36), -- UUID del CFDI si aplica cfdi_xml_url VARCHAR(500), -- Integración pasarela stripe_invoice_id VARCHAR(255), -- Notas notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, updated_at TIMESTAMP, CONSTRAINT uq_invoices_number UNIQUE (invoice_number), CONSTRAINT chk_invoices_amounts CHECK (total >= 0 AND subtotal >= 0 AND amount_due >= 0) ); -- Tabla: invoice_lines (Líneas de detalle de factura) CREATE TABLE billing.invoice_lines ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE, description VARCHAR(255) NOT NULL, quantity DECIMAL(12,4) NOT NULL DEFAULT 1, unit_price DECIMAL(12,2) NOT NULL, amount DECIMAL(12,2) NOT NULL, -- Para facturación por uso period_start TIMESTAMP, period_end TIMESTAMP, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_invoice_lines_qty CHECK (quantity > 0), CONSTRAINT chk_invoice_lines_price CHECK (unit_price >= 0) ); -- Tabla: payments (Pagos recibidos) CREATE TABLE billing.payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, invoice_id UUID REFERENCES billing.invoices(id), payment_method_id UUID REFERENCES billing.payment_methods(id), -- Monto y moneda amount DECIMAL(12,2) NOT NULL, currency_code VARCHAR(3) NOT NULL DEFAULT 'MXN', -- Estado status billing.payment_status NOT NULL DEFAULT 'pending', -- Fechas paid_at TIMESTAMP, failed_at TIMESTAMP, refunded_at TIMESTAMP, -- Detalles del error (si falló) failure_reason VARCHAR(255), failure_code VARCHAR(50), -- Referencia de transacción transaction_id VARCHAR(255), stripe_payment_intent_id VARCHAR(255), -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_payments_amount CHECK (amount > 0) ); -- Tabla: usage_records (Registros de uso para billing por consumo) CREATE TABLE billing.usage_records ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, subscription_id UUID REFERENCES billing.subscriptions(id), -- Tipo de métrica metric_type VARCHAR(50) NOT NULL, -- Ejemplos: 'users', 'storage_gb', 'api_calls', 'invoices_sent', 'emails_sent' quantity DECIMAL(12,4) NOT NULL, billing_period DATE NOT NULL, -- Mes de facturación (YYYY-MM-01) -- Auditoría recorded_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT chk_usage_quantity CHECK (quantity >= 0) ); -- Tabla: coupons (Cupones de descuento) CREATE TABLE billing.coupons ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, -- Tipo de descuento discount_type VARCHAR(20) NOT NULL DEFAULT 'percent', -- 'percent': Porcentaje de descuento -- 'fixed': Monto fijo de descuento discount_value DECIMAL(12,2) NOT NULL, currency_code VARCHAR(3) DEFAULT 'MXN', -- Solo para tipo 'fixed' -- Restricciones max_redemptions INTEGER, -- Máximo de usos totales max_redemptions_per_tenant INTEGER DEFAULT 1, -- Máximo por tenant redemptions_count INTEGER NOT NULL DEFAULT 0, -- Vigencia valid_from TIMESTAMP, valid_until TIMESTAMP, -- Aplicable a applicable_plans UUID[], -- Array de plan_ids, NULL = todos -- Estado is_active BOOLEAN NOT NULL DEFAULT true, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID, CONSTRAINT chk_coupons_discount CHECK ( (discount_type = 'percent' AND discount_value > 0 AND discount_value <= 100) OR (discount_type = 'fixed' AND discount_value > 0) ), CONSTRAINT chk_coupons_dates CHECK (valid_until IS NULL OR valid_until > valid_from) ); -- Tabla: coupon_redemptions (Uso de cupones) CREATE TABLE billing.coupon_redemptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), coupon_id UUID NOT NULL REFERENCES billing.coupons(id), tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, subscription_id UUID REFERENCES billing.subscriptions(id), -- Auditoría redeemed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, redeemed_by UUID, CONSTRAINT uq_coupon_redemptions UNIQUE (coupon_id, tenant_id) ); -- Tabla: subscription_history (Historial de cambios de suscripción) CREATE TABLE billing.subscription_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), subscription_id UUID NOT NULL REFERENCES billing.subscriptions(id) ON DELETE CASCADE, event_type VARCHAR(50) NOT NULL, -- 'created', 'upgraded', 'downgraded', 'renewed', 'cancelled', -- 'paused', 'resumed', 'payment_failed', 'payment_succeeded' previous_plan_id UUID REFERENCES billing.subscription_plans(id), new_plan_id UUID REFERENCES billing.subscription_plans(id), previous_status billing.subscription_status, new_status billing.subscription_status, -- Metadata adicional metadata JSONB DEFAULT '{}'::jsonb, notes TEXT, -- Auditoría created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_by UUID ); -- ===================================================== -- ÍNDICES -- ===================================================== -- subscription_plans CREATE INDEX idx_plans_is_active ON billing.subscription_plans(is_active) WHERE is_active = true; CREATE INDEX idx_plans_is_public ON billing.subscription_plans(is_public) WHERE is_public = true; -- tenant_owners CREATE INDEX idx_tenant_owners_tenant_id ON billing.tenant_owners(tenant_id); CREATE INDEX idx_tenant_owners_user_id ON billing.tenant_owners(user_id); -- subscriptions CREATE INDEX idx_subscriptions_tenant_id ON billing.subscriptions(tenant_id); CREATE INDEX idx_subscriptions_status ON billing.subscriptions(status); CREATE INDEX idx_subscriptions_period_end ON billing.subscriptions(current_period_end); -- payment_methods CREATE INDEX idx_payment_methods_tenant_id ON billing.payment_methods(tenant_id); CREATE INDEX idx_payment_methods_default ON billing.payment_methods(tenant_id, is_default) WHERE is_default = true; -- invoices CREATE INDEX idx_invoices_tenant_id ON billing.invoices(tenant_id); CREATE INDEX idx_invoices_status ON billing.invoices(status); CREATE INDEX idx_invoices_due_date ON billing.invoices(due_date); CREATE INDEX idx_invoices_stripe_id ON billing.invoices(stripe_invoice_id); -- payments CREATE INDEX idx_payments_tenant_id ON billing.payments(tenant_id); CREATE INDEX idx_payments_status ON billing.payments(status); CREATE INDEX idx_payments_invoice_id ON billing.payments(invoice_id); -- usage_records CREATE INDEX idx_usage_records_tenant_id ON billing.usage_records(tenant_id); CREATE INDEX idx_usage_records_period ON billing.usage_records(billing_period); CREATE INDEX idx_usage_records_metric ON billing.usage_records(metric_type, billing_period); -- coupons CREATE INDEX idx_coupons_code ON billing.coupons(code); CREATE INDEX idx_coupons_active ON billing.coupons(is_active) WHERE is_active = true; -- subscription_history CREATE INDEX idx_subscription_history_subscription ON billing.subscription_history(subscription_id); CREATE INDEX idx_subscription_history_created ON billing.subscription_history(created_at); -- ===================================================== -- TRIGGERS -- ===================================================== -- Trigger updated_at para subscriptions CREATE TRIGGER trg_subscriptions_updated_at BEFORE UPDATE ON billing.subscriptions FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger updated_at para payment_methods CREATE TRIGGER trg_payment_methods_updated_at BEFORE UPDATE ON billing.payment_methods FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger updated_at para invoices CREATE TRIGGER trg_invoices_updated_at BEFORE UPDATE ON billing.invoices FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- Trigger updated_at para subscription_plans CREATE TRIGGER trg_plans_updated_at BEFORE UPDATE ON billing.subscription_plans FOR EACH ROW EXECUTE FUNCTION auth.update_updated_at_column(); -- ===================================================== -- FUNCIONES -- ===================================================== -- Función para obtener el plan actual de un tenant CREATE OR REPLACE FUNCTION billing.get_tenant_plan(p_tenant_id UUID) RETURNS TABLE( plan_code VARCHAR, plan_name VARCHAR, max_users INTEGER, max_companies INTEGER, features JSONB, subscription_status billing.subscription_status, days_until_renewal INTEGER ) AS $$ BEGIN RETURN QUERY SELECT sp.code, sp.name, sp.max_users, sp.max_companies, sp.features, s.status, EXTRACT(DAY FROM s.current_period_end - CURRENT_TIMESTAMP)::INTEGER FROM billing.subscriptions s JOIN billing.subscription_plans sp ON s.plan_id = sp.id WHERE s.tenant_id = p_tenant_id; END; $$ LANGUAGE plpgsql; -- Función para verificar si tenant puede agregar más usuarios CREATE OR REPLACE FUNCTION billing.can_add_user(p_tenant_id UUID) RETURNS BOOLEAN AS $$ DECLARE v_max_users INTEGER; v_current_users INTEGER; BEGIN -- Obtener límite del plan SELECT sp.max_users INTO v_max_users FROM billing.subscriptions s JOIN billing.subscription_plans sp ON s.plan_id = sp.id WHERE s.tenant_id = p_tenant_id AND s.status IN ('active', 'trialing'); -- Si no hay límite (NULL), permitir IF v_max_users IS NULL THEN RETURN true; END IF; -- Contar usuarios actuales SELECT COUNT(*) INTO v_current_users FROM auth.users WHERE tenant_id = p_tenant_id AND deleted_at IS NULL; RETURN v_current_users < v_max_users; END; $$ LANGUAGE plpgsql; -- Función para verificar si una feature está habilitada para el tenant CREATE OR REPLACE FUNCTION billing.has_feature(p_tenant_id UUID, p_feature VARCHAR) RETURNS BOOLEAN AS $$ DECLARE v_features JSONB; BEGIN SELECT sp.features INTO v_features FROM billing.subscriptions s JOIN billing.subscription_plans sp ON s.plan_id = sp.id WHERE s.tenant_id = p_tenant_id AND s.status IN ('active', 'trialing'); -- Si no hay plan o features, denegar IF v_features IS NULL THEN RETURN false; END IF; -- Verificar feature RETURN COALESCE((v_features ->> p_feature)::boolean, false); END; $$ LANGUAGE plpgsql; -- ===================================================== -- DATOS INICIALES (Plans por defecto) -- ===================================================== -- Plan Free/Trial INSERT INTO billing.subscription_plans (code, name, description, price_monthly, price_yearly, max_users, max_companies, max_storage_gb, trial_days, is_default, sort_order, features) VALUES ( 'free', 'Free / Trial', 'Plan gratuito para probar el sistema', 0, 0, 3, 1, 1, 14, true, 1, '{"inventory": true, "sales": true, "financial": false, "purchase": false, "crm": false, "projects": false, "reports_basic": true, "reports_advanced": false, "api_access": false}'::jsonb ); -- Plan Básico INSERT INTO billing.subscription_plans (code, name, description, price_monthly, price_yearly, max_users, max_companies, max_storage_gb, trial_days, sort_order, features) VALUES ( 'basic', 'Básico', 'Ideal para pequeños negocios', 499, 4990, 5, 1, 5, 14, 2, '{"inventory": true, "sales": true, "financial": true, "purchase": true, "crm": false, "projects": false, "reports_basic": true, "reports_advanced": false, "api_access": false}'::jsonb ); -- Plan Profesional INSERT INTO billing.subscription_plans (code, name, description, price_monthly, price_yearly, max_users, max_companies, max_storage_gb, trial_days, sort_order, features) VALUES ( 'professional', 'Profesional', 'Para empresas en crecimiento', 999, 9990, 15, 3, 20, 14, 3, '{"inventory": true, "sales": true, "financial": true, "purchase": true, "crm": true, "projects": true, "reports_basic": true, "reports_advanced": true, "api_access": true}'::jsonb ); -- Plan Enterprise INSERT INTO billing.subscription_plans (code, name, description, price_monthly, price_yearly, max_users, max_companies, max_storage_gb, trial_days, sort_order, features) VALUES ( 'enterprise', 'Enterprise', 'Solución completa para grandes empresas', 2499, 24990, NULL, NULL, 100, 30, 4, -- NULL = ilimitado '{"inventory": true, "sales": true, "financial": true, "purchase": true, "crm": true, "projects": true, "reports_basic": true, "reports_advanced": true, "api_access": true, "white_label": true, "priority_support": true, "custom_integrations": true}'::jsonb ); -- Plan Single-Tenant (para instalaciones on-premise) INSERT INTO billing.subscription_plans (code, name, description, price_monthly, price_yearly, max_users, max_companies, max_storage_gb, trial_days, is_public, sort_order, features) VALUES ( 'single_tenant', 'Single Tenant / On-Premise', 'Instalación dedicada sin restricciones', 0, 0, NULL, NULL, NULL, 0, false, 99, -- No público, solo asignación manual '{"inventory": true, "sales": true, "financial": true, "purchase": true, "crm": true, "projects": true, "reports_basic": true, "reports_advanced": true, "api_access": true, "white_label": true, "priority_support": true, "custom_integrations": true, "unlimited": true}'::jsonb ); -- ===================================================== -- COMENTARIOS -- ===================================================== COMMENT ON SCHEMA billing IS 'Schema para gestión de suscripciones SaaS, planes, pagos y facturación'; COMMENT ON TABLE billing.subscription_plans IS 'Planes de suscripción disponibles (global, no por tenant)'; COMMENT ON TABLE billing.tenant_owners IS 'Propietarios/administradores de facturación de cada tenant'; COMMENT ON TABLE billing.subscriptions IS 'Suscripciones activas de cada tenant'; COMMENT ON TABLE billing.payment_methods IS 'Métodos de pago registrados por tenant'; COMMENT ON TABLE billing.invoices IS 'Facturas de suscripción'; COMMENT ON TABLE billing.invoice_lines IS 'Líneas de detalle de facturas'; COMMENT ON TABLE billing.payments IS 'Pagos recibidos'; COMMENT ON TABLE billing.usage_records IS 'Registros de uso para billing por consumo'; COMMENT ON TABLE billing.coupons IS 'Cupones de descuento'; COMMENT ON TABLE billing.coupon_redemptions IS 'Registro de cupones usados'; COMMENT ON TABLE billing.subscription_history IS 'Historial de cambios de suscripción'; COMMENT ON FUNCTION billing.get_tenant_plan IS 'Obtiene información del plan actual de un tenant'; COMMENT ON FUNCTION billing.can_add_user IS 'Verifica si el tenant puede agregar más usuarios según su plan'; COMMENT ON FUNCTION billing.has_feature IS 'Verifica si una feature está habilitada para el tenant';