# Proceso de Extractos de Clientes (Statements)

> Documento de referencia técnica para retomar rápidamente el trabajo sobre el
> flujo de generación y envío de extractos mensuales a clientes.
> Última actualización de este análisis: 2026-08-25.

## 1. Propósito del proceso

Generar mensualmente el "Extracto de Cuenta" (estado de cartera) de cada
cliente activo, guardarlo como registro de control en base de datos y
enviarlo por correo electrónico en formato PDF adjunto.

El flujo se divide en **dos fases desacopladas**, ejecutadas como eventos
distintos del mismo endpoint:

1. **`save`** — identifica los clientes con extracto para el período y crea
   un "documento" de control en estado `pending`.
2. **`send`** — recorre los documentos `pending`, genera el PDF de cada uno
   y lo envía por correo; actualiza el estado a `closed` o `error`.

## 2. Archivos involucrados

| Archivo | Rol |
|---|---|
| `invoices/statement_process.php` | Orquestador. Expone `?event=save` y `?event=send`. Cron/disparador externo. |
| `generatePDFv2.php` | Worker por cliente: valida existencia, pide el HTML a `statement_customers.php`, lo convierte a PDF vía Gotenberg y envía el correo. |
| `invoices/statement_customers.php` | Genera el HTML del extracto (plantilla + datos) para un cliente/período específico. Es consumido por Gotenberg, no por el usuario final. |
| `lib/dao/crcextDAO.php` | Acceso a datos de cartera (`crcext`, `facli`, `faven`, `fasem`, `faclides`). |
| `lib/dao/crcpasDAO.php` | Acceso a la tabla de control de envíos `crcpas`. |
| `lib/funcionesGenerales/envioCorreo.php` | Envío de correo vía SMTP Office365 (PHPMailer) y variantes legacy (AWS SES, Power Automate, SMTP interno). |
| `error_http.php` | Helpers `render()` / `render_error()` y catálogo de códigos de error HTTP. |
| `lib/param.inc.php` | Constantes de conexión a BD (Informix) y credenciales varias. |

Base de datos: **Informix** (se evidencia por la sintaxis `SELECT FIRST n`),
accedida vía ADODB/PDO (`lib/conexionDB.php`).

## 3. Flujo end-to-end

```
Cron/Disparador externo
   │
   ├─▶ statement_process.php?event=save     (día 5–8 del mes)
   │        └─ CrcextDAO::getCustomersByYearMonth(mes, año)
   │        └─ por cada cliente → CrcpasDAO::addDocument(status='pending')
   │
   └─▶ statement_process.php?event=send     (día 5–8 del mes)
            └─ CrcpasDAO::getDocument(status='pending')  [máx. 100 filas]
            └─ por lotes de 20, 1 por minuto:
                  curl → generatePDFv2.php?month&year&customer&email&link=statement
                            ├─ curl → statement_customers.php  (valida <title> no vacío)
                            ├─ curl POST → Gotenberg (HTML → PDF)
                            ├─ guarda copia en log/testPDF.pdf (se sobreescribe)
                            ├─ valida dominio de los correos (MX/A)
                            └─ EnvioCorreo::enviarCorreoOffice365_3() → SMTP Office365
                  └─ según httpCode devuelto: CrcpasDAO::updateDocumentStatus('closed'|'error')
```

### 3.1 `statement_process.php`

- Requiere `?event=save|send`. Cualquier otro valor → `PARAMETER_DOES_NOT_EXIST`.
- **Ventana de ejecución forzada**: solo corre si el día del mes está entre
  **5 y 8** (`$currentDay`, líneas 34-39). Fuera de ese rango responde
  `PROCESS_OUT_OF_SCHEDULE` (403) y registra el intento en el log.
- Período objetivo: por defecto es el **mes calendario anterior** al actual
  (`last day of last month`); puede forzarse con `?month=MM&year=YYYY`
  (útil para reprocesos/pruebas).
