📦 ADD-ON SCHEMA: Sistema de Planes y Suscripciones
🎯 Tablas necesarias para implementar la Guía Técnica de Planes
Tu schema actual está muy bien para el MVP. Para agregar el sistema completo de planes descrito en la guía técnica, necesitas estas 5 tablas adicionales:
✅ TABLAS A AGREGAR
1. subscription_plans (Catálogo de planes)
CREATE TABLE subscription_plans (
id BIGSERIAL PRIMARY KEY,
-- Identificación
name VARCHAR(50) UNIQUE NOT NULL, -- 'individual', 'starter', 'pro', 'business'
display_name VARCHAR(100) NOT NULL, -- 'Individual', 'Starter', 'Pro', 'Business'
description TEXT,
-- Pricing
price INT NOT NULL, -- Precio base mensual (CLP sin decimales)
extra_professional_price INT NOT NULL DEFAULT 4000, -- $4.000 por profesional adicional
-- Límites (JSONB para flexibilidad)
limits JSONB NOT NULL DEFAULT '{}',
/* Estructura del JSON:
{
"workers": 7, // Máximo de workers (-1 = ilimitado)
"base_workers": 2, // Workers incluidos en precio base
"services": 10, // Máximo de servicios (-1 = ilimitado)
"locations": 1, // Máximo de sucursales (-1 = ilimitado)
"emails": 1000, // Emails por mes (-1 = ilimitado)
"whatsapp_messages": 150, // WhatsApp por mes
"storage_mb": 500 // Almacenamiento
}
*/
-- Features (JSONB con array de features)
features JSONB NOT NULL DEFAULT '[]',
/* Estructura del JSON:
[
"dashboard",
"calendar",
"team_management",
"basic_finances",
"advanced_analytics",
...
]
*/
-- Orden y visibilidad
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
is_visible_marketplace BOOLEAN DEFAULT TRUE,
-- Metadata
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Índices
CREATE INDEX idx_subscription_plans_name ON subscription_plans(name);
CREATE INDEX idx_subscription_plans_active ON subscription_plans(is_active);
CREATE INDEX idx_subscription_plans_sort ON subscription_plans(sort_order);2. team_subscriptions (Suscripción activa del team)
CREATE TABLE team_subscriptions (
id BIGSERIAL PRIMARY KEY,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
subscription_plan_id BIGINT NOT NULL REFERENCES subscription_plans(id),
-- Estado
status VARCHAR(50) DEFAULT 'active', -- active, cancelled, suspended, past_due, trialing
-- Pricing
base_price INT NOT NULL, -- Snapshot del precio base al contratar
extra_professionals INT DEFAULT 0, -- Cantidad de profesionales adicionales contratados
extra_professionals_cost INT DEFAULT 0, -- Costo de profesionales adicionales
-- Add-ons WhatsApp
whatsapp_packs INT DEFAULT 0, -- Cantidad de packs WhatsApp comprados
whatsapp_packs_cost INT DEFAULT 0, -- Costo mensual de packs WhatsApp
-- Total calculado
total_monthly_cost INT NOT NULL, -- base_price + extra_professionals_cost + whatsapp_packs_cost
-- Períodos
current_period_start TIMESTAMP NOT NULL,
current_period_end TIMESTAMP NOT NULL,
-- Trial
trial_ends_at TIMESTAMP,
trial_days INT DEFAULT 14,
-- Cancelación
cancelled_at TIMESTAMP,
cancel_reason TEXT,
ends_at TIMESTAMP, -- Si cancela, hasta cuándo tiene acceso
-- Renovación automática
auto_renew BOOLEAN DEFAULT TRUE,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Constraint: Solo 1 suscripción activa por team
UNIQUE(team_id) WHERE status = 'active'
);
-- Índices
CREATE INDEX idx_team_subscriptions_team_id ON team_subscriptions(team_id);
CREATE INDEX idx_team_subscriptions_plan_id ON team_subscriptions(subscription_plan_id);
CREATE INDEX idx_team_subscriptions_status ON team_subscriptions(status);
CREATE INDEX idx_team_subscriptions_trial ON team_subscriptions(trial_ends_at) WHERE trial_ends_at IS NOT NULL;
CREATE INDEX idx_team_subscriptions_period ON team_subscriptions(current_period_end);
-- RLS
ALTER TABLE team_subscriptions ENABLE ROW LEVEL SECURITY;
CREATE POLICY team_isolation ON team_subscriptions
USING (team_id = current_setting('app.current_team_id')::bigint);3. subscription_usage (Tracking de uso mensual)
CREATE TABLE subscription_usage (
id BIGSERIAL PRIMARY KEY,
team_subscription_id BIGINT NOT NULL REFERENCES team_subscriptions(id) ON DELETE CASCADE,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
-- Período de medición
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Contadores de uso
emails_sent INT DEFAULT 0,
whatsapp_messages_sent INT DEFAULT 0,
storage_used_mb INT DEFAULT 0,
appointments_created INT DEFAULT 0,
-- Workers activos en el período
active_workers INT DEFAULT 0,
-- Metadata adicional
usage_data JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
-- Un registro por team por período
UNIQUE(team_id, period_start, period_end)
);
-- Índices
CREATE INDEX idx_subscription_usage_team_id ON subscription_usage(team_id);
CREATE INDEX idx_subscription_usage_period ON subscription_usage(period_start, period_end);
CREATE INDEX idx_subscription_usage_subscription ON subscription_usage(team_subscription_id);
-- RLS
ALTER TABLE subscription_usage ENABLE ROW LEVEL SECURITY;
CREATE POLICY team_isolation ON subscription_usage
USING (team_id = current_setting('app.current_team_id')::bigint);4. subscription_invoices (Historial de facturación)
CREATE TABLE subscription_invoices (
id BIGSERIAL PRIMARY KEY,
team_subscription_id BIGINT NOT NULL REFERENCES team_subscriptions(id) ON DELETE CASCADE,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
-- Identificadores
invoice_number VARCHAR(50) UNIQUE NOT NULL, -- INV-2025-001234
-- Montos
subtotal INT NOT NULL,
tax_amount INT DEFAULT 0, -- IVA (19% en Chile)
total_amount INT NOT NULL,
-- Líneas de cobro
line_items JSONB NOT NULL DEFAULT '[]',
/* Estructura del JSON:
[
{
"description": "Plan Pro - Enero 2025",
"quantity": 1,
"unit_price": 47990,
"total": 47990
},
{
"description": "3 profesionales adicionales",
"quantity": 3,
"unit_price": 4000,
"total": 12000
},
{
"description": "2 packs WhatsApp",
"quantity": 2,
"unit_price": 2500,
"total": 5000
}
]
*/
-- Período facturado
period_start DATE NOT NULL,
period_end DATE NOT NULL,
-- Estado
status VARCHAR(50) DEFAULT 'draft', -- draft, issued, paid, overdue, void
-- Fechas importantes
issued_at TIMESTAMP,
due_at TIMESTAMP,
paid_at TIMESTAMP,
-- Método de pago
payment_method VARCHAR(50), -- credit_card, debit, transfer, cash
payment_reference VARCHAR(255), -- ID de transacción externa
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Índices
CREATE INDEX idx_subscription_invoices_team_id ON subscription_invoices(team_id);
CREATE INDEX idx_subscription_invoices_subscription ON subscription_invoices(team_subscription_id);
CREATE INDEX idx_subscription_invoices_number ON subscription_invoices(invoice_number);
CREATE INDEX idx_subscription_invoices_status ON subscription_invoices(status);
CREATE INDEX idx_subscription_invoices_due ON subscription_invoices(due_at) WHERE status IN ('issued', 'overdue');
-- RLS
ALTER TABLE subscription_invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY team_isolation ON subscription_invoices
USING (team_id = current_setting('app.current_team_id')::bigint);5. subscription_changes_log (Auditoría de cambios de plan)
CREATE TABLE subscription_changes_log (
id BIGSERIAL PRIMARY KEY,
team_subscription_id BIGINT NOT NULL REFERENCES team_subscriptions(id) ON DELETE CASCADE,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
-- Cambio realizado
change_type VARCHAR(50) NOT NULL, -- plan_upgrade, plan_downgrade, addon_purchase, addon_cancel, cancellation, reactivation
-- Plan anterior y nuevo (si aplica)
old_plan_id BIGINT REFERENCES subscription_plans(id),
new_plan_id BIGINT REFERENCES subscription_plans(id),
-- Valores antes y después
old_values JSONB DEFAULT '{}',
new_values JSONB DEFAULT '{}',
/* Ejemplo:
old_values: {"plan": "starter", "extra_professionals": 2, "whatsapp_packs": 1}
new_values: {"plan": "pro", "extra_professionals": 5, "whatsapp_packs": 3}
*/
-- Metadata del cambio
reason TEXT, -- Razón del cambio (opcional)
changed_by BIGINT REFERENCES users(id), -- Usuario que realizó el cambio
-- Impacto financiero
price_difference INT, -- Diferencia de precio (positivo = más caro, negativo = más barato)
effective_from TIMESTAMP, -- Cuándo entra en efecto
created_at TIMESTAMP DEFAULT NOW()
);
-- Índices
CREATE INDEX idx_subscription_changes_team_id ON subscription_changes_log(team_id);
CREATE INDEX idx_subscription_changes_subscription ON subscription_changes_log(team_subscription_id);
CREATE INDEX idx_subscription_changes_type ON subscription_changes_log(change_type);
CREATE INDEX idx_subscription_changes_date ON subscription_changes_log(created_at DESC);
-- RLS
ALTER TABLE subscription_changes_log ENABLE ROW LEVEL SECURITY;
CREATE POLICY team_isolation ON subscription_changes_log
USING (team_id = current_setting('app.current_team_id')::bigint);🔧 MODIFICACIONES A TABLAS EXISTENTES
Actualizar teams (agregar campos de tracking)
ALTER TABLE teams ADD COLUMN IF NOT EXISTS current_plan_name VARCHAR(50) DEFAULT 'free';
ALTER TABLE teams ADD COLUMN IF NOT EXISTS trial_ends_at TIMESTAMP;
-- Contadores de uso mensual (se resetean cada mes)
ALTER TABLE teams ADD COLUMN IF NOT EXISTS emails_sent_this_month INT DEFAULT 0;
ALTER TABLE teams ADD COLUMN IF NOT EXISTS whatsapp_sent_this_month INT DEFAULT 0;
ALTER TABLE teams ADD COLUMN IF NOT EXISTS usage_reset_date DATE DEFAULT CURRENT_DATE;
-- Índices
CREATE INDEX idx_teams_plan_name ON teams(current_plan_name);
CREATE INDEX idx_teams_trial ON teams(trial_ends_at) WHERE trial_ends_at IS NOT NULL;Nota: Estos campos son cache/denormalizados para queries rápidas. La fuente de verdad sigue siendo team_subscriptions.
Actualizar subscriptions existente (si la tienes muy básica)
Si ya tienes una tabla subscriptions muy simple, puedes:
Opción A: Renombrarla y usar team_subscriptions (recomendado)
ALTER TABLE subscriptions RENAME TO team_subscriptions_old;
-- Crear team_subscriptions nueva
-- Migrar datos manualmenteOpción B: Extenderla con los campos necesarios
ALTER TABLE subscriptions ADD COLUMN whatsapp_packs INT DEFAULT 0;
ALTER TABLE subscriptions ADD COLUMN whatsapp_packs_cost INT DEFAULT 0;
ALTER TABLE subscriptions ADD COLUMN extra_professionals INT DEFAULT 0;
ALTER TABLE subscriptions ADD COLUMN total_monthly_cost INT;
-- etc...📊 VERSIÓN DBML PARA dbdiagram.io
Agrega esto al final de tu schema actual:
// ========================================
// GRUPO 6: SISTEMA DE PLANES
// ========================================
Table subscription_plans {
id bigserial [pk, increment]
name varchar(50) [unique, not null, note: 'individual, starter, pro, business']
display_name varchar(100) [not null]
description text
price int [not null, note: 'Precio base mensual CLP']
extra_professional_price int [not null, default: 4000]
limits jsonb [not null, default: '{}', note: 'workers, services, locations, emails, whatsapp_messages']
features jsonb [not null, default: '[]', note: 'Array de features permitidas']
sort_order int [default: 0]
is_active boolean [default: true]
is_visible_marketplace boolean [default: true]
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
Indexes {
name
is_active
sort_order
}
Note: 'Catálogo de planes disponibles'
}
Table team_subscriptions {
id bigserial [pk, increment]
team_id bigint [not null, ref: > teams.id]
subscription_plan_id bigint [not null, ref: > subscription_plans.id]
status varchar(50) [default: 'active', note: 'active, cancelled, suspended, past_due, trialing']
base_price int [not null]
extra_professionals int [default: 0]
extra_professionals_cost int [default: 0]
whatsapp_packs int [default: 0, note: 'Cantidad de packs WhatsApp']
whatsapp_packs_cost int [default: 0]
total_monthly_cost int [not null]
current_period_start timestamp [not null]
current_period_end timestamp [not null]
trial_ends_at timestamp
trial_days int [default: 14]
cancelled_at timestamp
cancel_reason text
ends_at timestamp
auto_renew boolean [default: true]
metadata jsonb [default: '{}']
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
Indexes {
team_id
subscription_plan_id
status
trial_ends_at
current_period_end
}
Note: 'Suscripción activa del team - RLS enabled'
}
Table subscription_usage {
id bigserial [pk, increment]
team_subscription_id bigint [not null, ref: > team_subscriptions.id]
team_id bigint [not null, ref: > teams.id]
period_start date [not null]
period_end date [not null]
emails_sent int [default: 0]
whatsapp_messages_sent int [default: 0]
storage_used_mb int [default: 0]
appointments_created int [default: 0]
active_workers int [default: 0]
usage_data jsonb [default: '{}']
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
Indexes {
team_id
(period_start, period_end)
team_subscription_id
(team_id, period_start, period_end) [unique]
}
Note: 'Tracking de uso mensual - RLS enabled'
}
Table subscription_invoices {
id bigserial [pk, increment]
team_subscription_id bigint [not null, ref: > team_subscriptions.id]
team_id bigint [not null, ref: > teams.id]
invoice_number varchar(50) [unique, not null]
subtotal int [not null]
tax_amount int [default: 0]
total_amount int [not null]
line_items jsonb [not null, default: '[]', note: 'Detalle de cobros']
period_start date [not null]
period_end date [not null]
status varchar(50) [default: 'draft', note: 'draft, issued, paid, overdue, void']
issued_at timestamp
due_at timestamp
paid_at timestamp
payment_method varchar(50)
payment_reference varchar(255)
metadata jsonb [default: '{}']
created_at timestamp [default: `now()`]
updated_at timestamp [default: `now()`]
Indexes {
team_id
team_subscription_id
invoice_number
status
due_at
}
Note: 'Historial de facturación - RLS enabled'
}
Table subscription_changes_log {
id bigserial [pk, increment]
team_subscription_id bigint [not null, ref: > team_subscriptions.id]
team_id bigint [not null, ref: > teams.id]
change_type varchar(50) [not null, note: 'plan_upgrade, plan_downgrade, addon_purchase, cancellation']
old_plan_id bigint [ref: > subscription_plans.id]
new_plan_id bigint [ref: > subscription_plans.id]
old_values jsonb [default: '{}']
new_values jsonb [default: '{}']
reason text
changed_by bigint [ref: > users.id]
price_difference int
effective_from timestamp
created_at timestamp [default: `now()`]
Indexes {
team_id
team_subscription_id
change_type
created_at [note: 'DESC']
}
Note: 'Auditoría de cambios de plan - RLS enabled'
}🎯 RESUMEN DE CAMBIOS
Schema completo pasaría de:
- 27 tablas (tu MVP actual)
A:
- 32 tablas (MVP + Sistema de Planes completo)
5 tablas nuevas:
subscription_plans- Catálogo de planesteam_subscriptions- Suscripción activasubscription_usage- Tracking de usosubscription_invoices- Facturaciónsubscription_changes_log- Auditoría
Plus: 3 campos nuevos en teams para cache/performance
✅ LO QUE YA TIENES (no necesitas cambiar)
Tu schema actual YA cubre estas necesidades de la guía:
- ✅
users- Para el sistema de permisos - ✅
teams- Team principal - ✅
user_teams- Relación con roles - ✅
services- Con límites por plan - ✅
appointments- Core del negocio - ✅
transactions- Para pagos de suscripciones - ✅
notifications- Para avisos de renovación - ✅
activity_logs- Para auditoría
💡 RECOMENDACIÓN FINAL
Para el MVP (primeros 3 meses):
Agrega solo 2 tablas mínimas:
subscription_plans(catálogo)team_subscriptions(suscripción activa)
Esto te permite:
- ✅ Implementar el sistema de permisos (PlanPermissions.php)
- ✅ Validar límites (servicios, workers)
- ✅ Cobrar profesionales adicionales
- ✅ Tracking básico de uso (en
teams)
Para Fase 2 (mes 4-6):
Agrega las otras 3 tablas:
3. subscription_usage (tracking detallado)
4. subscription_invoices (facturación formal)
5. subscription_changes_log (auditoría)
🚀 PRIORIDAD DE IMPLEMENTACIÓN
FASE MVP (ahora):
├── subscription_plans ⭐⭐⭐⭐⭐ CRÍTICO
├── team_subscriptions ⭐⭐⭐⭐⭐ CRÍTICO
└── campos tracking en teams ⭐⭐⭐⭐ MUY IMPORTANTE
FASE 2 (después):
├── subscription_usage ⭐⭐⭐ IMPORTANTE
├── subscription_invoices ⭐⭐⭐ IMPORTANTE
└── subscription_changes_log ⭐⭐ ÚTIL (auditoría)
¿Quieres que genere el SQL completo de creación de estas 5 tablas listo para ejecutar?