167 lines
6.6 KiB
Markdown
167 lines
6.6 KiB
Markdown
# Plan Custom — gratis, sin fecha fin, solo asignable por Admin Global
|
|
|
|
## Contexto
|
|
|
|
El owner pidió un plan "Custom" para casos donde quiere otorgar acceso al
|
|
sistema sin cobro y sin fecha de finalización (cortesía, beta tester, caso
|
|
especial). Solo el Admin Global puede asignarlo; los usuarios finales no
|
|
deben verlo en su catálogo de planes.
|
|
|
|
## Decisión clave — Reusar enum `custom`
|
|
|
|
El Plan enum de Prisma ya incluye `custom` (legacy: "precio variable por
|
|
tenant"). En dev hay **0 tenants** en ese plan, y la lógica antigua en
|
|
`subscription.service.ts` rechaza `custom` del flujo self-serve — patrón
|
|
que coincide con la nueva semántica. Reusar el enum evita migration y
|
|
mantiene compatibilidad.
|
|
|
|
## Reglas
|
|
|
|
- **Comportamiento**: idéntico a Mi Empresa (1 RFC, MANAGED, 50 timbres/mes,
|
|
features básicas, sin API ni Lolita).
|
|
- **Costo**: $0. No genera Subscription, no usa MercadoPago.
|
|
- **Vigencia**: indefinida. `tenant.trialEndsAt = null`. Sin
|
|
`currentPeriodEnd`. Ningún cron lo expira.
|
|
- **Visibilidad**: oculto del catálogo user-facing. Solo aparece como
|
|
opción en `/clientes` (admin global).
|
|
|
|
## Cambios — Catálogo
|
|
|
|
`packages/shared/src/constants/despacho-plans.ts`:
|
|
|
|
```ts
|
|
custom: {
|
|
name: 'Custom',
|
|
maxRfcs: 1,
|
|
maxUsers: 3,
|
|
maxCfdisPorContribuyente: 1_000_000,
|
|
timbresIncluidosMes: 50,
|
|
dbMode: 'MANAGED' as const,
|
|
permiteServidorBackup: false,
|
|
features: [
|
|
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
|
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
|
'forecasting', 'xml_sat',
|
|
],
|
|
},
|
|
```
|
|
|
|
NO se agrega a `DESPACHO_PLAN_PRICES` (gratis). Helpers existentes:
|
|
- `permiteOverage('custom')` → `false` ✓ (ya retorna false porque solo
|
|
cubre business_control y business_cloud)
|
|
- `isDespachoPaidPlan('custom')` → `false` ✓ (idem)
|
|
- `permiteFrecuenciaMensual('custom')` → `false` ✓ (no está en
|
|
DESPACHO_PLAN_PRICES)
|
|
|
|
## Cambios — Frontend types
|
|
|
|
`apps/web/lib/api/tenants.ts`:
|
|
|
|
Extender el tipo del campo `plan` en `CreateTenantData` y `UpdateTenantData`:
|
|
|
|
```ts
|
|
type AdminAssignablePlan =
|
|
| 'starter' | 'business' | 'business_ia' | 'enterprise' // legacy Horux 360
|
|
| 'custom'; // nuevo
|
|
```
|
|
|
|
(Despacho paid plans NO se incluyen — esos van por self-serve del owner,
|
|
fuera de scope per acuerdo con owner.)
|
|
|
|
## Cambios — Página `/clientes` (admin)
|
|
|
|
`apps/web/app/(dashboard)/clientes/page.tsx`:
|
|
|
|
1. Reemplazar `PlanType` local por `AdminAssignablePlan` importado.
|
|
2. Eliminar el `planLabels` local (líneas 174-178) y `planColors` local
|
|
(líneas 180-184). Usar el `PLAN_LABELS` global que ya existe arriba
|
|
del archivo (cubre todo). Para colores, expandir el map global o
|
|
inline en el render.
|
|
3. Extender el `<Select>` del form para incluir `custom`:
|
|
|
|
```tsx
|
|
<SelectContent>
|
|
<SelectItem value="starter">Starter (legacy) — Sin CFDIs, 1 usuario</SelectItem>
|
|
<SelectItem value="business">Business (legacy) — 50 CFDIs, 3 usuarios</SelectItem>
|
|
<SelectItem value="business_ia">Business + IA (legacy)</SelectItem>
|
|
<SelectItem value="enterprise">Enterprise (legacy) — 100 CFDIs, ilimitado</SelectItem>
|
|
<SelectItem value="custom">Custom — Sin cobro, sin fecha fin (despacho)</SelectItem>
|
|
</SelectContent>
|
|
```
|
|
|
|
4. Cuando el plan seleccionado es `custom`, ocultar el campo `amount`
|
|
(no aplica) o forzarlo a 0.
|
|
|
|
## Cambios — Página `/configuracion/planes-despacho` (user)
|
|
|
|
`apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx`:
|
|
|
|
- Las cards visibles son `mi_empresa`, `mi_empresa_plus`,
|
|
`business_control`, `business_cloud`. `custom` NO aparece (no está en
|
|
ese array).
|
|
- Si `planInfo?.plan === 'custom'`: mostrar un banner read-only
|
|
prominente:
|
|
|
|
> "Estás en el plan **Custom** asignado por tu administrador. Contacta
|
|
> a soporte si necesitas cambiar."
|
|
|
|
Y NO renderizar las cards (o renderizarlas atenuadas con botones
|
|
deshabilitados).
|
|
|
|
## No-cambios
|
|
|
|
- Schema BD / migration — el enum `custom` ya existe.
|
|
- Backend `PUT /api/tenants/:id` — ya acepta cualquier valor del enum
|
|
Prisma (sin Zod gate). Cero cambios.
|
|
- `subscription.service.ts` — su lógica anti-`custom` existente sigue
|
|
vigente y coincide con el nuevo comportamiento (rechaza self-serve).
|
|
- `getMyPlan` en `despacho.controller.ts` — ya lee `tenant.plan`
|
|
directamente. Custom se reportará al frontend correctamente.
|
|
- Cron `applyPendingChanges` y `expireTrials` — Custom no tiene
|
|
Subscription ni trialEndsAt, no le afectan.
|
|
- Trial RFC limit (V.1.0.11) — Custom tiene `trialEndsAt=null`, así
|
|
que el limit de 5 no aplica. Aplica el límite duro del catálogo (1).
|
|
|
|
## Riesgos / limitaciones aceptadas
|
|
|
|
1. **Transición paid → custom**: si el admin cambia un tenant que tenía
|
|
suscripción MP activa a `custom`, el preapproval MP **sigue
|
|
cobrando** hasta que se cancele manualmente. Mitigación: el admin
|
|
debe cancelar la suscripción primero desde `/configuracion/suscripcion`
|
|
del tenant impersonado, luego asignar custom. Documentar en runbook.
|
|
2. **Transición custom → paid**: el admin NO puede asignar planes
|
|
despacho pagables desde `/clientes` (no incluidos en el dropdown).
|
|
El tenant debe pasar por self-serve normal en
|
|
`/configuracion/planes-despacho`. Esto evita el escenario de un
|
|
tenant en plan paid sin Subscription que sería inconsistente.
|
|
3. **Hard limit de 1 RFC en custom**: igual que Mi Empresa, el límite
|
|
de 1 RFC para custom es solo billing-only hoy (no enforced en
|
|
`contribuyente.controller.ts:create`). Si se quiere enforce duro,
|
|
replicar el patrón del trial limit. Out of scope.
|
|
|
|
## Plan de pruebas
|
|
|
|
1. `pnpm typecheck` shared + api + web targeted: PASS.
|
|
2. **Admin asigna custom**: desde `/clientes`, edit tenant, seleccionar
|
|
"Custom", guardar. Verificar `tenant.plan === 'custom'` en BD.
|
|
3. **Admin asigna custom a tenant en trial**: trialEndsAt debería
|
|
limpiarse (a través de la lógica del service). Si el service no lo
|
|
limpia, agregar.
|
|
4. **User en custom**: login como ese tenant, ir a
|
|
`/configuracion/planes-despacho` → ver banner "Estás en plan Custom".
|
|
5. **Admin asigna otro plan a tenant en custom**: dropdown muestra los
|
|
demás planes legacy. Asignación funciona.
|
|
6. **`getMyPlan` retorna custom**: `/api/despachos/me/plan` retorna
|
|
`{ plan: 'custom', isTrialActive: false, ... }`.
|
|
|
|
## Implementación
|
|
|
|
~30 líneas netas en 4 archivos:
|
|
- `despacho-plans.ts` — agregar entrada custom (~12 líneas).
|
|
- `tenants.ts` (api client) — extender tipos (~3 líneas).
|
|
- `clientes/page.tsx` — dropdown + cleanup (~10 líneas).
|
|
- `planes-despacho/page.tsx` — banner Custom (~10 líneas).
|
|
|
|
Cambio chico, hago directo sin subagents. Una commit en Downloads + V.1.0.14
|
|
en OneDrive.
|