erp-core-database-v2/ddl/08-plans.sql
rckrdmrd 5043a640e4 refactor: Restructure DDL with numbered schema files
- Replace old DDL structure with new numbered files (01-24)
- Update migrations and seeds for new schema
- Clean up deprecated files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 00:40:32 -06:00

545 lines
21 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 08-plans.sql
-- DESCRIPCION: Extensiones de planes SaaS (features, limits, Stripe)
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- EPIC: SAAS-BILLING (EPIC-SAAS-002)
-- HISTORIAS: US-007, US-010, US-011, US-012
-- =============================================================
-- =====================
-- MODIFICACIONES A TABLAS EXISTENTES
-- =====================
-- Agregar columnas Stripe a tenant_subscriptions (US-007, US-008)
ALTER TABLE billing.tenant_subscriptions
ADD COLUMN IF NOT EXISTS stripe_customer_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS stripe_subscription_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS stripe_payment_method_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS stripe_price_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS canceled_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS cancel_at TIMESTAMPTZ;
-- Agregar columnas Stripe a subscription_plans
ALTER TABLE billing.subscription_plans
ADD COLUMN IF NOT EXISTS stripe_product_id VARCHAR(255),
ADD COLUMN IF NOT EXISTS stripe_price_id_monthly VARCHAR(255),
ADD COLUMN IF NOT EXISTS stripe_price_id_annual VARCHAR(255);
-- Agregar columnas Stripe a payment_methods
ALTER TABLE billing.payment_methods
ADD COLUMN IF NOT EXISTS stripe_payment_method_id VARCHAR(255);
-- Indices para campos Stripe
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_customer ON billing.tenant_subscriptions(stripe_customer_id) WHERE stripe_customer_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_sub ON billing.tenant_subscriptions(stripe_subscription_id) WHERE stripe_subscription_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_plans_stripe_product ON billing.subscription_plans(stripe_product_id) WHERE stripe_product_id IS NOT NULL;
-- =====================
-- TABLA: billing.plan_features
-- Features habilitadas por plan (US-010, US-011)
-- =====================
CREATE TABLE IF NOT EXISTS billing.plan_features (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plan_id UUID NOT NULL REFERENCES billing.subscription_plans(id) ON DELETE CASCADE,
-- Identificacion
feature_key VARCHAR(100) NOT NULL,
feature_name VARCHAR(255) NOT NULL,
category VARCHAR(100),
-- Estado
enabled BOOLEAN DEFAULT TRUE,
-- Configuracion
configuration JSONB DEFAULT '{}',
-- Metadata
description TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(plan_id, feature_key)
);
-- Indices para plan_features
CREATE INDEX IF NOT EXISTS idx_plan_features_plan ON billing.plan_features(plan_id);
CREATE INDEX IF NOT EXISTS idx_plan_features_key ON billing.plan_features(feature_key);
CREATE INDEX IF NOT EXISTS idx_plan_features_enabled ON billing.plan_features(plan_id, enabled) WHERE enabled = TRUE;
-- =====================
-- TABLA: billing.plan_limits
-- Limites cuantificables por plan (US-010, US-012)
-- =====================
CREATE TABLE IF NOT EXISTS billing.plan_limits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plan_id UUID NOT NULL REFERENCES billing.subscription_plans(id) ON DELETE CASCADE,
-- Identificacion
limit_key VARCHAR(100) NOT NULL,
limit_name VARCHAR(255) NOT NULL,
-- Valor
limit_value INTEGER NOT NULL,
limit_type VARCHAR(50) DEFAULT 'monthly', -- monthly, daily, total, per_user
-- Overage (si se permite exceder)
allow_overage BOOLEAN DEFAULT FALSE,
overage_unit_price DECIMAL(10,4) DEFAULT 0,
overage_currency VARCHAR(3) DEFAULT 'MXN',
-- Alertas
alert_threshold_percent INTEGER DEFAULT 80,
hard_limit BOOLEAN DEFAULT TRUE, -- Si true, bloquea; si false, solo alerta
-- Metadata
description TEXT,
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(plan_id, limit_key)
);
-- Indices para plan_limits
CREATE INDEX IF NOT EXISTS idx_plan_limits_plan ON billing.plan_limits(plan_id);
CREATE INDEX IF NOT EXISTS idx_plan_limits_key ON billing.plan_limits(limit_key);
-- =====================
-- TABLA: billing.coupons
-- Cupones de descuento
-- =====================
CREATE TABLE IF NOT EXISTS billing.coupons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identificacion
code VARCHAR(50) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
description TEXT,
-- Descuento
discount_type VARCHAR(20) NOT NULL CHECK (discount_type IN ('percentage', 'fixed')),
discount_value DECIMAL(10,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'MXN',
-- Aplicabilidad
applicable_plans UUID[] DEFAULT '{}', -- Vacio = todos
min_amount DECIMAL(10,2) DEFAULT 0,
-- Limites
max_redemptions INTEGER,
times_redeemed INTEGER DEFAULT 0,
max_redemptions_per_customer INTEGER DEFAULT 1,
-- Vigencia
valid_from TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
valid_until TIMESTAMPTZ,
-- Duracion del descuento
duration VARCHAR(20) DEFAULT 'once', -- once, forever, repeating
duration_months INTEGER, -- Si duration = repeating
-- Stripe
stripe_coupon_id VARCHAR(255),
-- Estado
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para coupons
CREATE INDEX IF NOT EXISTS idx_coupons_code ON billing.coupons(code);
CREATE INDEX IF NOT EXISTS idx_coupons_active ON billing.coupons(is_active, valid_until);
-- =====================
-- TABLA: billing.coupon_redemptions
-- Uso de cupones
-- =====================
CREATE TABLE IF NOT EXISTS 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),
subscription_id UUID REFERENCES billing.tenant_subscriptions(id),
-- Descuento aplicado
discount_amount DECIMAL(10,2) NOT NULL,
-- Timestamps
redeemed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ,
UNIQUE(coupon_id, tenant_id)
);
-- Indices para coupon_redemptions
CREATE INDEX IF NOT EXISTS idx_coupon_redemptions_coupon ON billing.coupon_redemptions(coupon_id);
CREATE INDEX IF NOT EXISTS idx_coupon_redemptions_tenant ON billing.coupon_redemptions(tenant_id);
-- =====================
-- TABLA: billing.stripe_events
-- Log de eventos de Stripe (US-008)
-- =====================
CREATE TABLE IF NOT EXISTS billing.stripe_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Evento de Stripe
stripe_event_id VARCHAR(255) NOT NULL UNIQUE,
event_type VARCHAR(100) NOT NULL,
api_version VARCHAR(20),
-- Datos
data JSONB NOT NULL,
-- Procesamiento
processed BOOLEAN DEFAULT FALSE,
processed_at TIMESTAMPTZ,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
-- Tenant relacionado (si aplica)
tenant_id UUID REFERENCES auth.tenants(id),
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para stripe_events
CREATE INDEX IF NOT EXISTS idx_stripe_events_type ON billing.stripe_events(event_type);
CREATE INDEX IF NOT EXISTS idx_stripe_events_processed ON billing.stripe_events(processed) WHERE processed = FALSE;
CREATE INDEX IF NOT EXISTS idx_stripe_events_tenant ON billing.stripe_events(tenant_id);
-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE billing.plan_features ENABLE ROW LEVEL SECURITY;
-- Plan features son globales, no requieren isolation
CREATE POLICY public_read_plan_features ON billing.plan_features
FOR SELECT USING (true);
ALTER TABLE billing.plan_limits ENABLE ROW LEVEL SECURITY;
-- Plan limits son globales, no requieren isolation
CREATE POLICY public_read_plan_limits ON billing.plan_limits
FOR SELECT USING (true);
ALTER TABLE billing.coupons ENABLE ROW LEVEL SECURITY;
-- Cupones son globales pero solo admins pueden modificar
CREATE POLICY public_read_coupons ON billing.coupons
FOR SELECT USING (is_active = TRUE);
ALTER TABLE billing.coupon_redemptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_coupon_redemptions ON billing.coupon_redemptions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.stripe_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_stripe_events ON billing.stripe_events
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid OR tenant_id IS NULL);
-- =====================
-- FUNCIONES
-- =====================
-- Funcion para verificar si un tenant tiene una feature habilitada
CREATE OR REPLACE FUNCTION billing.has_feature(
p_tenant_id UUID,
p_feature_key VARCHAR(100)
)
RETURNS BOOLEAN AS $$
DECLARE
v_enabled BOOLEAN;
BEGIN
SELECT pf.enabled INTO v_enabled
FROM billing.tenant_subscriptions ts
JOIN billing.plan_features pf ON pf.plan_id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status = 'active'
AND pf.feature_key = p_feature_key;
RETURN COALESCE(v_enabled, FALSE);
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para obtener limite de un tenant
CREATE OR REPLACE FUNCTION billing.get_limit(
p_tenant_id UUID,
p_limit_key VARCHAR(100)
)
RETURNS INTEGER AS $$
DECLARE
v_limit INTEGER;
BEGIN
SELECT pl.limit_value INTO v_limit
FROM billing.tenant_subscriptions ts
JOIN billing.plan_limits pl ON pl.plan_id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status = 'active'
AND pl.limit_key = p_limit_key;
RETURN COALESCE(v_limit, 0);
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para verificar si se puede usar una feature (con limite)
CREATE OR REPLACE FUNCTION billing.can_use_feature(
p_tenant_id UUID,
p_limit_key VARCHAR(100),
p_current_usage INTEGER DEFAULT 0
)
RETURNS TABLE (
allowed BOOLEAN,
limit_value INTEGER,
current_usage INTEGER,
remaining INTEGER,
allow_overage BOOLEAN,
overage_price DECIMAL
) AS $$
BEGIN
RETURN QUERY
SELECT
CASE
WHEN pl.hard_limit THEN p_current_usage < pl.limit_value
ELSE TRUE
END as allowed,
pl.limit_value,
p_current_usage as current_usage,
GREATEST(0, pl.limit_value - p_current_usage) as remaining,
pl.allow_overage,
pl.overage_unit_price
FROM billing.tenant_subscriptions ts
JOIN billing.plan_limits pl ON pl.plan_id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status = 'active'
AND pl.limit_key = p_limit_key;
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para obtener todas las features de un tenant
CREATE OR REPLACE FUNCTION billing.get_tenant_features(p_tenant_id UUID)
RETURNS TABLE (
feature_key VARCHAR(100),
feature_name VARCHAR(255),
enabled BOOLEAN,
configuration JSONB
) AS $$
BEGIN
RETURN QUERY
SELECT
pf.feature_key,
pf.feature_name,
pf.enabled,
pf.configuration
FROM billing.tenant_subscriptions ts
JOIN billing.plan_features pf ON pf.plan_id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status IN ('active', 'trial');
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para obtener todos los limites de un tenant
CREATE OR REPLACE FUNCTION billing.get_tenant_limits(p_tenant_id UUID)
RETURNS TABLE (
limit_key VARCHAR(100),
limit_name VARCHAR(255),
limit_value INTEGER,
limit_type VARCHAR(50),
allow_overage BOOLEAN,
overage_unit_price DECIMAL
) AS $$
BEGIN
RETURN QUERY
SELECT
pl.limit_key,
pl.limit_name,
pl.limit_value,
pl.limit_type,
pl.allow_overage,
pl.overage_unit_price
FROM billing.tenant_subscriptions ts
JOIN billing.plan_limits pl ON pl.plan_id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status IN ('active', 'trial');
END;
$$ LANGUAGE plpgsql STABLE;
-- Funcion para aplicar cupon
CREATE OR REPLACE FUNCTION billing.apply_coupon(
p_tenant_id UUID,
p_coupon_code VARCHAR(50)
)
RETURNS TABLE (
success BOOLEAN,
message TEXT,
discount_amount DECIMAL
) AS $$
DECLARE
v_coupon RECORD;
v_subscription RECORD;
v_discount DECIMAL;
BEGIN
-- Obtener cupon
SELECT * INTO v_coupon
FROM billing.coupons
WHERE code = p_coupon_code
AND is_active = TRUE
AND (valid_until IS NULL OR valid_until > CURRENT_TIMESTAMP);
IF NOT FOUND THEN
RETURN QUERY SELECT FALSE, 'Cupon no valido o expirado'::TEXT, 0::DECIMAL;
RETURN;
END IF;
-- Verificar max redemptions
IF v_coupon.max_redemptions IS NOT NULL AND v_coupon.times_redeemed >= v_coupon.max_redemptions THEN
RETURN QUERY SELECT FALSE, 'Cupon agotado'::TEXT, 0::DECIMAL;
RETURN;
END IF;
-- Verificar si ya fue usado por este tenant
IF EXISTS (SELECT 1 FROM billing.coupon_redemptions WHERE coupon_id = v_coupon.id AND tenant_id = p_tenant_id) THEN
RETURN QUERY SELECT FALSE, 'Cupon ya utilizado'::TEXT, 0::DECIMAL;
RETURN;
END IF;
-- Obtener suscripcion
SELECT * INTO v_subscription
FROM billing.tenant_subscriptions
WHERE tenant_id = p_tenant_id
AND status = 'active';
IF NOT FOUND THEN
RETURN QUERY SELECT FALSE, 'No hay suscripcion activa'::TEXT, 0::DECIMAL;
RETURN;
END IF;
-- Calcular descuento
IF v_coupon.discount_type = 'percentage' THEN
v_discount := v_subscription.current_price * (v_coupon.discount_value / 100);
ELSE
v_discount := v_coupon.discount_value;
END IF;
-- Registrar uso
INSERT INTO billing.coupon_redemptions (coupon_id, tenant_id, subscription_id, discount_amount)
VALUES (v_coupon.id, p_tenant_id, v_subscription.id, v_discount);
-- Actualizar contador
UPDATE billing.coupons SET times_redeemed = times_redeemed + 1 WHERE id = v_coupon.id;
RETURN QUERY SELECT TRUE, 'Cupon aplicado correctamente'::TEXT, v_discount;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- TRIGGERS
-- =====================
-- Trigger para updated_at en plan_features
CREATE OR REPLACE FUNCTION billing.update_plan_features_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_plan_features_updated_at
BEFORE UPDATE ON billing.plan_features
FOR EACH ROW
EXECUTE FUNCTION billing.update_plan_features_timestamp();
CREATE TRIGGER trg_plan_limits_updated_at
BEFORE UPDATE ON billing.plan_limits
FOR EACH ROW
EXECUTE FUNCTION billing.update_plan_features_timestamp();
-- =====================
-- SEED DATA: Features por Plan
-- =====================
-- Features para plan Starter
INSERT INTO billing.plan_features (plan_id, feature_key, feature_name, category, enabled) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'pos', 'Punto de Venta', 'sales', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'inventory_basic', 'Inventario Basico', 'inventory', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'reports_basic', 'Reportes Basicos', 'reports', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'email_support', 'Soporte por Email', 'support', TRUE)
ON CONFLICT DO NOTHING;
-- Features para plan Professional
INSERT INTO billing.plan_features (plan_id, feature_key, feature_name, category, enabled) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'pos', 'Punto de Venta', 'sales', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'mobile_app', 'App Movil', 'platform', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'inventory_advanced', 'Inventario Avanzado', 'inventory', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'reports_advanced', 'Reportes Avanzados', 'reports', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'multi_branch', 'Multi-Sucursal', 'core', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'api_access', 'Acceso a API', 'integration', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'chat_support', 'Soporte por Chat', 'support', TRUE)
ON CONFLICT DO NOTHING;
-- Features para plan Business
INSERT INTO billing.plan_features (plan_id, feature_key, feature_name, category, enabled) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'pos', 'Punto de Venta', 'sales', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'mobile_app', 'App Movil', 'platform', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'desktop_app', 'App Desktop', 'platform', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'inventory_advanced', 'Inventario Avanzado', 'inventory', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'manufacturing', 'Manufactura', 'production', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'hr_module', 'Modulo RRHH', 'hr', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'accounting', 'Contabilidad', 'financial', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'ai_assistant', 'Asistente IA', 'ai', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'webhooks', 'Webhooks', 'integration', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'priority_support', 'Soporte Prioritario', 'support', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'sla_99', 'SLA 99.9%', 'support', TRUE)
ON CONFLICT DO NOTHING;
-- =====================
-- SEED DATA: Limits por Plan
-- =====================
-- Limits para plan Starter
INSERT INTO billing.plan_limits (plan_id, limit_key, limit_name, limit_value, limit_type, allow_overage) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'users', 'Usuarios', 3, 'total', FALSE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'branches', 'Sucursales', 1, 'total', FALSE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'storage_mb', 'Storage (MB)', 5120, 'total', TRUE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'api_calls', 'API Calls', 5000, 'monthly', FALSE),
((SELECT id FROM billing.subscription_plans WHERE code = 'starter'), 'invoices', 'Facturas', 100, 'monthly', TRUE)
ON CONFLICT DO NOTHING;
-- Limits para plan Professional
INSERT INTO billing.plan_limits (plan_id, limit_key, limit_name, limit_value, limit_type, allow_overage, overage_unit_price) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'users', 'Usuarios', 10, 'total', TRUE, 99),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'branches', 'Sucursales', 3, 'total', TRUE, 199),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'storage_mb', 'Storage (MB)', 20480, 'total', TRUE, 0.10),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'api_calls', 'API Calls', 25000, 'monthly', TRUE, 0.001),
((SELECT id FROM billing.subscription_plans WHERE code = 'professional'), 'invoices', 'Facturas', 500, 'monthly', TRUE, 2)
ON CONFLICT DO NOTHING;
-- Limits para plan Business
INSERT INTO billing.plan_limits (plan_id, limit_key, limit_name, limit_value, limit_type, allow_overage, overage_unit_price) VALUES
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'users', 'Usuarios', 25, 'total', TRUE, 79),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'branches', 'Sucursales', 10, 'total', TRUE, 149),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'storage_mb', 'Storage (MB)', 102400, 'total', TRUE, 0.05),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'api_calls', 'API Calls', 100000, 'monthly', TRUE, 0.0005),
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'invoices', 'Facturas', 0, 'monthly', FALSE, 0), -- Ilimitadas
((SELECT id FROM billing.subscription_plans WHERE code = 'business'), 'ai_tokens', 'Tokens IA', 100000, 'monthly', TRUE, 0.0001)
ON CONFLICT DO NOTHING;
-- =====================
-- COMENTARIOS
-- =====================
COMMENT ON TABLE billing.plan_features IS 'Features habilitadas por plan de suscripcion';
COMMENT ON TABLE billing.plan_limits IS 'Limites cuantificables por plan';
COMMENT ON TABLE billing.coupons IS 'Cupones de descuento';
COMMENT ON TABLE billing.coupon_redemptions IS 'Registro de uso de cupones';
COMMENT ON TABLE billing.stripe_events IS 'Log de eventos recibidos de Stripe';
COMMENT ON FUNCTION billing.has_feature IS 'Verifica si un tenant tiene una feature habilitada';
COMMENT ON FUNCTION billing.get_limit IS 'Obtiene el valor de un limite para un tenant';
COMMENT ON FUNCTION billing.can_use_feature IS 'Verifica si un tenant puede usar una feature con limite';
COMMENT ON FUNCTION billing.get_tenant_features IS 'Obtiene todas las features habilitadas para un tenant';
COMMENT ON FUNCTION billing.get_tenant_limits IS 'Obtiene todos los limites de un tenant';
COMMENT ON FUNCTION billing.apply_coupon IS 'Aplica un cupon de descuento a un tenant';