Initial commit - Horux Despachos NL
This commit is contained in:
129
docs/superpowers/specs/2026-04-27-trial-rfc-limit-design.md
Normal file
129
docs/superpowers/specs/2026-04-27-trial-rfc-limit-design.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# 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
|
||||
<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.
|
||||
Reference in New Issue
Block a user