# Sesión: Trial Business Control Prueba (Invitación desde Admin Global) **Fecha:** 2026-05-04 **Feature:** Sistema de invitaciones de trial configurable para plan Business Control --- ## 1. Requerimiento Crear un trial específico para el plan **Business Control**, llamado "Business Control Prueba", con las siguientes características: 1. **Registro simplificado**: Los nuevos usuarios se registran sin escoger plan. Todos empiezan con el trial genérico actual (30 días, 3 RFCs, 1 usuario, MANAGED). 2. **Invitación desde admin global**: El administrador global puede enviar una invitación a un tenant para activar "Business Control Prueba". 3. **Periodo gratuito configurable**: El admin define cuántos días dura la prueba (1-365 días). 4. **Todas las features de Business Control**: Durante el trial, el tenant tiene 100 RFCs, usuarios ilimitados, API, SAT incremental, etc. 5. **Al vencer**: El tenant debe pagar la suscripción de Business Control para continuar. --- ## 2. Decisiones Arquitectónicas ### 2.1 No nuevo enum Plan No se agregó `business_control_trial` al enum `Plan`. En su lugar: - `tenant.plan` se actualiza a `business_control` al aceptar la invitación. - `subscription.plan` = `business_control`, `subscription.status` = `'trial'`. - `tenant.trialEndsAt` se recalcula según los días configurados. **Ventaja**: El feature-gate (`requireFeature`) y los límites (`DESPACHO_PLANS['business_control']`) funcionan automáticamente. ### 2.2 dbMode siempre MANAGED (Opción 1) **Decisión arquitectónica clave**: Todos los tenants, sin importar el plan, siempre tienen `dbMode: 'MANAGED'`. El conector/BYO es una **feature de respaldo** que se activa por separado sin cambiar el modo de la base de datos. **Razón**: El BYO es "como respaldo" para cuando fallen los servicios en la nube, pero muchos clientes no tendrán servidor físico desde el inicio. Business Control y Enterprise operan 100% en la nube; el conector local es un respaldo opcional. **Cambios aplicados**: - `signupDespacho()`: siempre crea tenant con `dbMode: 'MANAGED'` - `provisionConnector()`: ya NO cambia `dbMode` a `'BYO'`; solo guarda `connectorTokenEnc` y `connectorTunnelHostname` - `DESPACHO_PLANS`: `business_control` y `business_cloud` ahora tienen `dbMode: 'MANAGED'` - Seed de `despacho_plan_prices`: actualizado a `MANAGED` para esos planes - BD: `UPDATE despacho_plan_prices SET db_mode = 'MANAGED' WHERE plan IN ('business_control', 'business_cloud')` ### 2.3 Registro simplificado Se eliminó la selección de plan y frecuencia del registro de despacho (`/register-despacho`). Todos los nuevos usuarios empiezan con trial genérico de 30 días. --- ## 3. Cambios Implementados ### 3.1 Backend #### Migración Prisma — `TrialInvitation` Nueva tabla en `apps/api/prisma/schema.prisma`: ```prisma model TrialInvitation { id String @id @default(uuid()) tenantId String @map("tenant_id") invitedBy String @map("invited_by") plan String @default("business_control") durationDays Int @map("duration_days") status String @default("pending") token String @unique emailSentTo String? @map("email_sent_to") sentAt DateTime @default(now()) @map("sent_at") expiresAt DateTime @map("expires_at") acceptedAt DateTime? @map("accepted_at") createdAt DateTime @default(now()) @map("created_at") } ``` **Migración aplicada:** `20260507201624_add_trial_invitations` #### Nuevo Service: `apps/api/src/services/trial-invitations.service.ts` - `createInvitation()` — Crea invitación, genera token, envía email al owner - `acceptInvitation()` — Valida token, actualiza tenant.plan, crea subscription trial - `getInvitations()` — Lista invitaciones con enrich de tenant data - `getPendingInvitationForTenant()` — Obtiene invitación pendiente para un tenant - `cancelInvitation()` — Cancela invitación pendiente #### Nuevo Controller: `apps/api/src/controllers/trial-invitations.controller.ts` Endpoints: - `POST /api/invitations/trial` — Solo global admin. Body: `{ tenantId, plan?, durationDays }` - `GET /api/invitations/trial` — Solo global admin. Lista todas. - `GET /api/invitations/trial/pending` — Autenticado. Devuelve invitación pendiente para el tenant. - `POST /api/invitations/trial/:token/accept` — Autenticado. Owner del tenant. - `POST /api/invitations/trial/:id/cancel` — Solo global admin. - `GET /api/invitations/trial/token/:token` — Público (autenticado). Detalles de invitación. #### Nuevas Routes: `apps/api/src/routes/trial-invitations.routes.ts` Registradas en `app.ts` bajo `/api/invitations/trial`. #### Modificación: `apps/api/src/controllers/despacho.controller.ts` `getMyPlan()` ahora respeta `subscription.plan` cuando `status === 'trial'`: ```ts if (subscription?.status === 'trial' && subscription.plan && subscription.plan !== 'trial') { currentPlan = subscription.plan; // 'business_control' si es Business Control Prueba } else if (isTrialActive) { currentPlan = 'trial'; } else { currentPlan = String(tenant.plan); } ``` Schema Zod de signup: `plan` y `frequency` ahora son puramente opcionales (sin defaults forzados). #### Modificación: `apps/api/src/services/despacho.service.ts` - `signupDespacho()` ya no crea suscripción pagada durante el registro. - Todos los nuevos tenants empiezan con `plan: 'trial'`, `dbMode: 'MANAGED'`. - No se devuelve `paymentUrl` en el response. #### Nuevo template de email: `apps/api/src/services/email/templates/trial-invitation.ts` Email de invitación con link de activación y detalles del plan. #### Modificación: `apps/api/src/services/email/email.service.ts` Agregado método `sendTrialInvitation()`. ### 3.2 Frontend #### Registro simplificado: `apps/web/app/(auth)/register-despacho/page.tsx` - Eliminado step 3 (selección de plan y frecuencia). - Solo 2 pasos: datos del despacho/owner → selección de vertical. - No se envía `plan` ni `frequency` en el POST. - Redirige a `/onboarding` tras registro exitoso. #### Nueva página: `apps/web/app/invitacion/trial/[token]/page.tsx` - Muestra detalles de la invitación (plan, días, despacho). - Botón "Aceptar invitación" (requiere autenticación como owner). - Si no está logueado, redirige a login con redirect. - Estados: loading, inválida, expirada, aceptada, éxito. #### Modificación: `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` - Detecta invitación pendiente vía `GET /api/invitations/trial/pending`. - Muestra banner destacado: "Invitación especial — Business Control Prueba" con botón "Activar ahora". - Al aceptar, recarga el plan info y muestra mensaje de éxito. #### Nueva página de admin: `apps/web/app/(dashboard)/admin/invitaciones-trial/page.tsx` - Formulario para crear invitación: selector de despacho, plan, duración en días. - Tabla de historial con estados (Pendiente, Aceptada, Expirada, Cancelada). - Acción cancelar para invitaciones pendientes. #### Modificación: `apps/web/components/layouts/sidebar.tsx` Agregado link "Invitaciones Trial" a la navegación de admin global. #### Nuevo API client: `apps/web/lib/api/trial-invitations.ts` Helpers para consumir todos los endpoints de invitaciones. --- ## 4. Flujo de Uso ### 4.1 Admin envía invitación 1. Admin global navega a **Invitaciones Trial** en el sidebar. 2. Selecciona un despacho, elige plan (Business Control o Enterprise), define duración (ej. 60 días). 3. Clic en "Enviar invitación". 4. El backend crea el token y envía email al owner del despacho. ### 4.2 Owner recibe y acepta 1. Owner recibe email con link `/invitacion/trial/{token}`. 2. Abre el link (si no está logueado, va a login primero). 3. Ve los detalles de la invitación y clic en "Aceptar invitación". 4. El backend: - Actualiza `tenant.plan = 'business_control'` - Actualiza `tenant.trialEndsAt = now + 60 días` - Marca subscriptions trial anteriores como `trial_converted` - Crea nueva subscription con `plan: 'business_control', status: 'trial'` - Marca invitación como `accepted` 5. Owner es redirigido a `/configuracion/planes-despacho`. ### 4.3 Durante el trial - El tenant tiene todas las features de Business Control (100 RFCs, usuarios ilimitados, API, etc.). - `getMyPlan()` retorna `plan: 'business_control'` en lugar de `'trial'`. - Feature-gate permite acceso a todas las funciones del plan. ### 4.4 Al vencer - `plan-limits.middleware` detecta `needsRenewal = true`. - Bloquea escrituras (POST/PUT/DELETE) con código `SUBSCRIPTION_INACTIVE`. - Muestra banner: "Tu prueba gratuita terminó. Renueva tu plan para continuar." - Owner puede contratar Business Control desde `/configuracion/planes-despacho`. --- ## 5. Archivos Modificados/Creados ### Backend (nuevos) - `apps/api/src/services/trial-invitations.service.ts` - `apps/api/src/controllers/trial-invitations.controller.ts` - `apps/api/src/routes/trial-invitations.routes.ts` - `apps/api/src/services/email/templates/trial-invitation.ts` - `apps/api/prisma/migrations/20260507201624_add_trial_invitations/migration.sql` ### Backend (modificados) - `apps/api/prisma/schema.prisma` - `apps/api/prisma/seed.ts` - `apps/api/src/app.ts` - `apps/api/src/services/despacho.service.ts` - `apps/api/src/controllers/despacho.controller.ts` - `apps/api/src/services/email/email.service.ts` - `apps/api/src/services/connector.service.ts` - `apps/api/src/services/admin-dashboard.service.ts` - `packages/shared/src/constants/despacho-plans.ts` ### Frontend (nuevos) - `apps/web/app/invitacion/trial/[token]/page.tsx` - `apps/web/app/(dashboard)/admin/invitaciones-trial/page.tsx` - `apps/web/lib/api/trial-invitations.ts` ### Frontend (modificados) - `apps/web/app/(auth)/register-despacho/page.tsx` - `apps/web/app/(dashboard)/configuracion/planes-despacho/page.tsx` - `apps/web/components/layouts/sidebar.tsx` --- ## 6. Deploy ```bash cd /root/HoruxDespachosNuevo # Backend: migración ya aplicada + build implícito en reload # Frontend: pnpm build --filter=@horux/web # PM2 reload: pm2 reload horux-api pm2 reload horux-web ``` **Estado:** ✅ Exitoso. Build sin errores. Procesos reiniciados. --- ## 7. Notas para Futuras Sesiones - Si se quiere agregar más planes a las invitaciones (ej. `mi_empresa_plus`), solo hay que agregarlos al Select del frontend de admin. - El email de invitación usa `FRONTEND_URL` del environment. Si no está seteado, fallback a `https://app.horux360.com`. - La invitación expira en 7 días desde el envío (configurable en `createInvitation()`). - Si un tenant en trial genérico acepta Business Control Prueba, su trial anterior se pierde (se sobreescribe `trialEndsAt`). Esto es intencional. - Considerar agregar un cron que marque invitaciones expiradas automáticamente (hoy solo se marcan al intentar aceptar). - **dbMode siempre MANAGED**: El conector/BYO es una feature de respaldo independiente. Nunca cambiar `dbMode` a `'BYO'` en ningún flujo. Si en el futuro se quiere usar la conexión BYO como fallback cuando la nube falla, implementarlo en el `tenant.middleware.ts` como lógica de retry/fallback, no como modo principal.