# Plan de ejecución — Facturación electrónica (post-MVP)

> Estado base: factura afecta (33) y nota de crédito (61) **emitidas y aceptadas en producción**
> (EVENTSUD, palena, Res. 80/22-08-2014). Flujo automático POS → cola `dte` → SII funcionando.
> Este documento ordena el trabajo restante en bloques y pasos. Marcar `[x]` al cerrar cada paso.

---

## Convenciones
- Cada paso indica: **archivos**, **qué hacer**, **cómo se prueba**.
- Regla de oro (vigente): **no emitir DTE real al SII sin luz verde explícita**; si un error real se
  repite 2 veces seguidas, pausar y diagnosticar. Commits granulares.
- Set de pruebas SII: facturas usan `TpoDocRef=SET`; boletas usan `CodRef=SET` + `RazonRef=CASO-N`.

---

## 🔖 HANDOFF — pendientes para la próxima persona

Implementado: facturas (33/34), boletas (39/41), notas de crédito (61) y guías de despacho (52),
con su flujo POS, impresión (hoja + térmico 80mm), guardas de folios y anulación. **Probado y
aceptado en producción:** factura 33, NC 61 y boletas 39. Lo que queda por atender:

### 1. Bloqueado por el usuario — cargar CAFs de producción y probar
- Faltan **CAF 34** (factura exenta), **CAF 52** (guía) y **renovar el CAF 61** (los folios se fueron
  usando). El **CAF 39** y **CAF 34**… 39 ya cargado; 34 cargado pero verificar folios.
- Con cada CAF: **emitir + anular** ese tipo y verificar aceptación. La **anulación de guías** usa el
  mismo botón "Anular / Nota de Crédito" (emite NC 61 con `CodRef=1` referenciando la 52); el flujo
  ya lo soporta pero **no se ha probado en vivo** (falta CAF 52 + CAF 61 con folios).

### 2. Bloques NO implementados (pendientes principales)
- **C — Almacenamiento S3** (detalle en el Bloque C más abajo): la app ya guarda XML+PDF por empresa
  en un disco Laravel; solo falta **configurar el disco `s3` y `DTE_STORAGE_DISK=s3`**. No empezado.
- **D1 — Worker en Linux** (detalle en D1): hoy el worker corre como proceso suelto en Windows. En el
  servidor Linux hay que dejarlo como **servicio systemd**. ⚠️ **El RVD diario de boletas (A2)
  también depende** del cron del scheduler en Linux. No empezado.
- **D2 — BD/servidor por cliente**: decisión de arquitectura mayor, sin empezar.

### 3. Detalles operativos a revisar antes de producción
- **`aconta-fact/plugins/api.ts`**: `DEFAULT_MAIN_API_BASE_URL` está apuntando al **backend local**
  (para pruebas). **REVERTIR a `https://sistema.a-conta.cl/api/public/api`** antes de desplegar.
- **Entorno local (XAMPP PHP 8.2) NO tiene OPcache** → los cambios de código se reflejan al instante.
  El "PDF viejo" que se veía era **caché del navegador** (resuelto con headers `no-cache` en
  `dte/{id}/pdf`). En producción, si se activa OPcache, dejar `opcache.validate_timestamps=1`.
- **Pendiente visual menor**: la separación "Forma de pago: <valor>" en el PDF formato hoja quedó por
  revisar (se ve algo pegada según la impresora/navegador).

---

## FASE 0 — Cierre del MVP actual ✅ (worker queda para D1)
- [x] Revertir frontend `aconta-fact/plugins/api.ts` → `DEFAULT_MAIN_API_BASE_URL` a producción.
- [x] Limpieza de datos de prueba: borradas ventas POS huérfanas sin DTE (2,3,4,5) + sus detalles.
      Los DteDocumentos de producción se **conservan** (son fiscales). Las facturas de prueba de
      producción (folios 4,5,6,7) quedaron **anuladas** con NC (folios 1,2,3,4), balance cuadrado.
      Nota: los ~120 DTE de ambiente `certificacion` se dejaron en la BD local (no urgían).
