# Proceso de Generación y Envío de PDF de Multiorden

> Documento de referencia técnica para retomar el trabajo sobre el flujo de
> generación del PDF de una multiorden y su envío por correo al cliente.
> Última actualización de este análisis: 2026-09-19 (incluye los fixes de
> firma/orden de ítems, carpeta `temporal_image/` y reintento de ATW — ver
> §7).

## 1. Propósito del proceso

Cuando se crea una **multiorden** (recibo de reencauche/servicio) en el
sistema ATW, este proceso genera el **comprobante en PDF** con el detalle de
la multiorden (ítems, firma del cliente, soportes fotográficos y
condiciones) y lo envía por **correo electrónico** al cliente como archivo
adjunto.

El flujo se apoya en dos endpoints propios más un servicio externo de
renderizado HTML → PDF:

1. **`generatePDF.php`** — orquestador. Recibe `id` de la multiorden y
   `email` del destinatario, dispara la conversión a PDF, arma el cuerpo del
   correo y lo envía.
2. **`multiorderPDF.php`** — worker de renderizado. Genera el **HTML** del
   comprobante para una multiorden específica (imágenes, tabla de ítems,
   firmas, condiciones). No lo consume el usuario final, sino el servicio de
   conversión.
3. **Gotenberg** (`gotenberg.api.automundial.app`) — servicio externo que
   renderiza la URL de `multiorderPDF.php` en un navegador headless
   (Chromium) y devuelve el PDF resultante.

## 2. Archivos involucrados

| Archivo | Rol |
|---|---|
| `generatePDF.php` | Orquestador del endpoint. Valida parámetros, obtiene el consecutivo, pide el PDF a Gotenberg y envía el correo. |
| `multiorderPDF.php` | Genera el HTML del comprobante de la multiorden (detalle, imágenes de soporte, firma, condiciones). Es consumido por Gotenberg vía GET. |
| `lib/funcionesGenerales/envioCorreo.php` | Envío de correo vía SMTP Office365 (PHPMailer). `generatePDF.php` usa `enviarCorreoOffice365()`. |
| `error_http.php` | Helpers `render()` / `render_error()` y catálogo de códigos de error HTTP. |
| `back2.test.atwplus.com` (ATW API) | Fuente de datos de la multiorden: consecutivo, ítems, cliente, firmas, archivos base64 de soporte. |
| `gotenberg.api.automundial.app` | Servicio externo de conversión HTML → PDF (Chromium headless vía Gotenberg). |

Directorios de trabajo (relativos a `app/`):

- `log/` — copia de depuración del último PDF generado (`log/testPDF.pdf`) y adjuntos temporales (`log/multiorden_<id>_<uniqid>.pdf`).
- `temporal_image/` — imágenes de soporte y firma decodificadas desde base64, usadas solo mientras se renderiza el HTML.

## 3. Flujo end-to-end

```
Cliente/Sistema → POST generatePDF.php  { id, email }
   │
   ├─ 1. Valida id y email (formato de correo)
   │
   ├─ 2. getMultiorderConsecutive($id)
   │        └─ GET back2.test.atwplus.com/v1/multiorders/{id}/detail
   │             → extrae output[0].consecutive
   │
   ├─ 3. POST gotenberg.api.automundial.app/forms/chromium/convert/url
   │        { url: "https://.../app/multiorderPDF.php?id={id}" }
   │        │
   │        └─ Gotenberg hace GET a multiorderPDF.php?id={id}
   │             ├─ GET back2.test.atwplus.com/v1/multiorders/{id}/detail (de nuevo)
   │             ├─ decodifica base64 de firma y de soportes → temporal_image/*
   │             └─ echo del HTML del comprobante (detalle, soportes, firma, condiciones)
   │        │
   │        └─ Gotenberg responde con el binario del PDF renderizado
   │
   ├─ 4. Valida que la respuesta empiece con "%PDF"
   │        └─ guarda copia en log/testPDF.pdf (se sobreescribe siempre)
   │        └─ guarda adjunto temporal en log/multiorden_{id}_{uniqid}.pdf
   │
   ├─ 5. Arma asunto y cuerpo HTML del correo (getMultiordenEmailBody)
   │
   ├─ 6. EnvioCorreo::enviarCorreoOffice365(mensaje, asunto, 'html',
   │        attachmentPath, '.pdf', email, 'Automundial S.A.', '', '')
   │        └─ PHPMailer vía smtp.office365.com:587 (STARTTLS)
   │        └─ adjunta attachmentPath+'.pdf' y lo elimina tras el envío
   │
   └─ 7. Si el correo se envió → render('Ok', pdf_base64)
        Si no → borra el adjunto y responde UNEXPECTED_ERROR
```

