# Limpieza de deuda técnica TypeScript

## Resumen

`tsc --noEmit` pasa de ~25 errores a **0** en `@horux/api` y `@horux/shared`. El proyecto corre con `tsx watch` que transpila sin chequear tipos, por eso la deuda había crecido sin afectar runtime. Algunos fixes resolvieron bugs latentes que no se habían manifestado por cobertura parcial de features.

## Motivación

Sin typecheck limpio:
- Regresiones se detectan sólo en runtime (después de desplegar)
- CI no puede bloquear PRs que rompen tipos
- IDE señala falsos positivos por todas partes, lo que entrena al desarrollador a ignorar el linter
- Fixes legítimos (como el bug runtime de `uploadCertificate`) quedan ocultos entre el ruido

## Alcance

Tocados **11 archivos** en 3 categorías:

### Quick wins (mecánicos, cero riesgo)

| Archivo | Problema | Fix |
|---------|----------|-----|
| `packages/shared/src/constants/roles.ts` | Constante `ROLES` sin entradas para `cfo` y `auxiliar` — los nuevos roles de v0.9.0 no se habían reflejado aquí | Agregadas dos entradas con permisos equivalentes (CFO = admin; Auxiliar = contador) |
| `apps/api/tsconfig.json` | Código usaba `document.querySelectorAll()` sin `DOM` lib; iteración `for...of` sobre `NodeList` sin `DOM.Iterable` | `lib: ["ES2022", "DOM", "DOM.Iterable"]` |
| `apps/api/src/controllers/bancos.controller.ts` | `parseInt(req.params.id)` — `req.params.id` es `string \| string[]` bajo `@types/express@5` | `parseInt(String(req.params.id))` |
| `apps/api/src/controllers/calendario.controller.ts` | Igual | Igual |
| `apps/api/src/controllers/conciliacion.controller.ts` | Igual | Igual |
| `apps/api/src/controllers/documentos.controller.ts` | Igual | Igual |
| `apps/api/src/controllers/facturacion.controller.ts` | `const { id } = req.params; facturapiService.downloadPdf(..., id)` — id tipado `string \| string[]` | `const id = String(req.params.id)` |
| `apps/api/src/controllers/alertas.controller.ts` | Igual al patrón de params | `String(req.params.id)` |

### Cambios de lógica (riesgo medio, comportamiento preservado)

| Archivo | Problema | Fix |
|---------|----------|-----|
| `apps/api/src/services/payment/subscription.service.ts` | `include: { users: { where: { role: 'admin' } } }` — el schema migró `User.role` → `User.rolId` con FK a tabla `Rol`. Los 3 include-nested donde se buscaba admin fallaban, lo que a su vez cascadeaba en ~7 errores TS2339 sobre `tenant.users[0]` porque el include inválido perdía la inferencia del shape con relación | `where: { rol: { nombre: 'admin' } }` navega la relación correctamente. **Comportamiento preservado:** sigue emailando únicamente a usuarios con rol exacto `'admin'`. CFO no recibe emails de pago — si se desea, es un cambio UX separado |
| `apps/api/src/services/impuestos.service.ts` | `result.push({ mes, ivaTrasladado, ... })` — el tipo `IvaMensual` exigía `id`, `año`, `estado`, `fechaDeclaracion` pero el cálculo nunca los producía. Frontend en `/impuestos` usa `row.estado === 'declarado'` — siempre resolvía `undefined !== 'declarado'` → badge "Pendiente" por accidente | Poblados 4 campos con semántica "calculado, sin declaración persistida": `id: 0`, `año`, `estado: 'pendiente'`, `fechaDeclaracion: null`. **Arregla bug silencioso del frontend** además del tipo |

### Fixes de runtime disfrazados de errores de tipos