- [x] Commits granulares: backend (montos netos POS, CafService+splitRut, seeder giro, este plan) y
      frontend (puerto 3000, fix subida certificado, mejoras registros).
- [ ] Worker en producción → ver **FASE D1** (en Linux, no Windows).

---

## Robustez / guardas (transversal)
- [x] **Anulación única + NC relacionada**: columna `dte_documentos.anulado_por_dte_id` + relación
      `anuladoPor()`. Al anular (NC total) se guarda el vínculo; el controlador rechaza una segunda
      anulación (`ya_anulado`). El POS oculta el botón "Anular" y muestra "Anulada · NC N°X" + la NC
      relacionada. Backfill aplicado a los DTE ya anulados.
- [!] **OPcache**: el PDF de muestra se genera al emitir. Si se cambia el template `preview.php`,
      reiniciar el servicio web (o `opcache_reset`) para que el servidor tome el cambio; los PDF ya
      cacheados (`pdf_path`) se regeneran al limpiar esa columna.
- [x] **Guarda de folios CAF**: `CafService::verificarDisponibilidad()` (sin reservar) distingue
      sin_caf / vencido / sin_folios. `PosVentaController` **bloquea la venta** (rollback + 422) y
      `PosNotaCreditoController` rechaza con mensaje claro si no hay folios. Endpoint
      `GET /dteCafs/disponibilidad[?tipo_dte=]` + aviso proactivo en el POS (banner bajo el selector
      de tipo de documento) y mensajes claros en venta y anulación.

---

## BLOQUE A — Tipos de DTE faltantes

### A1. Factura exenta (34) — ✅ código listo y certificado; falta CAF 34 producción
- Archivos: `Services/DteXmlBuilder.php` (maneja `IndExe`/`MntExe`), `Services/DataMapper.php`,
  `Http/Controllers/Pos/PosVentaController.php`, `aconta-fact/pages/pos/ventas/punto-de-venta.vue`.
- Pasos:
  - [x] POS marca exento: `ConDocTributario` cod 34 ya existe y aparece en el selector; `esDocExento`
        lo detecta por nombre. **Bug corregido**: el watcher solo recalculaba líneas libres → los
        artículos de catálogo quedaban afectos en una 34. Ahora recalcula TODO el carrito.
  - [x] Garantía backend: en tipo 34, `PosVentaController` fuerza cada línea a exento con el monto
        bruto (la 34 nunca lleva IVA aunque el front mande un item afecto). Lógica validada en tinker.
  - [x] Certificación SII: la 34 **ya está certificada** (decenas de DTE 34 aceptados en ambiente
        `certificacion`, p.ej. folios 1–14).
  - [ ] **PENDIENTE (acción usuario):** cargar **CAF tipo 34 de producción** desde el portal SII
        (hoy solo hay CAF 34 de certificación, id=2). Sin él no se puede emitir 34 en producción.
  - [ ] Tras cargar el CAF: emitir una 34 real de prueba y anularla (igual que el flujo 33/61).
- Prueba: factura 34 aceptada (EPR), `MntExe>0`, sin IVA ni `MntNeto`.

### A2. Boletas electrónicas (39 afecta / 41 exenta) — *proceso aparte*
> Las boletas van por host `pangal.sii.cl` (prod) / `rahue.sii.cl` (cert), envelope `EnvioBOLETA`,
> y requieren **RCOF/RVD diario**. Certificación es un set aparte del de facturas.
- Archivos: `Services/DataMapper.php`, `Services/DteXmlBuilder.php`, `Services/SiiBolTransportService.php`
  (existe pero no se usa en el flujo), `Services/RvdBuilder.php` (genérico, revisar), controlador POS.
