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
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:
- 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).
- Invitación desde admin global: El administrador global puede enviar una invitación a un tenant para activar "Business Control Prueba".
- Periodo gratuito configurable: El admin define cuántos días dura la prueba (1-365 días).
- Todas las features de Business Control: Durante el trial, el tenant tiene 100 RFCs, usuarios ilimitados, API, SAT incremental, etc.
- 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.planse actualiza abusiness_controlal aceptar la invitación.subscription.plan=business_control,subscription.status='trial'.tenant.trialEndsAtse 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 condbMode: 'MANAGED'provisionConnector(): ya NO cambiadbModea'BYO'; solo guardaconnectorTokenEncyconnectorTunnelHostnameDESPACHO_PLANS:business_controlybusiness_cloudahora tienendbMode: 'MANAGED'- Seed de
despacho_plan_prices: actualizado aMANAGEDpara 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 owneracceptInvitation()— Valida token, actualiza tenant.plan, crea subscription trialgetInvitations()— Lista invitaciones con enrich de tenant datagetPendingInvitationForTenant()— Obtiene invitación pendiente para un tenantcancelInvitation()— 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
paymentUrlen 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
plannifrequencyen el POST. - Redirige a
/onboardingtras 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
- Admin global navega a Invitaciones Trial en el sidebar.
- Selecciona un despacho, elige plan (Business Control o Enterprise), define duración (ej. 60 días).
- Clic en "Enviar invitación".
- El backend crea el token y envía email al owner del despacho.
4.2 Owner recibe y acepta
- Owner recibe email con link
/invitacion/trial/{token}. - Abre el link (si no está logueado, va a login primero).
- Ve los detalles de la invitación y clic en "Aceptar invitación".
- 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
- Actualiza
- 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()retornaplan: 'business_control'en lugar de'trial'.- Feature-gate permite acceso a todas las funciones del plan.
4.4 Al vencer
plan-limits.middlewaredetectaneedsRenewal = 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.tsapps/api/src/controllers/trial-invitations.controller.tsapps/api/src/routes/trial-invitations.routes.tsapps/api/src/services/email/templates/trial-invitation.tsapps/api/prisma/migrations/20260507201624_add_trial_invitations/migration.sql
Backend (modificados)
apps/api/prisma/schema.prismaapps/api/prisma/seed.tsapps/api/src/app.tsapps/api/src/services/despacho.service.tsapps/api/src/controllers/despacho.controller.tsapps/api/src/services/email/email.service.tsapps/api/src/services/connector.service.tsapps/api/src/services/admin-dashboard.service.tspackages/shared/src/constants/despacho-plans.ts
Frontend (nuevos)
apps/web/app/invitacion/trial/[token]/page.tsxapps/web/app/(dashboard)/admin/invitaciones-trial/page.tsxapps/web/lib/api/trial-invitations.ts
Frontend (modificados)
apps/web/app/(auth)/register-despacho/page.tsxapps/web/app/(dashboard)/configuracion/planes-despacho/page.tsxapps/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_URLdel environment. Si no está seteado, fallback ahttps://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
dbModea'BYO'en ningún flujo. Si en el futuro se quiere usar la conexión BYO como fallback cuando la nube falla, implementarlo en eltenant.middleware.tscomo lógica de retry/fallback, no como modo principal.