Files
HoruxDespachosNuevo/docs/sessions/2026-05-04-business-control-prueba-trial.md
Horux Dev 46846200da feat(sat): factura global + fecha_efectiva, fallback tenant-contribuyente, fix anio_global typo
Factura Global & fecha_efectiva:
- Migracion 045_factura_global.sql: periodicidad, meses_global, año_global, fecha_efectiva
- sat-parser.service.ts: extrae InformacionGlobal del XML
- sat.service.ts: calcFechaEfectiva con soporte bimestral (periodicidad 05)
- metricas-compute, dashboard, impuestos, cfdi, export, conciliacion, alertas:
  reemplaza fecha_emision-1h por COALESCE(fecha_efectiva, fecha_emision-1h)
- Script recalc-metricas.ts para recalculo manual

Fallback datos fiscales tenant → contribuyente:
- contribuyente.service.ts: fetchTenantFiscalData + mergeContribuyenteWithTenant
  rellena regimenFiscal, codigoPostal y domicilio cuando el contribuyente
  tiene el mismo RFC que el tenant y sus campos estan vacios
- contribuyente.controller.ts y contribuyente-config.controller.ts:
  pasan req.user!.tenantId al servicio

Fix critico SAT sync:
- sat.service.ts: anio_global → año_global en INSERT/UPDATE de CFDIs
  (la migracion creo 'año_global' con tilde; el codigo usaba 'anio_global',
   causando fallo en 100% de inserciones de CFDI)
- determineChunkMonths: salta sondeo si existe job previo con requestIds
- MAX_POLL_ATTEMPTS: 45 → 500 (~8h) para syncs iniciales grandes

Docs:
- docs/sessions/2026-05-22-factura-global-contribuyente-fallback.md
2026-05-22 15:52:10 +00:00

11 KiB

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:

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':

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

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.