- Pasos:
  - [x] **Receptor genérico**: `PosVentaController` no exige RUT para 39/41 (acepta `66666666-6`).
  - [x] **Boleta exenta (41)**: el `carritoDte` la fuerza a exento puro (sin IVA), igual que la 34.
  - [x] **Montos correctos desde POS (bug crítico)**: el motor trata el monto afecto de boleta como
        BRUTO (back-out del IVA), no como neto. El `carritoDte` ahora pasa el monto según el tipo
        (factura→neto, boleta→bruto, exentos→bruto sin IVA). Validado end-to-end los 4 tipos.
  - [x] **RVD diario (ConsumoFolios)**: `RvdBuilder` + comando `dte:enviar-rvd` ya existían; agregado
        scheduler opcional (`DTE_RVD_AUTO`, hora/empresa por env). Dry-run validado.
  - [x] Endpoint de auditoría etiquetado `DTEUpload-BOLETA` para envíos de boleta.
  - [x] **Envío por API REST (corregido)**: en producción las boletas van por REST
        (`SiiBolTransportService`, pangal cert / rahue prod), NO por DTEUpload (da HTTP 404 en
        pangal). El envío individual del job (`enviarAlSii`) ahora ramifica a REST; flag
        `DTE_BOLETAS_POR_DTEUPLOAD=true` solo para el SET de certificación.
        **Probado: boleta folio 30006 aceptada (EPR) en producción.**
  - [ ] **PENDIENTE (acción usuario):** cargar **CAF 39 (y 41 si se usa) de producción** en el SII.
  - [ ] Tras el CAF: emitir boleta 39 y 41 reales desde POS y verificar montos/aceptación.
  - [ ] RVD diario real: requiere cron del scheduler en Linux (**D1**) y boletas del día; al probarlo,
        verificar que el `RutEmisor` del ConsumoFolios use el RUT con guión (78073481-7).
  - [ ] Certificación: las **39 ya están certificadas**; verificar set de **41** si se requiere.
- Prueba: boleta 39 y 41 aceptadas; RVD del día enviado y aceptado.

### A3. Guía de despacho (52) — *faltan transporte y destinatario*
- Archivos: `DTO/DteEmissionRequest.php` (`datosGuia`), `Services/DataMapper.php` (hoy solo
  `IndTraslado`/`TipoDespacho`), controlador + UI POS.
- Pasos:
  - [x] Bloque **Transporte** mapeado en `DataMapper::buildTransporte` (Patente, RUTTrans, Chofer
        [RUTChofer/NombreChofer], DirDest, CmnaDest, CiudadDest) en orden XSD, entre Receptor y
        Totales. Datos: POS → `PosVentaController` (`datos_guia`) → `PosVentaToDteMapper` →
        `DteEmissionRequest.datosGuia` → `DataMapper`. XML validado.
  - [x] UI POS: al elegir Guía de Despacho aparece el formulario (tipo traslado/despacho, patente,
        transportista, chofer, destino). El select limita `IndTraslado` a valores válidos.
  - [ ] **PENDIENTE (acción usuario):** cargar **CAF 52 de producción** (la 52 ya está certificada).
  - [ ] Tras el CAF: emitir una guía real con transporte y verificar aceptación.
  - [ ] **Anulación de guía**: se anula con NC (61) `CodRef=1` referenciando la 52 (mismo botón
        "Anular / Nota de Crédito" del POS). El código ya lo soporta (no es boleta → receptor real,
        espeja montos, check de doble anulación); **probar con CAF 52 + CAF 61 con folios**. Aplica si
        la guía aún NO fue facturada.
  - [ ] (Opcional) Facturar desde guía (referencia) y libro de guías por `IndTraslado`.
- Prueba: guía 52 con transporte aceptada.

---

## BLOQUE B — Impresiones (representación impresa)

### B1. Formato hoja (actual) — SKU + métodos de pago
- Archivos: `resources/views/pdf/dte/preview.php` (tabla items, col "Código" hardcodeada `ITM{n}`),
  `Services/Printing/PrintedDteDataBuilder.php`, `Services/Printing/ReferenceDocumentService.php`,
  `Services/FacturacionElectronica/Services/DteXmlBuilder.php` (para timbrar el SKU en el XML).