- `save`: inserta un registro en `crcpas` por cada cliente devuelto por
  `getCustomersByYearMonth` (filtra `semcod IN ('PAR')` y `extfue = 55`,
  es decir solo facturas de clientes con forma de pago "PAR").
  - **Exclusión por `crcclires`** (2026-08-25): `getCustomersByYearMonth`
    ahora agrega `AND NOT EXISTS (SELECT 1 FROM crcclires WHERE rescli =
    extcli)`, de modo que un cliente presente en `crcclires` **nunca**
    llega a crear un documento `pending` en `crcpas` — queda fuera desde
    el origen de la información, antes de que exista cualquier intención
    de envío.
  - Para trazabilidad, se agregó `CrcextDAO::getRestrictedCustomersByYearMonth()`,
    que corre la misma consulta pero **invertida** (`INNER JOIN` implícito
    con `crcclires`) para listar qué clientes habrían calificado para el
    período si no estuvieran restringidos. `statement_process.php` recorre
    ese listado y escribe una línea `info` por cliente en
    `log/statementProcessYYYYMM.txt` (incluye quién y cuándo lo restringió,
    tomado de `resusu`/`resfec`), y expone el conteo en la respuesta JSON
    del evento `save` como `excluded`.
  - **No verifica duplicados**: ejecutar `save` dos veces para el mismo
    período genera documentos `pending` duplicados → correos duplicados.
- `send`: procesa en lotes de 20 documentos, con una pausa hasta completar
  60s entre lotes (rate-limit ~20 correos/min hacia `generatePDFv2.php`).
  - Solo trae documentos con `pasest = 'pending'` (máx. 100 por la cláusula
    `FIRST 100` de Informix). Si hay más de 100 pendientes, requiere
    **múltiples ejecuciones manuales** de `send` (dentro de la ventana 5-8).
  - Los documentos que quedan en `error` **no se reintentan
    automáticamente**: `getDocument()` solo filtra `pending`. Se requiere
    intervención manual (o un evento nuevo) para reintentarlos.
- Variable `$address` (línea 13-14): apunta al ambiente de **desarrollo**
  (`ejimenez.automundial.app`) y tiene comentada la URL de **producción**
  (`api.automundial.app`). **Recordar cambiarla antes de pasar a
  producción** — ver sección 6.
- Logging propio a archivo plano: `log/statementProcessYYYYMM.txt`.
- **Sin autenticación**: el endpoint no valida ningún token/cabecera; la
  única protección es la ventana de fecha. Cualquiera que conozca la URL
  puede disparar `save` o `send` durante esos 4 días.

### 3.2 `generatePDFv2.php`

- Parámetros GET: `month`, `year`, `customer`, `link` (validados como
  requeridos) y `email` (usado pero **no incluido** en `$requiredParams` —
  inconsistencia menor, ver sección 5).
- `link` solo soporta el valor `statement`; el target está **hardcodeado**
  a `https://api.automundial.app/app/invoices/statement_customers.php`
  (producción), **independiente** del valor de `$address` en
  `statement_process.php`. Si `statement_process.php` apunta a desarrollo,
  igual llamará a `statement_customers.php` de **producción** con los
  parámetros de la prueba — riesgo de mezclar datos/ambientes.
- Pre-valida la existencia del cliente pidiendo el HTML y buscando el
  patrón `<title>Extracto AutoMundial - </title>` (título vacío = cliente
  sin datos). Es un mecanismo frágil (acoplado al HTML) pero funcional.
- Genera el PDF llamando a Gotenberg (`/forms/chromium/convert/url`) y lo
  guarda en `log/testPDF.pdf` (nombre fijo, se sobreescribe en cada
  ejecución; no se usa después de guardarlo — código muerto/IO
  innecesaria, y potencial exposición si `log/` es accesible por web).
- Valida dominios de correo (`checkdnsrr` MX/A) antes de enviar.
- Envía el correo con `EnvioCorreo::enviarCorreoOffice365_3()`, adjuntando
  el PDF en base64 (sin tocar disco para el adjunto real).
- `$email_copy` está **hardcodeado** al correo personal
  `ejimenez@automundial.com.co` — todo extracto enviado va en copia oculta
  (BCC) a esta cuenta. Si la persona cambia de correo/rol, hay que
  recordar actualizar este valor aquí y en `$email_tec` (línea 16-17,
  luego sobreescrito en línea 32 por el GET `email`).
- Contiene funciones **muertas/no usadas**: `sendEmailAWS()` (referencia a
  `$SesClient` que nunca se instancia — fallaría si se llamara) y
  `sendEmailPA()` (webhook de Power Automate con **firma/secreto
  expuesto** en el código, aunque no se invoca actualmente).