**Nota clave:** `back2.test.atwplus.com/v1/multiorders/{id}/detail` se
consulta **dos veces** en una misma ejecución: una desde `generatePDF.php`
(solo para el consecutivo) y otra desde `multiorderPDF.php` (para todo el
detalle que arma el HTML). Es redundante pero no rompe el flujo.

## 4. Detalle por archivo

### 4.1 `generatePDF.php`

- Único método soportado: `POST`. Cualquier otro método no hace nada
  (`default: break;`, sin respuesta explícita).
- Body esperado (JSON):
  ```json
  { "id": "<id de la multiorden>", "email": "cliente@dominio.com" }
  ```
- Validaciones:
  - `id` requerido → si falta: `UNPARAMETERED` (`id`).
  - `email` requerido y validado con `FILTER_VALIDATE_EMAIL` → si falla:
    `UNPARAMETERED` (`email`). Solo soporta **un** destinatario (no hay
    `explode(',')` como en otros procesos del repo).
- `getMultiorderConsecutive($id)`: si el API de ATW no devuelve
  `output[0].consecutive`, corta con `UNEXPECTED_ERROR`.
  - **Reintenta hasta 3 veces** (0.5s de espera entre intentos) ante error de
    cURL o respuesta sin `consecutive`, para tolerar fallos transitorios de
    `back2.test.atwplus.com` (ver §7).
- **`id` es el `multiorder_id` interno, no el `consecutive`/ticket ERP.**
  Enviar el consecutivo (p. ej. `50000006`) en vez del `multiorder_id` real
  (p. ej. `300969`) hace que la API de ATW responda `400` con un error tipo
  `Cannot read properties of undefined (reading 'file')`, que luego se
  traduce en `UNEXPECTED_ERROR` aquí o en "Formato Base64 inválido" en
  `multiorderPDF.php` (ver §7).
- Conversión a PDF: delega en Gotenberg, pasándole la URL pública de
  `multiorderPDF.php?id=`. **Importante:** esa URL debe ser accesible
  públicamente por Gotenberg (por eso apunta a un dominio `https://`, no a
  `localhost`).
- Verifica el PDF con `strpos($response, '%PDF') !== 0` (firma del formato
  PDF) en lugar de validar el `Content-Type` de la respuesta.
- Guarda dos copias del PDF:
  - `log/testPDF.pdf`: copia fija de depuración, se sobreescribe en cada
    ejecución (no sirve como histórico).
  - `log/multiorden_{id}_{uniqid()}.pdf`: adjunto real del correo, con
    nombre único para no chocar entre ejecuciones concurrentes.
- Envía el correo con `enviarCorreoOffice365(...)` (ver §4.3) y valida el
  éxito buscando el texto `'has been sent'` en la salida capturada con
  `ob_start()/ob_get_clean()` — **no** hay un valor de retorno estructurado,
  el "contrato" de éxito es un string fijo impreso por la función.
- Si el envío falla, borra el adjunto manualmente (`@unlink`) — necesario
  porque `enviarCorreoOffice365` solo borra el archivo cuando el envío es
  exitoso (ver §4.3).
- Respuesta exitosa: `render('Ok', $pdf_base64)` — devuelve el PDF completo
  codificado en base64 en el JSON de salida, además de haberlo enviado por
  correo.
- `error_reporting(0)` está activo: cualquier warning/notice de PHP se
  silencia y no aparece en la respuesta ni en logs de errores estándar.

### 4.2 `multiorderPDF.php`

- Recibe `id` por **GET** (`$_GET['id']`, sin validación de presencia).
- Al inicio, crea la carpeta `temporal_image/` si no existe
  (`is_dir()` + `mkdir()`). Es una carpeta de runtime, listada en
  `.gitignore`, que no viaja con el repo ni se recrea sola en un despliegue
  nuevo (ver §7).
- Consulta el detalle completo de la multiorden en ATW (mismo endpoint que
  `getMultiorderConsecutive`, pero usando todo el payload).
- **La API no garantiza el orden de los ítems** en `output[]` (se ha visto
  devolver, p. ej., los ítems 2, 1, 3 en ese orden). El código los ordena
  con `usort()` por `multiorder_items_number` antes de usarlos, tanto para
  la tabla "DETALLE" como para elegir el ítem de la firma (ver §7).
- La firma del cliente viaja en el campo `file->base64` de **uno solo** de
  los ítems del arreglo (los demás repiten los mismos metadatos de
  `file` — `file_name`, `geolocation`, `backoffice`, etc. — pero sin la
  clave `base64`, probablemente para no duplicar el blob). El código recorre
  los ítems ya ordenados y usa el primero cuyo `file->base64` no esté vacío;
  antes asumía que siempre era `$multiorder[0]` (ver §7).
