639 lines
22 KiB
PL/PgSQL
639 lines
22 KiB
PL/PgSQL
-- =====================================================
|
|
-- 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';
|