### 3.3 `statement_customers.php`

- Recibe `month`, `year`, `customer` por `$_REQUEST` (no `$_GET`) **sin
  ninguna validación ni sanitización** y los concatena directamente en SQL
  vía `CrcextDAO::getStatement()`.
- Construye el HTML completo del extracto (CSS inline embebido) con:
  - Datos del cliente y asesor comercial.
  - Resumen: saldo vencido, valor a vencerse este mes, total a pagar.
  - Detalle por cuota (factura, cuota, fechas, valores, días de mora).
- **Filtra únicamente `extfue == '55'` (facturas)** tanto para los totales
  del resumen como para el detalle. El `switch` interno maneja también
  `56` (nota crédito), `57` (nota débito) y `58` (abono sin aplicar), pero
  ese código es **inalcanzable** porque el `if` externo ya descartó todo
  lo que no sea `55`. Si la intención original era netear notas
  crédito/débito y abonos contra la cartera, **hoy no se está haciendo** —
  revisar si es un bug real de negocio (saldos podrían no reflejar
  abonos/notas).
- No usa `htmlspecialchars` al imprimir datos del cliente (nombre,
  vendedor, correo) — bajo riesgo práctico (dato interno), pero es una
  mala práctica si en algún momento estos campos permiten caracteres HTML.
- Usa la etiqueta corta `<?` (no `<?php`) — requiere `short_open_tag` en
  el `php.ini`; inconsistente con el resto del proyecto.
- No incluye `error_http.php` ni valida headers/autenticación — es un
  endpoint público que expone datos financieros completos de un cliente
  con solo conocer `customer/month/year`.

## 4. Modelo de datos relevante

- **`crcext`**: movimientos de cartera (facturas, notas, abonos) por
  cliente/período. Campos usados: `extfue` (tipo de movimiento: 55/56/57/58),
  `extfac`, `extcuo`, `extfca`, `extfve`, `exttot`, `extpag`, `extcli`,
  `extmesh`, `extanoh`.
- **`crcpas`**: tabla de control/cola de envío de extractos. Campos:
  `pasid`, `pasano`, `pasmes`, `pascii` (cliente), `passem`, `pasest`
  (`pending`/`closed`/`error`), `paseml` (correo destino), `paserr`
  (mensaje de error).
- **`facli` / `faven` / `faclides` / `fasem`**: catálogos de clientes,
  vendedores, correos de contacto y formas de pago (usados en joins).
- **`crcclires`** (agregada 2026-08-25): lista de clientes **excluidos** del
  envío de extractos.
  ```sql
  CREATE TABLE crcclires (
    resid  SERIAL NOT NULL,
    rescli CHAR(20),
    resusu CHAR(50),
    resfec DATETIME YEAR TO SECOND,
    PRIMARY KEY (resid)
  );
  ```
  - `rescli`: código del cliente restringido (equivalente a `extcli`).
  - `resusu`: usuario que registró la restricción.
  - `resfec`: fecha/hora en que se agregó la restricción.
  - No tiene fecha de expiración ni período asociado: la exclusión es
    **indefinida** hasta que alguien borre la fila manualmente (no hay
    endpoint/CRUD para administrar esta tabla en este código; se asume
    gestión directa en BD o desde otro módulo).

## 5. Seguridad — hallazgos

Ordenados por severidad aproximada:

1. **Inyección SQL en `statement_customers.php` (crítico)** — `month`,
   `year`, `customer` llegan por `$_REQUEST` sin ningún filtro y se
   concatenan directo en la consulta (`CrcextDAO::getStatement`). Es un
   endpoint alcanzable directamente por HTTP sin autenticación.
2. **Exposición/IDOR de datos financieros (crítico)** — Ni
   `generatePDFv2.php` ni `statement_customers.php` verifican que quien
   hace la petición tenga derecho a ver ese `customer`. Además,
   `generatePDFv2.php` acepta un parámetro `email` libre: cualquiera puede
   pedir `?customer=<id>&email=atacante@dominio.com&...` y recibir por
   correo el extracto de **cualquier** cliente cuyo ID conozca/adivine
   (los IDs parecen ser NITs/cédulas, potencialmente enumerables).
