erp-core-database/ddl/05-billing-usage.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

623 lines
21 KiB
PL/PgSQL

-- =============================================================
-- ARCHIVO: 05-billing-usage.sql
-- DESCRIPCION: Facturacion por uso, tracking de consumo, suscripciones
-- VERSION: 1.0.0
-- PROYECTO: ERP-Core V2
-- FECHA: 2026-01-10
-- =============================================================
-- =====================
-- SCHEMA: billing
-- =====================
CREATE SCHEMA IF NOT EXISTS billing;
-- =====================
-- TABLA: subscription_plans
-- Planes de suscripcion disponibles
-- =====================
CREATE TABLE IF NOT EXISTS billing.subscription_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identificacion
code VARCHAR(30) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
description TEXT,
-- Tipo
plan_type VARCHAR(20) NOT NULL DEFAULT 'saas', -- saas, on_premise, hybrid
-- Precios base
base_monthly_price DECIMAL(12,2) NOT NULL DEFAULT 0,
base_annual_price DECIMAL(12,2), -- Precio anual con descuento
setup_fee DECIMAL(12,2) DEFAULT 0,
-- Limites base
max_users INTEGER DEFAULT 5,
max_branches INTEGER DEFAULT 1,
storage_gb INTEGER DEFAULT 10,
api_calls_monthly INTEGER DEFAULT 10000,
-- Modulos incluidos
included_modules TEXT[] DEFAULT '{}',
-- Plataformas incluidas
included_platforms TEXT[] DEFAULT '{web}',
-- Features
features JSONB DEFAULT '{}',
-- Estado
is_active BOOLEAN DEFAULT TRUE,
is_public BOOLEAN DEFAULT TRUE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ
);
-- =====================
-- TABLA: tenant_subscriptions
-- Suscripciones activas de tenants
-- =====================
CREATE TABLE IF NOT EXISTS billing.tenant_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),
-- Periodo
billing_cycle VARCHAR(20) NOT NULL DEFAULT 'monthly', -- monthly, annual
current_period_start TIMESTAMPTZ NOT NULL,
current_period_end TIMESTAMPTZ NOT NULL,
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'active', -- trial, active, past_due, cancelled, suspended
-- Trial
trial_start TIMESTAMPTZ,
trial_end TIMESTAMPTZ,
-- Configuracion de facturacion
billing_email VARCHAR(255),
billing_name VARCHAR(200),
billing_address JSONB DEFAULT '{}',
tax_id VARCHAR(20), -- RFC para Mexico
-- Metodo de pago
payment_method_id UUID,
payment_provider VARCHAR(30), -- stripe, mercadopago, bank_transfer
-- Precios actuales (pueden diferir del plan por descuentos)
current_price DECIMAL(12,2) NOT NULL,
discount_percent DECIMAL(5,2) DEFAULT 0,
discount_reason VARCHAR(100),
-- Uso contratado
contracted_users INTEGER,
contracted_branches INTEGER,
-- Facturacion automatica
auto_renew BOOLEAN DEFAULT TRUE,
next_invoice_date DATE,
-- Cancelacion
cancel_at_period_end BOOLEAN DEFAULT FALSE,
cancelled_at TIMESTAMPTZ,
cancellation_reason TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tenant_id)
);
-- Indices para tenant_subscriptions
CREATE INDEX IF NOT EXISTS idx_subscriptions_tenant ON billing.tenant_subscriptions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_plan ON billing.tenant_subscriptions(plan_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON billing.tenant_subscriptions(status);
CREATE INDEX IF NOT EXISTS idx_subscriptions_period_end ON billing.tenant_subscriptions(current_period_end);
-- =====================
-- TABLA: usage_tracking
-- Tracking de uso por tenant
-- =====================
CREATE TABLE IF NOT EXISTS billing.usage_tracking (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Periodo
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Usuarios
active_users INTEGER DEFAULT 0,
peak_concurrent_users INTEGER DEFAULT 0,
-- Por perfil
users_by_profile JSONB DEFAULT '{}',
-- Ejemplo: {"ADM": 2, "VNT": 5, "ALM": 3}
-- Por plataforma
users_by_platform JSONB DEFAULT '{}',
-- Ejemplo: {"web": 8, "mobile": 5, "desktop": 0}
-- Sucursales
active_branches INTEGER DEFAULT 0,
-- Storage
storage_used_gb DECIMAL(10,2) DEFAULT 0,
documents_count INTEGER DEFAULT 0,
-- API
api_calls INTEGER DEFAULT 0,
api_errors INTEGER DEFAULT 0,
-- Transacciones
sales_count INTEGER DEFAULT 0,
sales_amount DECIMAL(14,2) DEFAULT 0,
invoices_generated INTEGER DEFAULT 0,
-- Mobile
mobile_sessions INTEGER DEFAULT 0,
offline_syncs INTEGER DEFAULT 0,
payment_transactions INTEGER DEFAULT 0,
-- Calculado
total_billable_amount DECIMAL(12,2) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(tenant_id, period_start)
);
-- Indices para usage_tracking
CREATE INDEX IF NOT EXISTS idx_usage_tenant ON billing.usage_tracking(tenant_id);
CREATE INDEX IF NOT EXISTS idx_usage_period ON billing.usage_tracking(period_start, period_end);
-- =====================
-- TABLA: usage_events
-- Eventos de uso en tiempo real (para calculo de billing)
-- =====================
CREATE TABLE IF NOT EXISTS billing.usage_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id),
device_id UUID REFERENCES auth.devices(id),
branch_id UUID REFERENCES core.branches(id),
-- Evento
event_type VARCHAR(50) NOT NULL, -- login, api_call, document_upload, sale, invoice, sync
event_category VARCHAR(30) NOT NULL, -- user, api, storage, transaction, mobile
-- Detalles
profile_code VARCHAR(10),
platform VARCHAR(20),
resource_id UUID,
resource_type VARCHAR(50),
-- Metricas
quantity INTEGER DEFAULT 1,
bytes_used BIGINT DEFAULT 0,
duration_ms INTEGER,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para usage_events (particionado por fecha recomendado)
CREATE INDEX IF NOT EXISTS idx_usage_events_tenant ON billing.usage_events(tenant_id);
CREATE INDEX IF NOT EXISTS idx_usage_events_type ON billing.usage_events(event_type);
CREATE INDEX IF NOT EXISTS idx_usage_events_category ON billing.usage_events(event_category);
CREATE INDEX IF NOT EXISTS idx_usage_events_created ON billing.usage_events(created_at DESC);
-- =====================
-- TABLA: invoices
-- Facturas generadas
-- =====================
CREATE TABLE IF NOT EXISTS 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.tenant_subscriptions(id),
-- Numero de factura
invoice_number VARCHAR(30) NOT NULL UNIQUE,
invoice_date DATE NOT NULL,
-- Periodo facturado
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Cliente
billing_name VARCHAR(200),
billing_email VARCHAR(255),
billing_address JSONB DEFAULT '{}',
tax_id VARCHAR(20),
-- Montos
subtotal DECIMAL(12,2) NOT NULL,
tax_amount DECIMAL(12,2) DEFAULT 0,
discount_amount DECIMAL(12,2) DEFAULT 0,
total DECIMAL(12,2) NOT NULL,
currency VARCHAR(3) DEFAULT 'MXN',
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft, sent, paid, partial, overdue, void, refunded
-- Fechas de pago
due_date DATE NOT NULL,
paid_at TIMESTAMPTZ,
paid_amount DECIMAL(12,2) DEFAULT 0,
-- Detalles de pago
payment_method VARCHAR(30),
payment_reference VARCHAR(100),
-- CFDI (para Mexico)
cfdi_uuid VARCHAR(36),
cfdi_xml TEXT,
cfdi_pdf_url TEXT,
-- Metadata
notes TEXT,
internal_notes TEXT,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para invoices
CREATE INDEX IF NOT EXISTS idx_invoices_tenant ON billing.invoices(tenant_id);
CREATE INDEX IF NOT EXISTS idx_invoices_subscription ON billing.invoices(subscription_id);
CREATE INDEX IF NOT EXISTS idx_invoices_number ON billing.invoices(invoice_number);
CREATE INDEX IF NOT EXISTS idx_invoices_status ON billing.invoices(status);
CREATE INDEX IF NOT EXISTS idx_invoices_date ON billing.invoices(invoice_date DESC);
CREATE INDEX IF NOT EXISTS idx_invoices_due ON billing.invoices(due_date) WHERE status IN ('sent', 'partial', 'overdue');
-- =====================
-- TABLA: invoice_items
-- Items de cada factura
-- =====================
CREATE TABLE IF NOT EXISTS billing.invoice_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
invoice_id UUID NOT NULL REFERENCES billing.invoices(id) ON DELETE CASCADE,
-- Descripcion
description VARCHAR(500) NOT NULL,
item_type VARCHAR(30) NOT NULL, -- subscription, user, profile, overage, addon
-- Cantidades
quantity INTEGER NOT NULL DEFAULT 1,
unit_price DECIMAL(12,2) NOT NULL,
subtotal DECIMAL(12,2) NOT NULL,
-- Detalles adicionales
profile_code VARCHAR(10), -- Si es cargo por perfil
platform VARCHAR(20), -- Si es cargo por plataforma
period_start DATE,
period_end DATE,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para invoice_items
CREATE INDEX IF NOT EXISTS idx_invoice_items_invoice ON billing.invoice_items(invoice_id);
CREATE INDEX IF NOT EXISTS idx_invoice_items_type ON billing.invoice_items(item_type);
-- =====================
-- TABLA: payment_methods
-- Metodos de pago guardados
-- =====================
CREATE TABLE IF NOT EXISTS billing.payment_methods (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Proveedor
provider VARCHAR(30) NOT NULL, -- stripe, mercadopago, bank_transfer
-- Tipo
method_type VARCHAR(20) NOT NULL, -- card, bank_account, wallet
-- Datos (encriptados/tokenizados)
provider_customer_id VARCHAR(255),
provider_method_id VARCHAR(255),
-- Display info (no sensible)
display_name VARCHAR(100),
card_brand VARCHAR(20),
card_last_four VARCHAR(4),
card_exp_month INTEGER,
card_exp_year INTEGER,
bank_name VARCHAR(100),
bank_last_four VARCHAR(4),
-- Estado
is_default BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ
);
-- Indices para payment_methods
CREATE INDEX IF NOT EXISTS idx_payment_methods_tenant ON billing.payment_methods(tenant_id);
CREATE INDEX IF NOT EXISTS idx_payment_methods_provider ON billing.payment_methods(provider);
CREATE INDEX IF NOT EXISTS idx_payment_methods_default ON billing.payment_methods(is_default) WHERE is_default = TRUE;
-- =====================
-- TABLA: billing_alerts
-- Alertas de facturacion
-- =====================
CREATE TABLE IF NOT EXISTS billing.billing_alerts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE,
-- Tipo de alerta
alert_type VARCHAR(30) NOT NULL, -- usage_limit, payment_due, payment_failed, trial_ending, subscription_ending
-- Detalles
title VARCHAR(200) NOT NULL,
message TEXT,
severity VARCHAR(20) NOT NULL DEFAULT 'info', -- info, warning, critical
-- Estado
status VARCHAR(20) NOT NULL DEFAULT 'active', -- active, acknowledged, resolved
-- Notificacion
notified_at TIMESTAMPTZ,
acknowledged_at TIMESTAMPTZ,
acknowledged_by UUID REFERENCES auth.users(id),
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Indices para billing_alerts
CREATE INDEX IF NOT EXISTS idx_billing_alerts_tenant ON billing.billing_alerts(tenant_id);
CREATE INDEX IF NOT EXISTS idx_billing_alerts_type ON billing.billing_alerts(alert_type);
CREATE INDEX IF NOT EXISTS idx_billing_alerts_status ON billing.billing_alerts(status) WHERE status = 'active';
-- =====================
-- RLS POLICIES
-- =====================
ALTER TABLE billing.tenant_subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_subscriptions ON billing.tenant_subscriptions
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.usage_tracking ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_usage ON billing.usage_tracking
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.usage_events ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_usage_events ON billing.usage_events
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_invoices ON billing.invoices
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.invoice_items ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_invoice_items ON billing.invoice_items
USING (invoice_id IN (
SELECT id FROM billing.invoices
WHERE tenant_id = current_setting('app.current_tenant_id', true)::uuid
));
ALTER TABLE billing.payment_methods ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_payment_methods ON billing.payment_methods
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
ALTER TABLE billing.billing_alerts ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation_billing_alerts ON billing.billing_alerts
USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);
-- =====================
-- FUNCIONES
-- =====================
-- Funcion para calcular uso mensual de un tenant
CREATE OR REPLACE FUNCTION billing.calculate_monthly_usage(
p_tenant_id UUID,
p_period_start DATE,
p_period_end DATE
)
RETURNS TABLE (
active_users INTEGER,
users_by_profile JSONB,
users_by_platform JSONB,
active_branches INTEGER,
storage_used_gb DECIMAL,
api_calls INTEGER,
total_billable DECIMAL
) AS $$
BEGIN
RETURN QUERY
WITH user_stats AS (
SELECT
COUNT(DISTINCT ue.user_id) as active_users,
jsonb_object_agg(
COALESCE(ue.profile_code, 'unknown'),
COUNT(DISTINCT ue.user_id)
) as by_profile,
jsonb_object_agg(
COALESCE(ue.platform, 'unknown'),
COUNT(DISTINCT ue.user_id)
) as by_platform
FROM billing.usage_events ue
WHERE ue.tenant_id = p_tenant_id
AND ue.created_at >= p_period_start
AND ue.created_at < p_period_end
AND ue.event_category = 'user'
),
branch_stats AS (
SELECT COUNT(DISTINCT branch_id) as active_branches
FROM billing.usage_events
WHERE tenant_id = p_tenant_id
AND created_at >= p_period_start
AND created_at < p_period_end
AND branch_id IS NOT NULL
),
storage_stats AS (
SELECT COALESCE(SUM(bytes_used), 0)::DECIMAL / (1024*1024*1024) as storage_gb
FROM billing.usage_events
WHERE tenant_id = p_tenant_id
AND created_at >= p_period_start
AND created_at < p_period_end
AND event_category = 'storage'
),
api_stats AS (
SELECT COUNT(*) as api_calls
FROM billing.usage_events
WHERE tenant_id = p_tenant_id
AND created_at >= p_period_start
AND created_at < p_period_end
AND event_category = 'api'
)
SELECT
us.active_users::INTEGER,
us.by_profile,
us.by_platform,
bs.active_branches::INTEGER,
ss.storage_gb,
api.api_calls::INTEGER,
0::DECIMAL as total_billable -- Se calcula aparte basado en plan
FROM user_stats us, branch_stats bs, storage_stats ss, api_stats api;
END;
$$ LANGUAGE plpgsql;
-- Funcion para generar numero de factura
CREATE OR REPLACE FUNCTION billing.generate_invoice_number()
RETURNS VARCHAR(30) AS $$
DECLARE
v_year VARCHAR(4);
v_sequence INTEGER;
v_number VARCHAR(30);
BEGIN
v_year := to_char(CURRENT_DATE, 'YYYY');
SELECT COALESCE(MAX(
CAST(SUBSTRING(invoice_number FROM 6 FOR 6) AS INTEGER)
), 0) + 1
INTO v_sequence
FROM billing.invoices
WHERE invoice_number LIKE 'INV-' || v_year || '-%';
v_number := 'INV-' || v_year || '-' || LPAD(v_sequence::TEXT, 6, '0');
RETURN v_number;
END;
$$ LANGUAGE plpgsql;
-- Funcion para verificar limites de uso
CREATE OR REPLACE FUNCTION billing.check_usage_limits(p_tenant_id UUID)
RETURNS TABLE (
limit_type VARCHAR,
current_value INTEGER,
max_value INTEGER,
percentage DECIMAL,
is_exceeded BOOLEAN
) AS $$
BEGIN
RETURN QUERY
WITH subscription AS (
SELECT
ts.contracted_users,
ts.contracted_branches,
sp.storage_gb,
sp.api_calls_monthly
FROM billing.tenant_subscriptions ts
JOIN billing.subscription_plans sp ON sp.id = ts.plan_id
WHERE ts.tenant_id = p_tenant_id
AND ts.status = 'active'
),
current_usage AS (
SELECT
ut.active_users,
ut.active_branches,
ut.storage_used_gb::INTEGER,
ut.api_calls
FROM billing.usage_tracking ut
WHERE ut.tenant_id = p_tenant_id
AND ut.period_start <= CURRENT_DATE
AND ut.period_end >= CURRENT_DATE
)
SELECT
'users'::VARCHAR as limit_type,
COALESCE(cu.active_users, 0) as current_value,
s.contracted_users as max_value,
CASE WHEN s.contracted_users > 0
THEN (COALESCE(cu.active_users, 0)::DECIMAL / s.contracted_users * 100)
ELSE 0 END as percentage,
COALESCE(cu.active_users, 0) > s.contracted_users as is_exceeded
FROM subscription s, current_usage cu
UNION ALL
SELECT
'branches'::VARCHAR,
COALESCE(cu.active_branches, 0),
s.contracted_branches,
CASE WHEN s.contracted_branches > 0
THEN (COALESCE(cu.active_branches, 0)::DECIMAL / s.contracted_branches * 100)
ELSE 0 END,
COALESCE(cu.active_branches, 0) > s.contracted_branches
FROM subscription s, current_usage cu
UNION ALL
SELECT
'storage'::VARCHAR,
COALESCE(cu.storage_used_gb, 0),
s.storage_gb,
CASE WHEN s.storage_gb > 0
THEN (COALESCE(cu.storage_used_gb, 0)::DECIMAL / s.storage_gb * 100)
ELSE 0 END,
COALESCE(cu.storage_used_gb, 0) > s.storage_gb
FROM subscription s, current_usage cu
UNION ALL
SELECT
'api_calls'::VARCHAR,
COALESCE(cu.api_calls, 0),
s.api_calls_monthly,
CASE WHEN s.api_calls_monthly > 0
THEN (COALESCE(cu.api_calls, 0)::DECIMAL / s.api_calls_monthly * 100)
ELSE 0 END,
COALESCE(cu.api_calls, 0) > s.api_calls_monthly
FROM subscription s, current_usage cu;
END;
$$ LANGUAGE plpgsql;
-- =====================
-- SEED DATA: Planes de suscripcion
-- =====================
INSERT INTO billing.subscription_plans (code, name, description, plan_type, base_monthly_price, max_users, max_branches, storage_gb, api_calls_monthly, included_modules, included_platforms) VALUES
('starter', 'Starter', 'Plan basico para pequenos negocios', 'saas', 499, 3, 1, 5, 5000, '{core,sales,inventory}', '{web}'),
('professional', 'Professional', 'Plan profesional con app movil', 'saas', 999, 10, 3, 20, 25000, '{core,sales,inventory,purchases,financial,reports}', '{web,mobile}'),
('business', 'Business', 'Plan empresarial completo', 'saas', 2499, 25, 10, 100, 100000, '{all}', '{web,mobile,desktop}'),
('enterprise', 'Enterprise', 'Plan enterprise personalizado', 'saas', 0, 0, 0, 0, 0, '{all}', '{web,mobile,desktop}'),
('on_premise', 'On-Premise', 'Instalacion en servidor propio', 'on_premise', 0, 0, 0, 0, 0, '{all}', '{web,mobile,desktop}')
ON CONFLICT DO NOTHING;
-- =====================
-- COMENTARIOS DE TABLAS
-- =====================
COMMENT ON TABLE billing.subscription_plans IS 'Planes de suscripcion disponibles para tenants';
COMMENT ON TABLE billing.tenant_subscriptions IS 'Suscripciones activas de cada tenant';
COMMENT ON TABLE billing.usage_tracking IS 'Resumen de uso por periodo para calculo de facturacion';
COMMENT ON TABLE billing.usage_events IS 'Eventos de uso en tiempo real para tracking granular';
COMMENT ON TABLE billing.invoices IS 'Facturas generadas para cada tenant';
COMMENT ON TABLE billing.invoice_items IS 'Items detallados de cada factura';
COMMENT ON TABLE billing.payment_methods IS 'Metodos de pago guardados por tenant';
COMMENT ON TABLE billing.billing_alerts IS 'Alertas de facturacion y limites de uso';