[CONSEJO — Doc 08] Contrarian: La tabla más crítica del schema de MindStack no es
ms_tenantsnims_subscriptions— esms_billing_events. Todo lo que afecte dinero debe quedar en el audit log de billing con timestamp, monto, y el estado antes y después del cambio. Si hay una discrepancia entre lo que ONVO dice que cobró y lo que MindStack dice que cobró, el audit log es el árbitro. Sin él, el soporte es un caos. Executor: El schema de MindStack debe ser lo más simple posible en el MVP. No construir tablas de metering sofisticadas hasta tener 50+ clientes que realmente necesiten facturación por uso. Empezar con planes flat-rate — es más fácil de soportar y suficiente para validar el modelo.
1. Prefijo de tablas: ms_*
Todas las tablas del portal MindStack usan el prefijo ms_ para convivir en la misma base de datos PostgreSQL de Cheryx sin colisiones.
2. Schema completo
-- ─── TENANTS ─────────────────────────────────────────────────────────────────
CREATE TABLE ms_tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nombre_empresa TEXT NOT NULL,
pais CHAR(2) NOT NULL DEFAULT 'CR', -- ISO 3166-1
email_admin TEXT NOT NULL UNIQUE,
telefono TEXT,
plan_tier TEXT NOT NULL DEFAULT 'trial', -- trial | starter | growth | pro | enterprise
estado TEXT NOT NULL DEFAULT 'active', -- active | suspended | churned
partner_id UUID REFERENCES ms_partners(id), -- contador que los refirió
onboarding_step INTEGER NOT NULL DEFAULT 0, -- 0-5, para personalizar el wizard
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
activated_at TIMESTAMPTZ, -- cuando completaron onboarding D7
churned_at TIMESTAMPTZ
);
-- ─── USUARIOS ────────────────────────────────────────────────────────────────
CREATE TABLE ms_usuarios (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id) ON DELETE CASCADE,
email TEXT NOT NULL UNIQUE,
nombre TEXT NOT NULL,
rol TEXT NOT NULL DEFAULT 'owner', -- owner | admin | viewer
mfa_secret BYTEA, -- TOTP secret cifrado
ultimo_login TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_ms_usuarios_tenant ON ms_usuarios(tenant_id);
-- ─── PLANES ──────────────────────────────────────────────────────────────────
CREATE TABLE ms_planes (
id TEXT PRIMARY KEY, -- 'stockmind_growth_monthly'
modulo TEXT NOT NULL, -- stockmind | salesmind | bookmind | paymind
nombre TEXT NOT NULL,
tier TEXT NOT NULL, -- starter | growth | pro | enterprise
periodo TEXT NOT NULL, -- monthly | annual
precio_usd NUMERIC(10,2) NOT NULL,
precio_anual_usd NUMERIC(10,2), -- precio total si paga anual
features JSONB NOT NULL DEFAULT '{}',
activo BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ─── SUSCRIPCIONES ───────────────────────────────────────────────────────────
CREATE TABLE ms_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
modulo TEXT NOT NULL,
plan_id TEXT NOT NULL REFERENCES ms_planes(id),
estado TEXT NOT NULL DEFAULT 'trial',
-- trial | active | past_due | cancelled | paused
period_start TIMESTAMPTZ NOT NULL DEFAULT NOW(),
period_end TIMESTAMPTZ NOT NULL,
trial_end TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
cancel_reason TEXT,
onvo_sub_id TEXT, -- ID de suscripción en ONVO Pay
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, modulo) -- un solo registro activo por módulo por tenant
);
CREATE INDEX idx_ms_subs_tenant ON ms_subscriptions(tenant_id);
CREATE INDEX idx_ms_subs_period_end ON ms_subscriptions(period_end)
WHERE estado IN ('active', 'trial');
-- ─── BILLING EVENTS (inmutable) ───────────────────────────────────────────────
CREATE TABLE ms_billing_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
subscription_id UUID REFERENCES ms_subscriptions(id),
tipo TEXT NOT NULL,
-- payment_success | payment_failed | trial_started | trial_ended |
-- subscription_activated | subscription_cancelled | refund | credit_applied
monto_usd NUMERIC(10,2),
moneda CHAR(3) DEFAULT 'USD',
onvo_payment_id TEXT,
metadata JSONB NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Inmutable: nadie puede modificar ni borrar eventos de billing
REVOKE UPDATE ON ms_billing_events FROM app_role;
REVOKE DELETE ON ms_billing_events FROM app_role;
CREATE INDEX idx_ms_billing_tenant ON ms_billing_events(tenant_id);
CREATE INDEX idx_ms_billing_created ON ms_billing_events(created_at DESC);
-- ─── CRÉDITOS DE REFERIDOS ────────────────────────────────────────────────────
CREATE TABLE ms_creditos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
monto_usd NUMERIC(10,2) NOT NULL,
origen TEXT NOT NULL, -- referral | promo | goodwill
referido_id UUID REFERENCES ms_tenants(id),
estado TEXT NOT NULL DEFAULT 'available', -- available | applied | expired
vence_at TIMESTAMPTZ NOT NULL, -- 12 meses desde generación
aplicado_at TIMESTAMPTZ,
aplicado_en UUID REFERENCES ms_billing_events(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ─── PARTNERS (contadores CPA) ────────────────────────────────────────────────
CREATE TABLE ms_partners (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
nombre TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
tipo TEXT NOT NULL DEFAULT 'contador', -- contador | agencia | reseller
pais CHAR(2) NOT NULL DEFAULT 'CR',
referral_code TEXT NOT NULL UNIQUE,
tasa_comision NUMERIC(5,4) NOT NULL DEFAULT 0.20, -- 20% default
estado TEXT NOT NULL DEFAULT 'active',
comisiones_acumuladas_usd NUMERIC(12,2) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ─── COMISIONES DE PARTNERS ───────────────────────────────────────────────────
CREATE TABLE ms_comisiones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
partner_id UUID NOT NULL REFERENCES ms_partners(id),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
billing_event_id UUID NOT NULL REFERENCES ms_billing_events(id),
monto_usd NUMERIC(10,2) NOT NULL,
tasa_aplicada NUMERIC(5,4) NOT NULL,
estado TEXT NOT NULL DEFAULT 'pending', -- pending | pagada
pagada_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ─── USO POR MÓDULO (para triggers de expansion) ─────────────────────────────
CREATE TABLE ms_uso_modulo (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
modulo TEXT NOT NULL,
metrica TEXT NOT NULL,
-- stockmind: 'oc_generadas', 'skus_activos'
-- salesmind: 'pipeline_valor_usd', 'contactos'
-- bookmind: 'fe_emitidas', 'empleados_en_fe'
-- paymind: 'empleados_activos'
valor NUMERIC(15,2) NOT NULL,
periodo_inicio DATE NOT NULL,
periodo_fin DATE NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, modulo, metrica, periodo_inicio)
);
-- ─── OFERTAS DE EXPANSION (cross-sell in-app) ─────────────────────────────────
CREATE TABLE ms_expansion_offers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES ms_tenants(id),
modulo_destino TEXT NOT NULL,
mensaje TEXT NOT NULL,
vista_at TIMESTAMPTZ,
accion TEXT, -- 'trial_started' | 'dismissed' | null
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(tenant_id, modulo_destino) -- una sola oferta activa por módulo por tenant
);
3. Queries operacionales clave
-- MRR actual del portal
SELECT
SUM(p.precio_usd) AS mrr_usd,
COUNT(*) AS suscripciones_activas
FROM ms_subscriptions s
JOIN ms_planes p ON s.plan_id = p.id
WHERE s.estado = 'active'
AND p.periodo = 'monthly';
-- Tenants en riesgo de churn (sin login en 7+ días)
SELECT t.id, t.email_admin, t.nombre_empresa,
MAX(u.ultimo_login) AS ultimo_login
FROM ms_tenants t
JOIN ms_usuarios u ON u.tenant_id = t.id
JOIN ms_subscriptions s ON s.tenant_id = t.id AND s.estado = 'active'
WHERE MAX(u.ultimo_login) < NOW() - INTERVAL '7 days'
GROUP BY t.id
ORDER BY ultimo_login ASC;
-- Conversión de trial a pago por cohorte mensual
SELECT
DATE_TRUNC('month', s.created_at) AS cohorte,
COUNT(*) FILTER (WHERE s.estado != 'trial') AS convertidos,
COUNT(*) AS total_trials,
ROUND(100.0 * COUNT(*) FILTER (WHERE s.estado != 'trial') / COUNT(*), 1) AS pct_conversion
FROM ms_subscriptions s
WHERE s.trial_end IS NOT NULL
GROUP BY 1
ORDER BY 1 DESC;
Ver también: Doc 07 (SRD Backend — servicios que usan estas tablas) · Doc 09 (Motor Core — lógica de suscripciones) · Doc 10 (Seguridad — roles y acceso a ms_billing_events)