- Pasos:
  - [x] **SKU en la columna "Código"**: el SKU viaja en el XML como `CdgItem` (TpoCodigo=INT1 /
        VlrCodigo), timbrado. Cadena: carrito → `PosVentaToDteMapper` → `itemPosToErp` → `DataMapper`
        → `DteXmlBuilder` (nodo antes de `NmbItem`, orden XSD) → `ReferenceDocumentService` (lee
        `CdgItem/VlrCodigo`) → `preview.php` (col "Código", con nº de línea de respaldo). Validado.
  - [x] **Métodos de pago**: `PrintedDteDataBuilder::extraerPagos()` toma los pagos de la venta
        (PosCajaMovimiento, soporta pago dividido; cae al método principal) y `preview.php` los
        muestra en un bloque "Forma de pago" bajo los totales de la factura. Render validado.
  - [ ] Pendiente menor: el SKU real solo aparece en DTE **nuevos** con artículo de catálogo (los ya
        emitidos no tienen CdgItem). La forma de pago está en la copia tributaria de **factura**;
        evaluar agregarla a la cedible/boleta-en-hoja si se requiere.
- Prueba: emitir una factura con un artículo de catálogo (no línea libre) y ver el PDF (SKU + pago).

### B2. Formato papel continuo (térmico 80mm) — factura y boleta
- Archivos nuevos: template térmico (ej. `resources/views/pdf/dte/thermal.php`),
  `Services/Printing/PrintedDtePdfService.php` (soporte de tamaño 80mm × alto variable),
  parámetro de formato en el endpoint `GET /dte/{id}/pdf?formato=termico|hoja`.
- Pasos:
  - [x] Template `thermal.php` 80mm × auto (compacto, timbre PDF417, SKU, forma de pago, totales).
  - [x] `PrintedDteHtmlRenderer` y `PrintedDtePdfService` aceptan `formato` ('hoja'|'termico');
        Browsershot con `preferCssPageSize()` (respeta `@page 80mm auto`) / TCPDF 80mm.
  - [x] Endpoint `GET /dte/{id}/pdf?formato=termico` (genera on-demand, sin compartir caché de hoja).
  - [x] Botón "Ticket térmico" en Registros (frontend). Probado: PDF térmico válido para factura 33.
  - [ ] Pendiente: validar visualmente el ticket (anchos, timbre escaneable) y ajustar CSS si hace falta;
        opcionalmente integrarlo a la impresión directa del POS (no solo "Ver muestra").
- Prueba: ticket térmico 80mm de boleta y factura legible con timbre escaneable.

---

## BLOQUE C — Almacenamiento en S3 (por cliente)  ⬅️ PENDIENTE (no empezado)
> Guardar **XML firmado** + **muestra impresa (PDF)** de cada DTE en S3, separado por cliente.
> Como cada cliente tendrá su propio servidor/BD (ver Bloque D), el bucket/prefijo se define en el
> `.env` de ese servidor.
> **Buena noticia:** la app ya escribe XML y PDF en un disco Laravel vía `DteStorageService` (ruta
> `dte/{par_empresa_id}/{tipo}/{folio}.{xml,pdf}`) y `DTE_STORAGE_DISK` ya elige el disco. En teoría
> basta con **apuntar ese disco a S3**; el código de guardado no debería cambiar.
- Archivos: `config/filesystems.php` (disco `s3` **ya definido**, sin credenciales),
  `config/dte.php` (`DTE_STORAGE_DISK`, hoy `local`),
  `Services/FacturacionElectronica/Services/DteStorageService.php` (genera la ruta y guarda),
  `DteXmlController::pdf` / `DteEmissionService::generarPdfMuestra` (leen/guardan el PDF).
- Pasos:
  - [ ] En `.env`: credenciales AWS y disco. Laravel 9 + Flysystem 3 ya traen S3 (sin paquete extra):
        ```
        DTE_STORAGE_DISK=s3
        AWS_ACCESS_KEY_ID=...
        AWS_SECRET_ACCESS_KEY=...
        AWS_DEFAULT_REGION=us-east-1
        AWS_BUCKET=...
        # AWS_ENDPOINT=...   # solo si es S3-compatible (MinIO, etc.)
        ```
        Luego `php artisan config:clear`.
  - [ ] Verificar que `DteStorageService` escribe XML y PDF al disco S3 sin cambios de ruta (emitir
        un DTE y revisar el bucket). Idealmente con **fallback a `local`** si S3 falla (no perder el DTE).
  - [ ] Estructura por cliente: como cada cliente = su `.env`/bucket, la ruta `dte/{empresa}/...` ya
        separa; opcionalmente prefijar por RUT: `{rut_empresa}/dte/{tipo}/{folio}.{xml,pdf}`.
  - [ ] Revisar el endpoint `dte/{id}/pdf`: hoy lee `pdf_path` del disco configurado (compatible con S3).
  - [ ] (Opcional) Migrar el XML que hoy vive en BD (`xml_firmado` longblob) a S3 y dejar la BD como índice.
