# Límite de 5 RFCs durante trial gratuito ## Contexto Despachos en periodo de prueba (30 días) pueden agregar RFCs sin restricción. El owner pidió un límite duro de 5 RFCs durante trial — para forzar al contador a contratar un plan si necesita gestionar más. ## Reglas | Estado | Límite RFCs | |---|---| | Trial activo (`tenant.trialEndsAt > now`) | **5 contribuyentes activos** (boundary: 5 OK, 6 bloqueado) | | Trial expirado | Aplica el límite del plan vigente; este spec no agrega nada nuevo | | Plan pagado (sin trial activo) | Sin nuevo límite (los del plan ya existen y son out of scope) | ## Cambios — Backend ### `apps/api/src/controllers/contribuyente.controller.ts` Constante local al archivo: ```ts const TRIAL_MAX_CONTRIBUYENTES = 5; ``` En el handler `create`, antes del `createContribuyente`: ```ts const tenant = await prisma.tenant.findUnique({ where: { id: req.user!.tenantId }, select: { trialEndsAt: true }, }); const isTrialActive = tenant?.trialEndsAt ? tenant.trialEndsAt > new Date() : false; if (isTrialActive) { const activeCount = await countActiveContribuyentes(req.tenantPool!); if (activeCount >= TRIAL_MAX_CONTRIBUYENTES) { return next(new AppError( 403, `Durante el periodo de prueba puedes gestionar hasta ${TRIAL_MAX_CONTRIBUYENTES} contribuyentes. Contrata un plan para agregar más.`, )); } } ``` Imports: agregar `prisma` desde `../config/database.js` (ya está disponible en otros controllers). ## Cambios — Frontend ### `apps/web/app/(dashboard)/contribuyentes/page.tsx` Fetch del plan info (sigue patrón existente en `planes-despacho/page.tsx`): ```ts const { data: planInfo } = useQuery({ queryKey: ['my-plan-info'], queryFn: () => apiClient.get<{ isTrialActive: boolean }>('/despachos/me/plan').then(r => r.data), }); const isTrialActive = planInfo?.isTrialActive ?? false; const activeCount = (contribuyentes ?? []).filter(c => c.active !== false).length; const trialAtLimit = isTrialActive && activeCount >= 5; ``` Modificar los 2 botones "Agregar RFC" (línea 70 y 78) para reflejar el estado: ```tsx ``` (Mismo patrón en el botón "Agregar primer RFC" — aunque cuando `activeCount === 0` el `trialAtLimit` es `false`, así que ese botón nunca se deshabilita. Aún así, aplico el atributo `disabled={trialAtLimit}` por consistencia defensiva.) Mensaje del tooltip (literal del owner): "Límite de contribuyentes para la prueba gratuita, para continuar agregando contribuyentes, selecciona un plan." ## No-cambios - Schema BD. - Cron de trial (`expireTrials`). - Mi Empresa hard limit a 1 RFC (sigue siendo solo billing-only, fuera de scope). - `tenant.cfdiLimit`, `tenant.usersLimit` — no se tocan. ## Riesgos - **Race condition**: si dos creaciones concurrentes ven `count=4` y ambas pasan, podríamos terminar con 6. Improbable en flujo manual UI; no se mitiga (costo > beneficio). - **Trial → paid mid-creación**: si el contador paga mientras está en 5 RFCs, el `trialEndsAt` no se modifica (sigue en futuro), pero la subscription ahora tiene status `authorized`. Per la lógica actual, el trial sigue "activo" hasta que `trialEndsAt < now`. El usuario pagado seguirá viendo el límite de 5 hasta que expire el trial. **Aceptable**: el owner gana dinero adicional el día que el contador convierte, no antes. Si se quiere lift inmediato, modificar la lógica de `isTrialActive` para excluir trials pagados — out of scope para este spec. ## Plan de pruebas 1. `pnpm typecheck` shared + api + web targeted: PASS. 2. Tenant en trial con 4 contribuyentes activos: - UI: botón "Agregar RFC" habilitado. - API: `POST /api/contribuyentes` con datos válidos retorna 201. 3. Tenant en trial con 5 contribuyentes activos: - UI: botón "Agregar RFC" deshabilitado, tooltip visible al hover. - API: `POST /api/contribuyentes` retorna 403 con el mensaje del spec. 4. Tenant trial expirado con 5 contribuyentes: - UI: botón habilitado. - API: 201 (puede crear el 6º — sin límite trial). 5. Tenant pagado (Business Control) con 5 contribuyentes: - UI: botón habilitado. - API: 201. ## Implementación ~15 líneas backend + ~8 líneas frontend. Cambio chico, una commit en Downloads + V.1.0.11 en OneDrive.