Files
HoruxDespachosNuevo/docs/superpowers/specs/2026-04-27-trial-rfc-limit-design.md

4.5 KiB

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:

const TRIAL_MAX_CONTRIBUYENTES = 5;

En el handler create, antes del createContribuyente:

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

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:

<Button
  onClick={() => { resetForm(); setShowDialog(true); }}
  disabled={trialAtLimit}
  title={trialAtLimit ? 'Límite de contribuyentes para la prueba gratuita, para continuar agregando contribuyentes, selecciona un plan.' : undefined}
  className="flex items-center gap-2"
>
  <Plus className="h-4 w-4" /> Agregar RFC
</Button>

(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.