- Prueba: emitir un DTE con `DTE_STORAGE_DISK=s3` y verlo en el bucket (XML + PDF) bajo el prefijo del cliente.

---

## BLOQUE D — Infraestructura de producción (Linux + multi-tenant)

### D1. Worker de la cola `dte` como servicio en Linux  ⬅️ PENDIENTE (no empezado)
> Hoy corre como proceso suelto en Windows (se cae al cerrar sesión). En Linux: servicio gestionado.
> Sin esto, los DTE quedan emitidos localmente pero **no se envían al SII** (el job `EnviarDteAlSiiJob`
> no se procesa). También de aquí depende el **RVD diario** de boletas (necesita `schedule:run` por cron).
- Pasos:
  - [ ] **Servicio systemd** para el worker, p.ej. `/etc/systemd/system/dte-worker.service`:
        ```ini
        [Unit]
        Description=DTE queue worker
        After=network.target mysql.service

        [Service]
        User=www-data
        WorkingDirectory=/var/www/api-a-conta-fact
        ExecStart=/usr/bin/php artisan queue:work database --queue=dte --tries=3 --timeout=120 --sleep=3
        Restart=always
        RestartSec=5

        [Install]
        WantedBy=multi-user.target
        ```
        Luego: `systemctl enable --now dte-worker` (o usar **Supervisor** si se prefiere).
  - [ ] **Cron del scheduler** (habilita el RVD diario de A2 y reintentos): en el crontab del usuario,
        `* * * * * cd /var/www/api-a-conta-fact && php artisan schedule:run >> /dev/null 2>&1`.
        Activar en `.env`: `DTE_RVD_AUTO=true` (RVD diario) y/o `DTE_REINTENTO_AUTO=true`.
  - [ ] `php artisan queue:restart` en cada deploy (para que el worker tome el código nuevo).
  - [ ] Monitoreo: alertar si crece `failed_jobs`; revisar `dte_envios`/`dte_logs`.
- Prueba: matar el proceso del worker y verificar que systemd lo reinicia; encolar un DTE y ver que
  se envía solo; con el cron activo, ver que el RVD del día se envía a la hora configurada.

### D2. Modelo "cada cliente = su propio servidor y BD individual"
> **Decisión de arquitectura mayor.** Hoy es monolítico (1 BD, `par_empresa_id`). Hay dos caminos:
> (a) **un servidor + BD por cliente** (deploy independiente por cliente; lo que pide el usuario), o
> (b) tenancy dinámica (1 código, N BD conmutadas por dominio/endpoint).
- Pasos (camino a — deploy por cliente):
  - [ ] Definir plantilla de deploy por cliente (código + `.env` propio: BD, S3, CAFs, certificado,
        resolución SII, worker).
  - [ ] Proceso de provisión: crear BD, correr migraciones/seeders base, cargar certificado y CAFs.
  - [ ] Aislar S3 por cliente (bucket o prefijo) y el worker por servidor (D1).
  - [ ] Revisar `UserSyncService`/`PRIMARY_AUTH_API_URL` si se mantiene login central.
- Prueba: levantar un cliente nuevo de cero siguiendo el runbook y emitir un DTE.

---

## Orden sugerido (dependencias)
1. **FASE 0** (cerrar MVP) → 2. **A1** (exenta, casi gratis) → 3. **B1** (SKU + pagos, alto valor visible)
→ 4. **A2** (boletas, core retail) → 5. **B2** (térmico, va de la mano con boletas)
→ 6. **C** (S3) → 7. **A3** (guía) → 8. **D1** (worker Linux) → 9. **D2** (multi-tenant).

> Nota: D1 (worker Linux) se puede adelantar apenas haya servidor Linux; no bloquea a los demás.