3. **Credenciales y secretos hardcodeados en el código** —
   `lib/funcionesGenerales/envioCorreo.php` (usuarios/contraseñas SMTP de
   Office365, contraseña de correo de facturación Ecuador),
   `lib/param.inc.php` (usuario/clave de BD, API keys de Driv.in y ATW+),
   y el webhook con firma de Power Automate en `sendEmailPA()`. Todo esto
   vive en el repositorio de código fuente.
4. **Sin autenticación en los tres endpoints** — El resto de la API
   parece soportar un esquema de cabeceras `App-Token` / `App-User`
   (existen códigos `INVALID_HEADER_TOKEN` / `INVALID_HEADER_USER` en
   `error_http.php`), pero `statement_process.php`, `generatePDFv2.php` y
   `statement_customers.php` no lo aplican. La única barrera de
   `statement_process.php` es la ventana de fecha (5-8 del mes), que no es
   un control de acceso.
5. **`CURLOPT_SSLVERIFYPEER` desactivado en PHPMailer**
   (`verify_peer' => false` en `enviarCorreoOffice365_3`) — abre la puerta
   a MITM en la conexión SMTP TLS.
6. **CORS abierto** (`Access-Control-Allow-Origin: *`) en
   `statement_process.php` y `generatePDFv2.php` — sin impacto directo por
   sí solo dado que no hay sesión/cookies, pero refuerza la falta de
   control de acceso.
7. **PDF temporal en `log/testPDF.pdf`** — si el directorio `log/` es
   servido por el vhost, cualquiera podría descargar el último extracto
   procesado.

## 6. Rendimiento

- `send` es **secuencial** (un `curl_exec` a la vez dentro del `foreach`),
  con una pausa fija para no superar ~20 correos/min. Para volúmenes
  grandes de clientes esto es lento (ej. 500 documentos ≈ 25 min mínimo)
  pero es intencional (rate-limit hacia el proveedor de correo/PDF).
- `ini_set('memory_limit', '-1')` en `statement_process.php` — quita el
  límite de memoria; razonable si el volumen de clientes es grande, pero
  sin límite puede enmascarar fugas de memoria en ejecuciones largas.
- Límite `FIRST 100` en `getDocument()` obliga a correr `send` varias
  veces si hay más de 100 pendientes — no hay paginación/cursor real, solo
  un tope fijo.
- `checkdnsrr` hace resolución DNS síncrona por cada envío — añade latencia
  de red por documento procesado.
- La generación de HTML en `statement_customers.php` concatena strings en
  un bucle (`$response .=`) — para el volumen esperado (decenas/cientos de
  filas por cliente) no es un problema real de rendimiento.

## 7. Mantenibilidad / buenas y malas prácticas

- Buenas prácticas presentes: logging propio con contexto
  (`writeLog`), separación DAO/orquestador, validación de formato de
  `month`/`year` con regex en el orquestador y en el worker, manejo de
  lotes con rate-limit explícito, uso de `render_error` centralizado.
- SQL armado por concatenación de strings en **todos** los DAOs (`crcext`,
  `crcpas`) en vez de sentencias preparadas — funciona hoy porque los
  valores que llegan a estos DAOs desde `statement_process.php` ya están
  validados por regex, pero es una práctica riesgosa que solo un
  endpoint (`statement_customers.php`) rompe activamente (ver §5.1).
- HTML mezclado con lógica de negocio en `statement_customers.php` (sin
  plantillas/motor de vistas) — dificulta mantenimiento del diseño del
  PDF.
- Código muerto: `sendEmailAWS`, `sendEmailPA` en `envioCorreo.php`;
  variables comentadas de prueba (`$targetYear`, `$targetMonth`,
  `$extmesh`, etc.) dejadas en varios archivos.
- Inconsistencia de estilo: `<?` vs `<?php`, mezcla de `$_GET`/`$_REQUEST`.
- No hay pruebas automatizadas para ninguno de los tres archivos.
- El estado `error` en `crcpas` no tiene un mecanismo de reintento
  documentado/automatizado — depende de proceso manual.

## 8. Recomendaciones de mejora (priorizadas)

