# Resultados de verificación de Hoja de Distribución

Fecha: **2026-08-05**. Rama: `nfeat-1517-HojaDeDistribucion`. Tiquete 1517.

## ⚠️ Decisión que hay que confirmar con Andrea o Manuel

La revisión del 2026-07-29 de este mismo documento decía:

> Por instrucción posterior del responsable, no se continúan ni se agregan
> entornos de prueba al repositorio. El entorno Docker temporal fue retirado.

La instrucción para este trabajo pide lo contrario y de forma explícita: crear
un ambiente aislado con PHP 5.6 y MySQL si no existe, y no declarar la tarea
terminada si no se pudo ejecutar en PHP 5.6. Se siguió la instrucción nueva y
se agregó `docker/hoja-test`.

**Queda a criterio de Andrea o Manuel si `docker/hoja-test` se conserva en el
repositorio o se retira antes del merge.** Si se retira, se pierde la única
forma reproducible que hay hoy de correr el módulo en el intérprete del cliente;
los comandos quedan documentados abajo para poder rearmarlo.

## Ambiente utilizado

| Componente | Versión | Origen |
|---|---|---|
| PHP | **5.6.40** (cli, built Jan 23 2019) | imagen oficial `php:5.6-cli` más `pdo_mysql` y `mysqli` (`docker/hoja-test/Dockerfile`) |
| MySQL | **5.7.44-log**, `server_id=77`, binlog activo | imagen oficial `mysql:5.7` |
| PHPUnit | 5.7.27 | `bin/phpunit` del proyecto |
| Symfony | 2.7 | `composer.json` sin cambios |
| Twig | 1.x del proyecto | sin cambios |
| Base | `mbinv_hoja_test`, creada vacía | esquema mínimo de `src/MB/ProductoBundle/Tests/Fixtures/hoja_esquema_minimo.sql` |

No se tocó ninguna base real. No se usó `demomasamadre` ni ninguna base de
cliente. `app/config/parameters.yml` del desarrollador **no se modificó**: el
contenedor lo tapa con `docker/hoja-test/parameters_test.yml`, que no contiene
secretos.

## Comandos reproducibles

```bash
# 1. Levantar la base aislada
docker compose -f docker/hoja-test/docker-compose.yml up -d db

# 2. Cargar el esquema mínimo y la migración de integridad
docker exec -i hoja-test-db-1 mysql -uroot -phojatest mbinv_hoja_test \
  < src/MB/ProductoBundle/Tests/Fixtures/hoja_esquema_minimo.sql
docker exec -i hoja-test-db-1 mysql -uroot -phojatest mbinv_hoja_test \
  < scripts/migrations/20260805_hoja_distribucion_integridad.sql

# 3. Construir la imagen de PHP 5.6 con pdo_mysql
docker compose -f docker/hoja-test/docker-compose.yml build php

# 4. Sintaxis con PHP 5.6 real
docker run --rm -v "$PWD":/app -w /app php:5.6-cli \
  bash scripts/testing/hoja-lint-php56.sh

# 5. Pruebas, twig y yaml
docker compose -f docker/hoja-test/docker-compose.yml run --rm php \
  "php bin/phpunit src/MB/ProductoBundle/Tests/Clases"
docker compose -f docker/hoja-test/docker-compose.yml run --rm php \
  "php app/console lint:twig src/MB/ProductoBundle/Resources/views/Hoja"
docker compose -f docker/hoja-test/docker-compose.yml run --rm php \
  "php app/console lint:yaml app/config"

# 6. Desmontar
docker compose -f docker/hoja-test/docker-compose.yml down -v
```

## Resultados

| Verificación | Resultado |
|---|---|
| `php -l` en PHP 5.6.40 sobre los PHP modificados | **OK**, sin errores |
| `php app/console lint:twig` en `views/Hoja` | **6 de 6 válidos** |
| `php app/console lint:yaml app/config` | **10 de 10 válidos** |
| `bin/phpunit src/MB/ProductoBundle/Tests/Clases` | **OK — 95 pruebas, 370 assertions** |
| `git diff --check` | limpio |

De esas 95 pruebas, **32 ejecutan SQL real** contra MySQL (3 de ciclos de
inventario, 22 de servicios, 7 de exportación remota), y 7 de esas 32 además
levantan un receptor HTTP simulado en memoria (Guzzle `MockHandler`, sin red
real). Sin `HOJA_TEST_DSN` esas 32 se marcan `skipped` en lugar de fallar, así
que la suite sigue siendo verde en una máquina sin Docker.

### Pruebas por archivo