| Archivo | Problema | Fix |
|---------|----------|-----|
| `apps/api/src/services/facturapi.service.ts` | `uploadCertificate(id, { cer, key, password })` — firma del SDK cambió a 4 args posicionales: `(id, cerFile: BinaryInput, keyFile: BinaryInput, password)`. Código pasaba un objeto donde el SDK esperaba un Buffer de certificado | `uploadCertificate(id, Buffer.from(cer, 'base64'), Buffer.from(key, 'base64'), password)`. **Este era un bug runtime latente** — habría fallado al primer intento real de subir CSD en producción. Nadie lo notó porque Facturapi está en modo test donde CSD no es requerido |
| `apps/api/src/services/facturapi.service.ts` | Tipo de `taxes` no declaraba `withholding`, pero el código usaba `...(t.withholding ? { withholding: true } : {})` | Agregado `withholding?: boolean` al tipo. Documenta lo que el código ya asumía |
| `apps/api/src/services/facturapi.service.ts` | `logoUrl: org.logo_url` donde `org.logo_url: string \| null` pero target esperaba `string \| undefined` | `org.logo_url ?? undefined` (coerción estándar null → undefined) |

### Infraestructura (no-cambio de arquitectura)

| Archivo | Problema | Fix |
|---------|----------|-----|
| `apps/api/src/config/tenant-migrations.ts` | `import.meta.url` + `fileURLToPath` para resolver path. TS con `NodeNext` + sin `"type": "module"` en package.json compila a CJS, donde `import.meta` es inválido | Reemplazado por `__dirname` directo — global nativo de Node en CJS. Cero cambios a deps, tsconfig o package.json |

## Decisiones descartadas (y por qué)

### Downgradear `@types/express@5.0.0` → `@types/express@4.17.21`

**Tentación:** los 7 errores de `string | string[]` son porque el tipado v5 anticipa params array de Express 5, pero el runtime es Express 4.21. Un downgrade de types los habría resuelto a todos en una línea.

**Por qué no:** alterar `package.json` + `pnpm install` requiere más testing de lo que el fix merece. Los casts `String(req.params.id)` son explícitos y funcionan en ambas versiones. Cuando migren a Express 5, esos casts seguirán siendo correctos.

### Cambiar a `"type": "module"` para eliminar `tenant-migrations.ts` con `import.meta`

**Tentación:** una línea en `package.json` y el error desaparece.

**Por qué no:** disparó 4 errores nuevos en `facturapi.service.ts`. El SDK de Facturapi no declara campo `"exports"` en su package.json, sólo `main`/`module`. Bajo ESM estricto, TS trata el default import como namespace en vez de clase, rompiendo `new Facturapi()` y `: Facturapi` como tipo. Resolverlo requeriría workarounds feos (`(Facturapi as any)`, destructuración de `.default`) en 4 sitios. **Regresión neta.**

### Expandir CFO a receptor de emails de pago

**Tentación:** según `CLAUDE.md`, CFO tiene mismo nivel de acceso que admin. Lógicamente debería recibir notificaciones.

**Por qué no:** este es un cambio UX explícito sobre quién recibe qué correos, no un fix de tipos. Si se desea, el cambio es:
```diff
- where: { rol: { nombre: 'admin' } }
+ where: { rol: { nombre: { in: ['admin', 'cfo'] } } }
```

## Verificación

```bash
# En apps/api
pnpm typecheck   # 0 errores
# En packages/shared
pnpm typecheck   # 0 errores
```

Dev server (`pnpm dev`) corre limpio después de todos los cambios:
- API `:4000` responde, Prisma queries ejecutan
- Web `:3000` sirve `/cfdi`, `/login`, `/dashboard`, `/impuestos` con 200

## Pendientes post-cleanup

1. **Verificar subida de CSD con certificado real** ahora que `uploadCertificate` usa la firma correcta. Caso de prueba: en modo producción de Facturapi, cargar un `.cer`/`.key` válido y confirmar que `is_production_ready` de la organización cambia a `true`.
2. **Verificar webhooks de MercadoPago** siguen llegando al admin — la query de include cambió de shape (de enum-role a relation-nombre).
3. **Decidir si CFO debe recibir emails de pago** (cambio UX opcional, no de tipos).
4. **Audit regular de typecheck en CI:** agregar `pnpm typecheck` como check obligatorio en PRs para que la deuda no vuelva a crecer.

## Decisiones que no se tocaron (siguen como deuda)

Ninguna relacionada con este cleanup. Los pendientes de producto (Nómina tipo N, Carta Porte, notificaciones email de alertas/recordatorios, SMTP local) siguen como estaban en v0.9.0.
