# apps/fiscal/ — Estación Fiscal

Reglas al trabajar en `apps/fiscal/**`. Se carga solo al tocar este directorio.

## Qué es y la frontera que no se cruza

Programa Windows (exe único, Node/TS) instalado en el equipo del negocio.
Una instalación, un RIF: no es servicio compartido. Escucha SOLO en
`127.0.0.1:8739` (el Cerebrito usa 8737 y 8738, no se pisan).

LA REGLA MADRE: Gustito Xpress le entrega a `EstacionFiscal` una venta ya
cobrada y recibe de vuelta el número del documento (`estacion.ts:1-11`). No
calcula impuestos, no numera, no toca el libro. Nunca escribas código que le
dé a Gustito una vía para editar, borrar o reordenar un asiento del libro
fiscal — eso viola inalterabilidad (Providencia SNAT/2024/000121, art. 3-4).
Las correcciones son SIEMPRE notas de crédito/débito nuevas que referencian
al documento original (`libro.ts:corregir`, `armador.ts:armarNotaCredito`).

## Piezas canónicas

- **`libro.ts`** — el libro fiscal, JSONL append-only. `asentar()` es la
  única forma de escribir; `huella()` firma el asiento con orden de campos
  FIJO (nunca `JSON.stringify` del objeto completo — si se reordena el
  objeto el hash no puede cambiar de golpe). Numeración ciega por tipo
  (`numero`) más un correlativo de control continuo (`control`). Cada
  escritura es `openSync 'a'` + `writeSync` + `fsyncSync`. `verificar()`
  recorre la cadena de `hash_anterior` y dice en qué línea se rompió.
- **`cola.ts`** — `ColaTransmision.procesar()` transmite en orden estricto
  (si uno falla por red, se detiene ahí, no salta al siguiente). `esRechazo()`
  distingue rechazo de imprenta (se marca y no se reintenta) de falla de red
  (se reintenta). El asiento ya está en el libro pase lo que pase con la cola.
- **`respaldo.ts`** — `RespaldoCifrado` cifra cada asiento por separado con
  AES-256-GCM (nunca descifra-y-reescribe el archivo entero). La frase de
  recuperación vive aparte (`rutaFrase`/`guardarFrase`), nunca junto al
  respaldo. Escritura de cabecera/estado siempre tmp + rename (`renameSync`).
- **`armador.ts`** — `armarDocumento()`/`desglosar()` calculan base e IVA por
  renglón; `ALICUOTAS_LEY = [0, 8, 16, 31]` valida que la alícuota exista en
  la ley (una alícuota ausente NO es lo mismo que exenta — se rechaza). IGTF
  vía `IGTF_PORCENTAJE` solo si `emisor.igtf_activo` está encendido a mano.
- **`acceso.ts`** — roles `dueno` / `cajero` / `auditor` con permisos fijos
  en `PERMISOS`. Claves con `scryptSync` + sal, comparación con
  `timingSafeEqual` (nunca `===` sobre hashes ni claves). `frenado()`/
  `entrarConClave()` implementan el freno tras `MAX_FALLIDOS` intentos.
- **`reloj.ts`** — `Reloj.aprueba()` es el guardián: no deja sellar si el
  equipo está corrido contra la nube, ni si la fecha queda antes del último
  asiento del libro. `guardar()` escribe con tmp + `renameSync`.

## Anti-patrones (NO hacer)

| No | Sí | Por qué |
|---|---|---|
| Agregar un método para editar/reordenar/borrar un asiento en `libro.ts` | Nota de crédito/débito vía `armarNotaCredito` + `LibroFiscal.corregir` | Rompe inalterabilidad (000121 art. 3); el libro solo agrega |
| Escribir en `estado/` con `openSync(ruta, 'w')` directo | Patrón tmp + `renameSync` (ver `reloj.ts:guardar`, `respaldo.ts:guardarFrase`, `acceso.ts:guardar`) | Un corte de luz a mitad de un `'w'` deja el archivo truncado; tmp+rename no |
| Confiar en `base_usd`/`iva_usd` que manda el cliente o Gustito | Recalcular siempre con `armarDocumento`/`desglosar` en `armador.ts` | El monto fiscal se calcula del lado del RIF, nunca del lado de gestión |
| Interpolar datos de cliente/venta en el HTML de `servidor.ts` sin pasar por `escapar()`/`esc()` | Usar `escapar()` (línea 194) para todo lo que viene de fuera | XSS sobre la propia consola de la estación (dueño/cajero/auditor) |
| Generar una frase de recuperación nueva cuando ya hay asientos cifrados en `respaldo.jsonl` | Pedir al dueño que restaure la frase de papel guardada aparte | `fraseDeRecuperacion()` ya lanza error a propósito en ese caso; una frase nueva deja los asientos viejos irrecuperables |

## Al cambiar comportamiento

Actualizar en el MISMO commit: `MEMORIA_TECNICA.md` (siempre que cambie algo
que la Providencia exige) y `COTEJO_ART7.md` si el cambio toca el formato o
contenido de la factura (art. 7 de la 000102).

Antes de dar por buena cualquier modificación, correr
`npm run prueba --workspace apps/fiscal` (ejecuta las suites en `test/*.mjs`
en orden fijo desde `package.json`). Hoy son 24 suites — si agregas una
funcionalidad nueva del libro, la cola, el respaldo o el reloj, agrega su
`prueba_*.mjs` y súmala a la cadena del script `prueba`, no la dejes suelta.
