- 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>
623 lines
21 KiB
PL/PgSQL
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';
|