| Archivo | Qué cubre | Contra la base |
|---|---|---|
| `HojaCicloInventarioTest` | 3 vueltas de aplicar parcial → incremental → desaplicar → reaplicar → reemplazar → desaplicar todo → reaplicar todo; que no se borren documentos ni movimientos; que la salida de bodega no se infle al reaplicar | sí |
| `HojaServiciosBaseDatosTest` | criterio de tiendas, diagnóstico, vales (localidad, oferta, mayoreo, sin permiso, tienda vacía, hoja inexistente), las 4 modalidades de ventas, acumulado, filtro con/sin venta, EXPLAIN, auditoría rápida, detallada, individual, falsos positivos tras reaplicar, comparación y sincronización de valores | sí |
| `HojaExportacionRondaCompletaTest` | contrato HTTP completo contra un receptor simulado: confirmación y `exportado='S'`, rechazo sin confirmar, error HTTP, reintento sin reenviar, estado por tienda, y la condición de carrera (otro proceso confirma un documento del mismo lote mientras el nuestro está en vuelo) | sí, más un receptor HTTP simulado (sin red real) |
| `HojaValeServiceTest` | las 4 reglas de precio del vale | no (cálculo puro) |
| `HojaVentasServiceTest` | cálculo de intervalos acumulado y no acumulado | no |
| `HojaGastosCalculatorTest` | fórmula de `frmGastos` con resultado conocido (725 y 7.25) | no |
| `HojaDetalleValidatorTest` | cuadre `total = bodega + tiendas` | no |
| `HojaAuditoriaServiceTest` | comparador de cantidades | no |
| `HojaPermisoServiceTest`, `HojaExportacionServiceTest`, `DocumentoAnulacionServiceTest` | permisos, clave de lote, validaciones de anulación | no |

### Defecto encontrado por `HojaExportacionRondaCompletaTest`

Guzzle 6 lanza excepción en respuestas 4xx/5xx por defecto
(`http_errors=true`). `HojaExportacionService::enviar()` asumía que
`post()` siempre devolvía el objeto de respuesta y validaba el código a mano
(`if ($codigo < 200 || $codigo >= 300) { throw ... }`); ese chequeo era código
muerto porque la llamada ya había lanzado antes de llegar ahí. Se corrigió
agregando `'http_errors' => false` a las opciones de la petición, y se agregó
una captura de `GuzzleException` para errores de red (timeout, DNS) que
también convierte el mensaje a uno de dominio en español.

### Migración de integridad en MySQL aislado

| Escenario | Resultado |
|---|---|
| Primera pasada | crea los 5 índices y `maedoc_anulado`; exit 0 |
| Segunda pasada (idempotencia) | todo responde "ya existe"; exit 0 |
| Rollback | quita índices, conserva `maedoc_anulado` y `hoja.version`; exit 0 |
| Rollback repetido | idempotente; exit 0 |
| Con duplicados sembrados en `hoja_detalle` | **exit 1** nombrando el problema, sin crear el índice y **sin borrar las 2 filas** |
| Duplicado corregido y nueva pasada | exit 0 |

### Recuento de seguridad por endpoint

Extraído del código, no de la intención: los 48 endpoints de `HojaController`
validan permiso, y las 24 escrituras `POST` validan además el token CSRF. No hay
endpoint de escritura sin CSRF ni endpoint sin permiso.

## Lo que sigue sin verificarse

1. **Exportación remota contra un receptor real.** El contrato HTTP completo
   (envío, confirmación, rollback, reintento, concurrencia) ya se probó contra
   un receptor simulado en memoria (`HojaExportacionRondaCompletaTest`), pero
   no hay `hoja_exportacion_url` configurada en ningún ambiente real; el
   servicio se niega a enviar sin ella. Además está pendiente, sin fecha,
   confirmar con el cliente si este canal ODBC sigue vigente o ya fue
   sustituido por otro mecanismo — se preguntó explícitamente y la respuesta
   fue que hay que confirmarlo con el cliente. Es la única fila `PARCIAL` de la
   matriz.
2. **Revisión visual de los PDF.** `lint:twig` sólo dice que las plantillas
   compilan, no que el vale, la hoja, los gastos y las ventas se vean bien
   impresos.
3. **Recorrido de interfaz por un operador de VB6.** Ninguna prueba automática
   sustituye eso.
4. **Datos del cliente.** El esquema de pruebas es mínimo: tiene las tablas y
   columnas que el módulo toca, no el esquema completo de producción. Una
   instalación real puede tener columnas, triggers o vistas que cambien el
   comportamiento.
5. **Concurrencia real.** El correlativo se protege con `GET_LOCK` más índice
   único y hay pruebas de que la lógica revalida dentro de la transacción, pero
   no se ejecutaron dos solicitudes simultáneas de verdad.

La aceptación funcional debe ejecutarse siguiendo
`docs/hoja-distribucion-pruebas-manuales.md` sobre una copia controlada de datos
reales.