1. **Sanitizar/parametrizar `statement_customers.php`**: validar
   `month`/`year`/`customer` igual que en `generatePDFv2.php` (regex +
   `ctype_digit`) antes de tocar la BD, e idealmente migrar los DAOs a
   consultas parametrizadas.
2. **Cerrar el IDOR**: no confiar en un `email` arbitrario por GET;
   resolver el correo destino desde `crcpas.paseml`/BD en
   `generatePDFv2.php`, o exigir un token firmado de un solo uso por
   documento (ej. HMAC de `customer+month+year+exp`).
3. **Autenticar los tres endpoints** con el mismo esquema `App-Token`/
   `App-User` que ya usa el resto de la API, o restringir por IP de
   origen (llamadas servidor-a-servidor / cron interno).
4. **Sacar credenciales del código** hacia variables de entorno o un
   vault, incluyendo `lib/param.inc.php` y `envioCorreo.php`.
5. **Reactivar `verify_peer`/`verify_peer_name`** en la conexión SMTP TLS.
6. Agregar **chequeo de duplicados** antes de `addDocument` en `save`
   (o un `UNIQUE` a nivel de BD sobre `pasano+pasmes+pascii`).
7. Definir una **política de reintento** explícita para documentos en
   `error` (ej. un evento `retry` que vuelva a poner `pending` los que
   tengan menos de N intentos).
8. Unificar la URL de destino de `generatePDFv2.php` con la variable de
   ambiente usada en `statement_process.php` (evitar el hardcode a
   producción).
9. Eliminar código muerto (`sendEmailAWS`, `sendEmailPA`) o moverlo a un
   módulo claramente marcado como legacy/no usado.
10. Revisar la exclusión de notas crédito/débito y abonos
    (`extfue == '55'` únicamente) en `statement_customers.php` — confirmar
    con negocio si los saldos deben netear esos movimientos.
11. **`crcclires` no tiene CRUD ni alcance por período**: hoy la única
    forma de agregar/quitar un cliente es escribiendo/borrando filas
    directo en BD, y la restricción es indefinida (no aplica solo a un
    mes puntual). Si el negocio necesita restricciones temporales
    (ej. "omitir solo el extracto de marzo"), habría que agregar columnas
    de período o una fecha de expiración y filtrar por ellas en
    `getCustomersByYearMonth`/`getRestrictedCustomersByYearMonth`.

## 9. Guía rápida para retomar el trabajo

- **Probar `save` para un período específico** (sin esperar al día 5-8,
  hay que ajustar temporalmente el chequeo de fecha o correr en la
  ventana real):
  `GET statement_process.php?event=save&month=03&year=2026`
  - La respuesta incluye `excluded` con el número de clientes que
    calificaban para el período pero fueron omitidos por estar en
    `crcclires`.
- **Probar la exclusión por `crcclires`**: insertar una fila de prueba
  (`INSERT INTO crcclires (rescli, resusu, resfec) VALUES ('<id_cliente>',
  '<usuario>', CURRENT)`) para un cliente que tenga extracto en el período
  a probar, correr `save` y confirmar que: (1) no aparece en `crcpas` como
  nuevo `pending`, (2) sí aparece una línea `info` en
  `log/statementProcessYYYYMM.txt`, y (3) el contador `excluded` de la
  respuesta aumentó en 1.
- **Probar `send`** para el mismo período una vez existan documentos
  `pending` en `crcpas`:
  `GET statement_process.php?event=send&month=03&year=2026`
- **Probar solo el PDF/correo de un cliente puntual** (sin pasar por la
  cola), llamando directo:
  `GET generatePDFv2.php?month=03&year=2026&customer=<id>&email=<correo>&link=statement`
- **Ver solo el HTML del extracto** (para depurar el diseño, sin generar
  PDF ni enviar correo):
  `GET invoices/statement_customers.php?month=03&year=2026&customer=<id>`
- Antes de pasar a producción, revisar y actualizar:
  - `$address` en `statement_process.php` (línea ~13-14).
  - `$targetUrl` hardcodeado en `generatePDFv2.php` (línea ~48).
  - `$email_copy` / destinatarios fijos en `generatePDFv2.php`.
- Logs a revisar ante fallos: `log/statementProcessYYYYMM.txt` (proceso) y
  errores guardados en `crcpas.paserr` por documento.
