Initial commit - Horux Despachos NL
This commit is contained in:
251
docs/plans/2026-04-26-admin-global-setup.md
Normal file
251
docs/plans/2026-04-26-admin-global-setup.md
Normal file
@@ -0,0 +1,251 @@
|
||||
# Setup admin global + gestión de clientes (2026-04-26 turno tardío)
|
||||
|
||||
Sesión continuación del día. Foco: habilitar al admin global del fork para
|
||||
que pueda operar como plataforma — gestión de clientes, edición de catálogos
|
||||
(planes, add-ons), auto-facturación de pagos, y experiencia de login dedicada.
|
||||
|
||||
> Relacionado: `2026-04-26-session.md` (índice del día), `2026-04-26-sprints-1-2-3.md`
|
||||
> (sprints anteriores), y los docs específicos de IVA y notificaciones.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bootstrap admin global del fork
|
||||
|
||||
El fork `Horux_despacho` no tenía tenant raíz ni `platform_admin`. Ejecutado:
|
||||
|
||||
```bash
|
||||
HORUX_ADMIN_EMAIL=carlos@horuxfin.com \
|
||||
HORUX_ADMIN_NOMBRE=Carlos \
|
||||
HORUX_TI_EMAIL=ivan@horuxfin.com \
|
||||
HORUX_TI_NOMBRE=Ivan \
|
||||
pnpm bootstrap:admin-global
|
||||
```
|
||||
|
||||
Crea:
|
||||
- Tenant `Horux 360` (RFC `HTS240708LJA`, plan enterprise, dbMode MANAGED).
|
||||
- `carlos@horuxfin.com` con rol `platform_admin` (admin global).
|
||||
- `ivan@horuxfin.com` con rol `platform_ti` (TI superset).
|
||||
|
||||
Contraseña fijada manualmente a `Admin12345!` con bcrypt cost 12 +
|
||||
`tokenVersion: increment` para invalidar JWTs viejos.
|
||||
|
||||
**Cuentas finales del fork** (todas con `Admin12345!`):
|
||||
- `carlos@horuxfin.com` — platform_admin
|
||||
- `ivan@horuxfin.com` — platform_ti
|
||||
- `jd@demo.com` — owner Patito
|
||||
- `jf@demo.com` — owner Zorro
|
||||
- `supervisor@patito.com` / `auxiliar@patito.com` / `cliente@patito.com`
|
||||
|
||||
---
|
||||
|
||||
## 2. Login y home redirect
|
||||
|
||||
`apps/web/app/(auth)/login/page.tsx` ahora detecta el admin global tras el
|
||||
login y lo manda directo a `/clientes` (gestión de tenants), no a `/dashboard`
|
||||
(operativo despacho). `dashboard/page.tsx` también tiene `useEffect` defensivo
|
||||
que redirige si llega ahí por URL directa.
|
||||
|
||||
```ts
|
||||
const isGlobalAdmin = isGlobalAdminRfc(response.user?.tenantRfc, userRole, platformRoles);
|
||||
if (isGlobalAdmin) router.push('/clientes');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sidebar — banner "Configuración inicial" oculto
|
||||
|
||||
`components/layouts/sidebar.tsx`: el banner que linkea a `/onboarding` solo se
|
||||
muestra cuando `(!contribuyentes || contribuyentes.length === 0) && role !== 'cliente' && !isGlobalAdmin`.
|
||||
El admin global nunca tendrá contribuyentes en su tenant raíz Horux 360 — el
|
||||
banner sería ruido permanente.
|
||||
|
||||
(Una versión inicial también limpiaba todos los items operativos del sidebar
|
||||
para el admin global. Revertida — el admin global mantiene acceso completo
|
||||
para impersonar tenants. Solo cambia el banner.)
|
||||
|
||||
---
|
||||
|
||||
## 4. `/admin/usuarios` — fix `roleLabels[role].icon` undefined
|
||||
|
||||
Bug latente desde que se introdujeron los roles del despacho. `roleLabels`
|
||||
solo tenía `owner/contador/visor` (Horux 360 mainline); cuando aparecía un
|
||||
user con rol `cfo`, `supervisor`, `auxiliar` o `cliente`, el lookup retornaba
|
||||
`undefined` y `roleInfo.icon` lanzaba TypeError.
|
||||
|
||||
Fix: agregar los 7 roles + `defaultRoleInfo` como fallback defensivo:
|
||||
|
||||
```ts
|
||||
const roleLabels: Record<string, { label: string; icon: typeof Shield; color: string }> = {
|
||||
owner: { label: 'Dueño', icon: Shield, color: 'text-primary' },
|
||||
cfo: { label: 'CFO', icon: Briefcase, color: 'text-indigo-600' },
|
||||
contador: { label: 'Contador', icon: Calculator, color: 'text-green-600' },
|
||||
supervisor: { label: 'Supervisor', icon: UserCheck, color: 'text-blue-600' },
|
||||
auxiliar: { label: 'Auxiliar', icon: UserCog, color: 'text-cyan-600' },
|
||||
cliente: { label: 'Cliente', icon: User, color: 'text-amber-600' },
|
||||
visor: { label: 'Visor', icon: Eye, color: 'text-muted-foreground' },
|
||||
};
|
||||
const defaultRoleInfo = { label: 'Sin rol', icon: User, color: 'text-muted-foreground' };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `/configuracion/precios-suscripcion` — planes despacho
|
||||
|
||||
La página leía `plan_prices` (BD central, planes Horux 360 legacy:
|
||||
starter/business/business_ia/enterprise). En el fork no aplican.
|
||||
|
||||
**Reescrita** para mostrar los 4 planes despacho desde `DESPACHO_PLAN_PRICES`:
|
||||
|
||||
| Plan | Mensual | Anual 1er año | Anual renovación | RFCs | Timbres/mes |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| Mi Empresa | $580 | $5,800 | $5,800 | 1 | 50 |
|
||||
| Mi Empresa + | $900 | $9,000 | $9,000 | 1 | 50 |
|
||||
| Business Control | No aplica | $25,850 | $25,850 | 100 | 0 |
|
||||
| Enterprise | No aplica | $43,000 | $43,000 | 100 | 0 |
|
||||
|
||||
**Modo read-only** porque los precios viven en `DESPACHO_PLAN_PRICES`
|
||||
(catálogo estático en `@horux/shared`).
|
||||
|
||||
### Capacidad latente para edición
|
||||
|
||||
Migración aplicada `20260426230000_despacho_plan_prices`:
|
||||
|
||||
```sql
|
||||
CREATE TABLE despacho_plan_prices (
|
||||
plan TEXT PRIMARY KEY,
|
||||
monthly DECIMAL(10,2),
|
||||
first_year DECIMAL(10,2) NOT NULL,
|
||||
renewal DECIMAL(10,2) NOT NULL,
|
||||
permite_monthly BOOLEAN NOT NULL DEFAULT false,
|
||||
updated_at TIMESTAMP NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
Seed inicial con los 4 planes y sus valores. Permite migrar a editable cuando
|
||||
se quiera — service + endpoints + UI editable quedan como follow-up.
|
||||
|
||||
---
|
||||
|
||||
## 6. Gestión de clientes (`/clientes`)
|
||||
|
||||
Página enriquecida con KPIs y operaciones para el admin global.
|
||||
|
||||
### Backend
|
||||
- **Service**: `apps/api/src/services/admin-clientes.service.ts`
|
||||
- `getClientesStats({ from, to })` — devuelve:
|
||||
- `suscripcionesPorPlan`: groupBy plan con count (status=authorized).
|
||||
- `ingresos`: sum + count de `Payment.amount` con status=approved en rango.
|
||||
- `noRenovaciones`: subs con `currentPeriodEnd` en rango y status terminal
|
||||
(cancelled / trial_expired / paused) + datos del tenant.
|
||||
- `usuariosPorCliente`: count de memberships activos por tenant.
|
||||
- `getTenantUsuarios(tenantId)` — drill-down: lista users + rol + isOwner.
|
||||
- **Controller**: `controllers/admin-clientes.controller.ts` con
|
||||
`requireStaff(req)` (platform_admin/ti).
|
||||
- **Routes**: `routes/admin-clientes.routes.ts` montadas en
|
||||
`/api/admin/clientes` (`GET /stats`, `GET /:tenantId/usuarios`).
|
||||
|
||||
### UI
|
||||
- Selector de rango de fechas (default mes en curso).
|
||||
- 4 KPI cards: clientes registrados / suscripciones activas (con breakdown por
|
||||
plan en pie) / ingresos del periodo / no renovaciones.
|
||||
- Tabla "Clientes que no renovaron" expandible con detalle (cliente, RFC, plan,
|
||||
vencimiento, status).
|
||||
- En cada item de la lista de clientes, click sobre el contador de usuarios
|
||||
abre un modal con la lista (email, nombre, rol, badge de owner).
|
||||
|
||||
---
|
||||
|
||||
## 7. Auto-facturación de pagos al cliente
|
||||
|
||||
CLAUDE.md ya documentaba `invoicing.service.ts:emitInvoiceIfApplicable`. Cada
|
||||
webhook `payment.approved` lo dispara. Idempotente por `Payment.facturapiInvoiceId`,
|
||||
fail-soft si Facturapi falla.
|
||||
|
||||
### Ajustes en este turno
|
||||
- `PLAN_LABELS` extendido con los 4 planes despacho. Antes la factura mostraba
|
||||
el codename (`mi_empresa`, `business_cloud`) en lugar de "Mi Empresa" /
|
||||
"Enterprise".
|
||||
- Texto "Horux 360" → "Horux Despachos" en la descripción del CFDI y en el
|
||||
concepto de timbres.
|
||||
|
||||
### Prerequisito operativo (manual del admin)
|
||||
|
||||
El emisor de las facturas es el tenant Horux 360 (RFC `HTS240708LJA`).
|
||||
Requiere:
|
||||
|
||||
1. **CSD válido** subido vía `/configuracion/csd` por Carlos (admin global).
|
||||
2. `tenants.facturapi_org_id` poblado tras crear la organización en Facturapi.
|
||||
|
||||
Hoy la columna está en `null` para el tenant recién bootstrapeado. Mientras
|
||||
falte, `emitInvoiceIfApplicable` registra error en consola y `Payment.facturapiInvoiceId`
|
||||
queda null. **Pendiente operativo**: que Carlos suba el CSD.
|
||||
|
||||
---
|
||||
|
||||
## 8. Gestión de add-ons (`/configuracion/addons`)
|
||||
|
||||
Página nueva para que el admin global edite el catálogo de add-ons.
|
||||
|
||||
### Backend
|
||||
- **Controller**: `controllers/admin-addons.controller.ts`
|
||||
- `listCatalogo` — incluye count de suscripciones activas por add-on.
|
||||
- `updateCatalogoItem(id, { nombre?, precio?, active? })` con audit log.
|
||||
- **Routes**: `/api/admin/addons/catalogo` (`GET`, `PUT /:id`). `requireStaff`.
|
||||
|
||||
### UI
|
||||
- Tabla con columnas: codename, nombre (editable), precio MXN (editable),
|
||||
frecuencia, suscripciones activas (read-only), estado (badge), acciones.
|
||||
- Edición inline con dos botones (guardar / cancelar) por fila.
|
||||
- Toggle de activación con **confirmación condicional**: si el add-on tiene
|
||||
≥1 suscripción activa, pide confirmar antes de desactivar.
|
||||
- Card link agregada al `/configuracion` raíz solo visible para admin global.
|
||||
|
||||
### Add-ons del catálogo actual
|
||||
- `lolita_ia_contribuyente` — Lolita IA (por contribuyente) — $250/mes
|
||||
- `modulo_ia` — Módulo IA Fiscal — $390/mes
|
||||
- `rfcs_extra_10` — +10 RFCs adicionales — $190/mes
|
||||
- `rfcs_extra_50` — +50 RFCs adicionales — $690/mes
|
||||
- `timbres_extra_500` — +500 timbres mensuales — $490/mes
|
||||
- `contribuyente_extra_business_cloud` — Contribuyente adicional — $45/mes (overage automático Business Control / Enterprise)
|
||||
|
||||
---
|
||||
|
||||
## 9. Sync al repo OneDrive
|
||||
|
||||
Tras cada bloque de cambios, ejecutado:
|
||||
|
||||
```powershell
|
||||
robocopy "C:\Users\chtr1\Downloads\Horux_despacho" "C:\Users\chtr1\OneDrive\Documentos\GitHub\Horux_despachos" /E `
|
||||
/XD node_modules .next .turbo dist .pnpm data email-previews .git xmls `
|
||||
/XF .env .env.local "*.log" tsconfig.tsbuildinfo /NFL /NDL /NJH
|
||||
```
|
||||
|
||||
Excluye `node_modules`, builds, caches, secretos, datos de tenants, y el
|
||||
`.git/` del destino para preservar el repo origen.
|
||||
|
||||
---
|
||||
|
||||
## 10. Pendientes derivados
|
||||
|
||||
- **Cargar CSD del Horux 360** vía `/configuracion/csd` para activar
|
||||
auto-facturación real. Sin esto, `emitInvoiceIfApplicable` falla silencioso.
|
||||
- **Hacer editable `DespachoPlanPrice` desde UI**: la migración + seed están
|
||||
aplicados pero falta service + endpoint + UI editable. Mientras tanto la
|
||||
página de precios queda read-only.
|
||||
- **Notificación al desactivar add-on con suscripciones activas**: hoy se
|
||||
bloquean las contrataciones nuevas pero las activas siguen vigentes. Quizá
|
||||
enviar email al owner avisando que el add-on quedará "deprecated".
|
||||
- **Visual cue del prerequisito faltante de Facturapi org**: agregar alerta
|
||||
en `/clientes` o `/configuracion` cuando el tenant Horux 360 no tenga
|
||||
`facturapi_org_id` para que Carlos lo configure.
|
||||
|
||||
---
|
||||
|
||||
## 11. Pendientes históricos (sin cambio)
|
||||
|
||||
- SMTP `.env` prod (cron de notificaciones loguea a consola sin SMTP).
|
||||
- Cloudflare Tunnel + `FRONTEND_URL` HTTPS dev.
|
||||
- Typecheck web cleanup (~18 errores preexistentes).
|
||||
- Nómina (tipo N) y Carta Porte sin implementar.
|
||||
- SAT rejections — esperando captura real.
|
||||
- Re-notificación bug 2.4 visibilidad auxiliares (sin reproducción específica).
|
||||
Reference in New Issue
Block a user