- Por cada ítem de la multiorden, si tiene
  `multiorder_items_file_description->files`, decodifica cada archivo base64
  (fotos de soporte) a `temporal_image/` vía `getImage()` y guarda la ruta
  en `->location` para poder referenciarla como `<img src="...">` en el
  HTML.
- Arma un documento HTML completo (con estilos inline) que incluye:
  - Encabezado con logo.
  - Tarjeta resumen (tipo, NIT, dirección, celular, fecha, cliente,
    teléfono, asesor/inspector).
  - Tabla "DETALLE" con un renglón por ítem (diseño, tipo, orig/reen, serie,
    diseño solicitado, tiquete quemado, fecha de entrega, observaciones).
  - Bloque de firma del cliente (nombre y documento) repetido después del
    detalle y, si existen soportes, otra vez después de estos.
  - Sección "Soportes" (solo si algún ítem tiene archivos adjuntos), con
    imágenes en grilla.
  - Sección "CONDICIONES" con las cláusulas legales del servicio de
    reencauche.
  - `<div class="page-break">` para forzar salto de página al convertir a
    PDF.
- Termina con `echo $response;` — es este HTML crudo el que Gotenberg
  captura y convierte a PDF.
- **No limpia** los archivos generados en `temporal_image/`: el bloque que
  hacía `unlink` de esa carpeta al inicio está comentado (líneas 25-29). Con
  el tiempo la carpeta acumula un archivo por cada imagen/firma procesada en
  cada ejecución — candidato a tarea de limpieza (cron o borrado post-uso).
- Si el `id` no existe o el API de ATW no devuelve datos, `json_decode(...)->output`
  puede fallar y no hay manejo de error explícito (no hay `render_error`
  aquí, es un script de salida HTML plana pensado para ser consumido por
  Gotenberg).

### 4.3 Envío de correo — `EnvioCorreo::enviarCorreoOffice365()`

Ubicado en `lib/funcionesGenerales/envioCorreo.php:118`. `generatePDF.php`
lo invoca así:

```php
$objEnvioCorreo->enviarCorreoOffice365(
    $mensaje,          // HTML del cuerpo del correo
    $asunto,           // "Multiorden No. {consecutivo} creada - Automundial S.A."
    'html',            // modo de cuerpo: activa isHTML(true) + UTF-8
    $attachmentPath,   // ruta del adjunto SIN extensión
    '.pdf',            // extensión del adjunto
    trim($email),      // destinatario(s), separados por coma
    'Automundial S.A.',// FromName visible en el correo
    '',                // correos en copia oculta (BCC), vacío aquí
    ''                 // "opcionCorreo": credenciales SMTP a usar (default = atwplusPBI@automundial.com.co)
);
```

Puntos a tener en cuenta:

- El parámetro `$archivo` **no es el contenido del archivo**: es un flag de
  modo (`'html'` vs. cualquier otro valor) que decide si el cuerpo del
  correo se manda como HTML o texto plano.
- El adjunto real se arma como `$nombreArchivo . $extensionArchivo`
  (`addAttachment`).
- **Efecto secundario importante:** si el envío es exitoso, la función hace
  `unlink($nombreArchivo.$extensionArchivo)` — borra el PDF adjunto del
  disco automáticamente. Por eso `generatePDF.php` solo necesita borrar el
  archivo manualmente en el camino de error.
- Las credenciales SMTP (usuario/clave de `atwplusPBI@automundial.com.co` u
  otra cuenta según `opcionCorreo`) están **hardcodeadas en el archivo**, no
  en variables de entorno ni en `lib/param.inc.php`.
- No lanza excepción hacia arriba: los errores de PHPMailer se capturan y
  solo se imprimen (`echo "Message could not be sent..."`), de ahí que
  `generatePDF.php` tenga que inspeccionar el texto de salida capturado con
  `ob_start()` para saber si el envío fue exitoso.

### 4.4 Manejo de errores — `error_http.php`

- `render($status, $content)`: responde `{ "output": content }` con el
  código HTTP correspondiente a `$status` (p. ej. `'Ok'` → 200) y termina la
  ejecución.
- `render_error($code, $msn)`: responde `{ "error": mensaje }` buscando
  `$code` en el catálogo de `filterError()`. Los códigos usados en este
  flujo son `UNPARAMETERED` (400) y `UNEXPECTED_ERROR` (500).

## 5. Entornos y URLs

Este proceso mezcla llamadas a **dos** dominios cuyo valor cambia según el
entorno; al fusionar `test` con `master` estas URLs suelen entrar en
conflicto porque cada rama apunta a un ambiente distinto:

| Uso | Test (rama `test`) | Producción (rama `master`) |
|---|---|---|
| API de multiordenes (ATW) | `https://back2.test.atwplus.com` | `https://back.atwplus.com` |
| URL pública que Gotenberg debe convertir (`multiorderPDF.php`) | `https://test.api.automundial.app` | `https://api.automundial.app` |

Ambas URLs deben ser consistentes entre sí (no mezclar API de test con
dominio público de producción o viceversa), porque `multiorderPDF.php`
vuelve a llamar a la misma API de ATW para armar el detalle.

## 6. Puntos de atención / posible trabajo pendiente

- **Duplicación de la consulta a ATW**: se pide el detalle completo de la
  multiorden dos veces (una para el consecutivo, otra para el HTML). Podría
  optimizarse pasando el detalle ya obtenido a `multiorderPDF.php` en vez de
  volver a consultarlo.
- **`temporal_image/` sin limpieza automática**: crece indefinidamente: cada
  ejecución agrega una imagen por soporte más la firma. El código de
  limpieza existe pero está comentado. (La carpeta ya no falta —ver §7—,
  pero sigue sin vaciarse.)
- **`error_reporting(0)`** oculta cualquier error/warning de PHP en
  `generatePDF.php`, lo que puede dificultar el diagnóstico de fallas
  silenciosas (p. ej. propiedades inexistentes en la respuesta de ATW).
- **Un solo destinatario**: a diferencia de otros procesos de envío del
  repo, `generatePDF.php` no soporta múltiples correos separados por coma
  para el campo `email` (sí lo soporta `enviarCorreoOffice365` a nivel de
  función, pero no se aprovecha aquí).
- **Credenciales hardcodeadas**: tanto el header `Authorization: Basic ...`
  hacia la API de ATW como el usuario/clave SMTP en `envioCorreo.php` están
  escritos directamente en el código fuente.
- **`log/testPDF.pdf`** es una copia de depuración de un solo archivo que se
  sobreescribe en cada llamada; no sirve como histórico ni como auditoría.

## 7. Historial de incidentes y fixes (2026-09-19)

Durante la validación de la multiorden **300969** (consecutivo `50000006`)
se encontraron y corrigieron 4 problemas encadenados en `multiorderPDF.php`
y `generatePDF.php`:

| # | Síntoma | Causa raíz | Fix |
|---|---|---|---|
| 1 | `generatePDF.php` respondía `UNEXPECTED_ERROR` / `multiorderPDF.php` moría con "Formato Base64 inválido" al usar `id=50000006` | Se estaba pasando el **consecutivo** (ticket ERP) como `id`, pero el endpoint de ATW espera el **`multiorder_id`** interno (`300969`). Con un id inexistente, la API de ATW responde `400` (`Cannot read properties of undefined (reading 'file')`), y el código no valida ese error antes de intentar decodificar base64. | No es un bug de código: es un recordatorio operativo — usar siempre `multiorder_id`, no `consecutive`, al llamar estos endpoints (documentado en §4.1). |
| 2 | El PDF salía sin la firma / "Formato Base64 inválido" incluso con el `id` correcto, en multiordenes donde el ítem con la firma no era el primero del arreglo | `multiorderPDF.php` asumía `$multiorder[0]->file->base64` a ciegas; la API solo incluye `base64` en uno de los ítems, no necesariamente en el índice `0` | Se reemplazó por un `foreach` que busca el primer ítem con `file->base64` no vacío |
| 3 | Los ítems de la tabla "DETALLE" aparecían en un orden distinto al esperado (p. ej. 2, 1, 3) | La API de ATW no garantiza el orden de `output[]` | Se agregó `usort()` por `multiorder_items_number` antes de renderizar |
| 4 | El correo llegaba con el PDF, pero **sin ninguna imagen** (ni firma ni soportes), sin ningún error visible | La carpeta `temporal_image/` (runtime, en `.gitignore`) no existía en el servidor; `file_put_contents()` fallaba en silencio y el HTML quedaba con `<img src="temporal_image/...">` apuntando a archivos inexistentes (404 para Gotenberg) | Se agregó `if (!is_dir('temporal_image')) { mkdir('temporal_image', 0777, true); }` al inicio de `multiorderPDF.php`, y se creó la carpeta manualmente en el servidor de test |
| 5 | Fallo transitorio ocasional al pedir el consecutivo (`UNEXPECTED_ERROR`) que desaparecía al reintentar manualmente | Llamada a ATW sin reintento ante errores intermitentes de red/timeout | `getMultiorderConsecutive()` ahora reintenta hasta 3 veces con 0.5s de espera |

Commit: `79f7de8` — *"Fix generacion de PDF y correo de multiorden"* (rama
`test`).
