# Migración progresiva a PHP 8.3 y Laravel 12 (pasando por Laravel 10)

**Estado:** propuesta · 22/10/2026
**Destino:** PHP 8.3 y, si es posible, Laravel 12 (decidido 22/10/2026). Laravel 10 / PHP 8.2 es el primer hito.
**Rama:** `feature/migracion-laravel10` (desde `main` en `15e8fae2`)
**Origen:** fallo de seguridad de PHP divulgado el 21/10/2026. PHP 7.3 no recibe parches desde diciembre de 2021.
**Prioridad:** máxima. El módulo de OC y Clasificación queda en pausa (rama `feature/oc-clasificacion`).

---

## 1. Objetivo y reglas del plan

Llevar el CTC de **Laravel 8.76.2 / PHP 7.3** a **PHP 8.3 y Laravel 12** sin afectar ninguna funcionalidad. El camino tiene dos hitos:

- **Hito 1 (fases 0 a 5):** Laravel 10 / PHP 8.2. Saca a producción de PHP 7.3 lo antes posible.
- **Hito 2 (fases 6 a 10):** PHP 8.3, Livewire 3, Laravel 11, Laravel 12 y cierre.

Reglas que aplican a todas las fases:

1. **Primero las correcciones de código, después el cambio de versión.** Cada fase deja el código funcionando en local antes de pasar a la siguiente, así el cambio de versión llega con el código ya probado.
2. **No se despliega nada hasta completar el hito 1 (decidido 22/10/2026).** Las fases 0 a 4 se trabajan y prueban solo en local, en la rama `feature/migracion-laravel10`. El único despliegue es el corte de la fase 5, y se puede revertir porque el servidor actual queda intacto. Por eso, en la rama de migración el código ya no necesita seguir siendo compatible con PHP 7.3. Las correcciones urgentes de producción se hacen en `main` (que sigue en 7.3) y se pasan a la rama de migración con merge.
3. **No hay bases de prueba: todas son de producción (decidido 22/10/2026).** Las pruebas automáticas son de solo lectura. Las pruebas que registran datos las hace a mano el responsable de la migración, cuando se necesiten, con el grupo de pruebas (grupo 2). El entorno local se configura para que no envíe correos, no ejecute comandos programados y no escriba por su cuenta (tarea 0.2).
4. **CLAUDE.md sigue vigente:** aislamiento por tenant, sin `migrate`, sin escribir en `websimex`/`websystem`. La regla de compatibilidad con PHP 7.3 (CLAUDE.md §2) se mantiene hasta la fase 4. Desde ese momento, el mínimo pasa a ser PHP 8.0, y PHP 8.2 desde la fase 8.
5. **No se mezclan mejoras funcionales con la migración.** Las excepciones son los arreglos de seguridad listados en la fase 0.

---

## 2. Por qué el destino es PHP 8.3 y Laravel 12

| Versión | Fin de parches de seguridad | Estado al 22/10/2026 |
|---|---|---|
| PHP 7.3 (actual) | 06/12/2021 | Sin soporte desde hace casi 5 años |
| PHP 8.2 (destino) | 31/12/2026 | Le quedan ~2 meses |
| PHP 8.3 | 31/12/2027 | Con soporte |
| PHP 8.4 | 31/12/2028 | Con soporte |
| Laravel 8 (actual) | 24/01/2023 | Sin soporte |
| Laravel 10 (destino) | 04/02/2025 | **Ya sin soporte** |
| Laravel 12 | 24/02/2027 (verificar) | Con soporte |

Laravel 10 / PHP 8.2 ya no es un destino seguro, pero es un escalón obligado: no se puede saltar de Laravel 8 a 12 sin pasar por 9, 10 y 11. **Decidido 22/10/2026: el destino es PHP 8.3 y, si es posible, Laravel 12.**

Versiones de PHP que soporta cada Laravel, y que fijan el orden de las fases:

| Laravel | PHP soportado | Livewire | En este plan |
|---|---|---|---|
| 9 | 8.0 – 8.2 | 2.12 | Fase 4, con PHP 8.2. **No soporta 8.3** |
| 10 | 8.1 – 8.3 | 2.12 o 3 | Fase 5: corte al servidor nuevo con PHP 8.3. Livewire 3 (fase 7) se adopta estando aquí |
| 11 | 8.2 – 8.4 | Solo 3 | Fase 8 |
| 12 | 8.2 – 8.4 | Solo 3 | Fase 9 |

**Puntos que deciden si se llega a Laravel 12** (ver fases 8 y 9):

- que cada dependencia tenga una versión compatible: JWT, IMAP, dompdf, avatar y heroicons;
- el cambio de Carbon 2 a Carbon 3, que modifica `diffIn*`.

Si alguno de esos puntos bloquea, el plan se detiene en **Laravel 11 / PHP 8.3**, que también tiene soporte. El objetivo de PHP 8.3 se cumple de todas formas en la fase 5.

---

## 3. Punto de partida (análisis del 22/10/2026)

Análisis de solo lectura del código propio. Se excluyeron `vendor/` y los archivos de respaldo.

### 3.1 Tamaño

| Elemento | Cantidad |
|---|---|
| Archivos PHP en `app/` | 881 |
| Vistas Blade | 803 |
| Componentes Livewire | 165 (Livewire 2.12.8) |
| Controladores | 159 |
| Comandos Artisan | 19, de los que 14 están programados |
| Rutas | 661 web y 56 api. De ellas, 409 usan `'Controller@method'` |
| Exports e Imports de Excel | 86 y 44 |
| Tests | Ninguno real (solo `ExampleTest`) |
| CI / Docker / despliegue en el repo | No hay |

### 3.2 PHP 7.3 → 8.2

Todo el código pasa `php -l` con PHP 8.2: **no hay errores fatales de compilación.** Los 1.702 archivos PHP y las 798 vistas Blade compiladas pasaron el lint. El riesgo está en la ejecución.

| Hallazgo | Ocurrencias | Archivos | Efecto en PHP 8 | Fase |
|---|---|---|---|---|
| Aritmética sobre texto de Excel, BD o request (`$row["cantidad"]/$row["cant_empaque"]`) | ~125 | 49 (sobre todo `app/Imports/`) | `TypeError` si llega `""` o texto. Además, ~39 divisiones pueden lanzar `DivisionByZeroError` | 1 |
| `count()` sobre un valor que puede ser null | 5 | 3 (`TableroController:218`, `ExTableroController:221`, `Api/v1/APIRestController:287-301`) | `TypeError` | 1 |
| Valor del request comparado con `0` (`$request->cboDinamico != 0`) | 14 | 5 (`JMetodos`, `POController`, `ExPedidoController`, `ExPOController`) | Cambia el resultado: `"abc" == 0` pasa a ser `false` | 1 |
| Otras comparaciones laxas (`== ''`, `== 0`, `in_array` sin estricto, `switch` numérico) | ~1.570 | ~200 | Cambio de comportamiento, solo en casos de borde. Se revisan por muestreo | 1 |
| Parámetro opcional antes de uno obligatorio | 8 firmas | 5 (`ExPOController:1157, 2916`, `ReportesController:173, 238`, `Api/v1/ApiContenedorController:44, 93`, `Api/v2/ApiContenedorController:14`, `UnirMundos:27`) | Deprecación. En controladores puede cambiar cómo se resuelven los parámetros de ruta | 1 |
| Null pasado a funciones nativas (`trim`, `explode`, `strtotime`, `number_format`, etc.) | ~686 | ~184 | Deprecación desde PHP 8.1. Solo es crítico si las deprecaciones se convierten en excepción (ver fase 3) | 1 y 3 |
| `utf8_encode` / `utf8_decode` | 19 | 10 | Deprecación en PHP 8.2 | 1 |
| Propiedades dinámicas | 2 | `ReportesExport:27`, `ExReportesExport:26` | Deprecación en PHP 8.2 | 1 |
| Tipos nativos en PDO MySQL (PHP 8.1 devuelve `int`/`float`, ya no `string`) | Toda la app | — | Cambian las comparaciones estrictas y los formatos | 1 (`ATTR_STRINGIFY_FETCHES`) |
| `each`, `create_function`, `$s{0}`, ternarios anidados, `match` como nombre, `mysql_*`, `${var}`, `strftime`, `Serializable` | 0 | — | — | — |

### 3.3 Laravel 8 → 9 → 10

| Hallazgo | Ocurrencias | Rompe en | Fase |
|---|---|---|---|
| `DB::select(DB::raw($sql))` y `DB::connection()->select(DB::raw(...))` | 54 | 29 (`EasyReport`, `EasyReportAlias`, `ExHistoricoExport`, exports de Cargill, Kpi, TorreControl) | L10 | 2 |
| `TrustProxies` extiende `Fideloper\Proxy` y usa `HEADER_X_FORWARDED_ALL` | 1 | `app/Http/Middleware/TrustProxies.php` | L9 | 4 |
| `\Fruitcake\Cors\HandleCors` | 1 | `app/Http/Kernel.php` | L9 | 4 |
| `CheckForMaintenanceMode` | 1 | `app/Http/Middleware/` | L9 | 4 |
| Flysystem 3: `storeAs` devuelve `false` en lugar de lanzar excepción | 3 subidas a S3 sin verificar el resultado | `Yara/Supplyrecord:505`, `Importaciones/ImusaAbastecedores:953`, `Operaciones/CargarDex:64` | L9 | 2 |
| `config/mail.php` en formato antiguo (`driver`, `MAIL_DRIVER`), con Symfony Mailer en lugar de SwiftMailer | 36 envíos y 14 Mailables | 30 | L9 | 2 (configuración) y 4 (prueba) |
| `config/filesystems.php` sin la opción `throw`. `FILESYSTEM_DRIVER` pasa a llamarse `FILESYSTEM_DISK` | — | 2 | L9 | 4 |
| Debugbar registrado a mano en `config/app.php`, con un alias mal escrito | 1 | `config/app.php:181` | Ya rompe con `composer install --no-dev` | 0 |
| 13 nombres de ruta duplicados (`exportpoex` ×10, `rdeldet` ×3, `importpo`, `exportReporte`, `roperacion2`…) y `Auth::routes()` dos veces | 13 | `routes/web.php` | Ya impide `route:cache` | 0 |
| Factory legacy (`$factory->define`) | 1 | `database/factories/UserFactory.php` | L9/L10 | 2 |
| `app/Utils/Collection.php` extiende `Illuminate\Support\Collection` | 1 | — | L10 (tipos y genéricos) | 5 |
| `env()` fuera de `config/` | 2 | `app/Services/ShipsgoService.php:14-15` | Con `config:cache` | 2 |
| `$dates`, `dispatchNow`, helpers `str_*`/`array_*`, regla `password`, `Swift_*`, `getDriver()` | 0 | — | — | — |
| Livewire 2.12.8 (165 componentes) | — | — | Soporta L9 y L10 sin cambios. **No soporta L11** | 7 |
| `RouteServiceProvider::$namespace` (409 rutas en texto) | — | — | Sigue funcionando. Hay que conservar la propiedad al actualizar el provider | 4 |

### 3.4 Dependencias (versiones instaladas según `composer.lock`)

| Paquete | Instalado | Destino L10 / PHP 8.2 | Uso en el código | Fase |
|---|---|---|---|---|
| `laravel/framework` | 8.76.2 (versión fija) | 8.83.x → ^9 → ^10 | — | 2, 4 y 5 |
| `nesbot/carbon` | 2.54.0 (versión fija) | ^2.62.1 | Toda la app | 2 |
| `fideloper/proxy` | 4.4.2 | Se elimina (el framework lo trae desde L9) | `TrustProxies` | 4 |
| `fruitcake/laravel-cors` | 2.2.0 | Se elimina (el framework lo trae desde L9) | `Kernel` | 4 |
| `tymon/jwt-auth` | 1.0.2 (con lcobucci/jwt 3.3) | `^2` o el fork `php-open-source-saver/jwt-auth` (verificar) | 8 archivos: guard `api`, `User`, `JwtMiddleware`, `UserController`, `Api/v1/AuthController`, `routes/api.php` | 4 |
| `consoletvs/charts` | 6.9.0, abandonado | Reemplazar por Chart.js directo en las vistas | 11 PHP (`app/Charts`, TorreControl, ExTorreControl, ExDashboard, Chart, Movil) y 9 vistas | 2 |
| `webklex/laravel-imap` | 2.4.0 | ^5, con otra API (verificar) | Solo `POController.php:~1353` | 4 |
| `league/flysystem-aws-s3-v3` | 1.0.30 | ^3 | `Storage` y `store()` (10 usos). `FileAWS`/`ConexionAWS` usan el SDK de AWS directamente y no les afecta | 4 |
| `laravel/ui` | 2.3.0 | ^3.4 (L9) → ^4 (L10) | `Auth::routes()` y las vistas de `auth/` | 4 y 5 |
| `laravel/legacy-factories` | 1.4.2 | Se elimina | `UserFactory` | 2 |
| `barryvdh/laravel-dompdf` | 2.2.0 | ^2 (ya soporta L10). El alias pasa a `Facade\Pdf` | 6 archivos | 4 |
| `laravolt/avatar` | 4.1.7 | ^5 (verificar) | 1 vista | 4 |
| `blade-ui-kit/blade-heroicons` | 1.7.0 | ^2 (cambian los nombres de los iconos; verificar) | 10 vistas, 28 usos | 4 |
| `maatwebsite/excel` | 3.1.69 (con PhpSpreadsheet 1.30.6) | Se queda | 130 clases | — |
| `livewire/livewire` | 2.12.8 | Se queda en 2.12 hasta la fase 7 | 165 componentes | — |
| `aws/aws-sdk-php`, `guzzlehttp/guzzle`, `smalot/pdfparser` | — | Se quedan | — | — |
| `realrashid/sweet-alert`, `mallardduck/blade-lucide-icons` | — | Se eliminan: no tienen uso | 0 | 2 |
| `facade/ignition` | 2.17.7 | `spatie/laravel-ignition` (requiere PHP 8) | — | 4 |
| `fzaninotto/faker` | 1.9.2 | `fakerphp/faker` | — | 2 |
| `beyondcode/laravel-dump-server` | 1.9.0 | Se elimina | — | 2 |
| `nunomaduro/collision` | 5.11 | ^6 (L9) → ^7 (L10) | — | 4 y 5 |
| `laravel-mix` (front-end) | ^4.0.7, con webpack 4 | Se queda. Solo compila `app.js` y `app.scss`, que usa `login.blade.php`. Vite es opcional | — | — |

### 3.5 Problemas que ya existen hoy y se arreglan en la fase 0

- **Seguridad:**
  - contraseña SMTP como valor por defecto en `config/mail.php:91`;
  - credenciales AWS en `app/ConexionAWS.php:29` (CLAUDE.md §6) y credenciales IMAP en `app/JMail.php`;
  - **inyección SQL** por concatenar `session()` en `app/Exports/COsiEta.php:60`, `COConEta`, `COConETD` y `COSinEta`.
- **Archivos rotos:**
  - `app/Models/LugarDevolucion.php` tiene bytes corruptos antes de `<?php`. Parece una copia muerta, porque el código usa `App\LugarDevolucion`.
  - `app/Imports/importacionpasar2.php:468` tiene un error de sintaxis.
  - `exdefinicionreportes/index.blade.php` y `exdefiniciontableropo/index.blade.php` tienen un `@if` sin cerrar.
- **Repositorio:** los `.DS_Store` están versionados, porque el patrón `**/ .DS_Store` del `.gitignore` tiene un espacio. Además hay respaldos que no están en la lista de CLAUDE.md §5:
  - `PoImportGlobal7062020.php`, `PoImportGlobal23jun2020.php` y `JPoImport_prueba.php`;
  - `ExPo/create18oct2022.blade.php`, `DefCamposPO/create.blade15fe.php`, `create.blade2.php` y `edit.blade2.php`;
  - `ExTablero/create.blade11feb2021.php`, `Factura/create.blae.php` y `Material/*.blade_19042023.php`;
  - `layouts/plantiblank01nov2022.blade.php`.
- **Configuración:** no existe `.env.example`.

---

## 4. Plan por fases

### 4.1 Servidores y estrategia de despliegue (decidido 22/10/2026)

| Servidor | PHP | Qué corre | Hasta cuándo |
|---|---|---|---|
| Actual | 7.3 | Producción. Recibe los despliegues de las fases 0, 1 y 2, todos compatibles con 7.3 | El corte al servidor nuevo |
| Nuevo | **8.3** (Composer 2.10.3) | Recibe la aplicación en Laravel 10, y luego Laravel 11 y 12 | Destino final |
| Local (Herd) | 7.3*, 8.1, 8.2, 8.3, 8.4 | Ensayos de las fases intermedias, que no se despliegan | — |

\* Si Herd no tiene PHP 7.3, la compatibilidad con 7.3 se comprueba con el lint de la CI y desplegando en el servidor actual.

- **Laravel 8 y 9 no soportan PHP 8.3.** Por eso las fases 3 y 4 (PHP 8.1/8.2 con Laravel 9) se ensayan **solo en local**: son escalones de código, no despliegues.
- **El corte a producción del servidor nuevo es con Laravel 10 sobre PHP 8.3** (fase 5, que absorbe la antigua fase 6).
- **Vuelta atrás:** el servidor actual queda intacto y basta con dirigir el tráfico de vuelta a él. Funciona porque los dos servidores usan las mismas bases.
- **Riesgo propio de este esquema:** los dos servidores comparten bases de producción. **El cron de `schedule:run` (sincronización con el ERP, reportes y correos) solo puede estar activo en un servidor a la vez.** Si queda activo en los dos, los `sync:*` corren dos veces y los reportes se envían dos veces. Al cortar, se desactiva en el actual y se activa en el nuevo, en ese orden.
- La versión que hoy corre en el servidor actual es la misma de `main` (confirmado 22/10/2026).

### 4.2 Resumen

| Fase | Qué | Versiones en producción al terminar | Días estimados* |
|---|---|---|---|
| Fase | Qué | Producción al terminar | Dónde se prueba | Días estimados* |
|---|---|---|---|---|
| 0 | Red de seguridad: configuración local segura, pruebas mínimas, limpieza y arreglos de seguridad | Actual: L8.76 / PHP 7.3 | Local 7.3 y producción | 4–6 |
| 1 | Código compatible con PHP 8 (y que siga corriendo en 7.3) | Actual: L8.76 / PHP 7.3 | Local 8.2/8.3 y producción | 5–8 |
| 2 | Adelantar en Laravel 8 los cambios de 9 y 10 que también funcionan en 8, y actualizar las dependencias | Actual: L8.83 / PHP 7.3 | Local y producción | 4–6 |
| 3 | Ensayo de L8.83 sobre PHP 8.1 | Sin cambios | **Solo local** | 1–2 |
| 4 | Laravel 9 sobre PHP 8.2 | Sin cambios | **Solo local** | 4–6 |
| 5 | Laravel 10 sobre PHP 8.3 y **corte al servidor nuevo** (**fin del hito 1**) | **Nuevo: L10 / PHP 8.3** | Local 8.3 y servidor nuevo antes del corte | 4–6 |
| 6 | *(Absorbida por la fase 5: el servidor nuevo ya tiene PHP 8.3)* | — | — | — |
| 7 | Livewire 2 → 3 sobre Laravel 10 | Nuevo: L10 / PHP 8.3 / Livewire 3 | Local y producción | 10–15 |
| 8 | Laravel 11 | **Nuevo: L11 / PHP 8.3** | Local y producción | 3–5 |
| 9 | Laravel 12 (**fin del hito 2**) | **Nuevo: L12 / PHP 8.3** | Local y producción | 3–5 |
| 10 | Estabilización y cierre | Nuevo: L12 / PHP 8.3 | — | 3–5 |

\* Días de desarrollo de una persona. No incluyen la validación funcional de los usuarios. Hito 1 (fases 0–5): **22–34 días**. Hito 2 (fases 7–10): **19–30 días**. Total aproximado: **41–64 días**.

**Si el fallo exige salir de PHP 7.3 antes:** las fases 3 y 4 son solo locales, así que se pueden comprimir. El orden se mantiene, porque las fases 1 y 2 dejan el código compatible y el servidor viejo sirve de vuelta atrás.

### Fase 0 — Red de seguridad (L8.76 / PHP 7.3)

Sin esta fase no se puede asegurar que no se afecte ninguna funcionalidad: hoy no hay tests ni entorno aislado.

| # | Tarea |
|---|---|
| 0.1 | **Servidor nuevo (parcial 22/10/2026):** PHP 8.3 y Composer 2.10.3 confirmados. El código del servidor actual es igual a `main`. Falta la lista de la sección 6 (extensiones, servidor web, subdominios, acceso a las bases y despliegue) |
| 0.2 | **Configuración local segura, sin bases de prueba (decidido 22/10/2026):**<br>• `.env` local con `MAIL_MAILER=log`: ningún correo sale de local;<br>• no se ejecuta `schedule:run` ni ningún comando `sync:*` o de reportes en local;<br>• `QUEUE_CONNECTION=sync` se mantiene;<br>• las cargas a S3 van al bucket real, y solo se prueban a mano con archivos del grupo de pruebas;<br>• las pruebas que escriben las hace a mano el responsable de la migración, con el grupo 2, y anota qué registros creó para limpiarlos |
| 0.3 | Crear `.env.example` sin secretos, con todas las claves que usa `config/` |
| 0.4 | **Lista de prueba funcional (smoke test) por módulo,** con un usuario del grupo de pruebas (grupo 2) y **verificando que no se vean datos de otro cliente**:<br>• login por subdominio, menú y perfiles;<br>• importaciones y exportaciones (OC V1 y V2, ExPO);<br>• tablero y Torre de Control (con gráficos);<br>• Dashboard V1 y V2;<br>• reportes a Excel (Easy Report, Cargill, KPI);<br>• PDFs (dompdf);<br>• cargas a S3 y descargas;<br>• correos;<br>• API v1/v2 con JWT y app móvil;<br>• IMAP de `POController`;<br>• ShipsGo;<br>• Programación de Arribos;<br>• Seguimiento RIM;<br>• cada comando programado en modo de prueba |
| 0.5 | **Pruebas automáticas mínimas, todas de solo lectura:**<br>• prueba de humo que recorre las rutas GET con un usuario autenticado del grupo 2 y exige que ninguna responda 500. **Usa una lista de exclusión de las rutas GET que escriben:** `ws/insertar`, `actualizaDO_OC`, `actualizarDatos`, `rimpo/pedidosporactualizar`, `rdeldetcs`, `eliminaLote/{id}`, `actualizaMaterial`, `rimpodocumentos/eliminar`, `exvistas/guardar`, `parametros/guardar`, `clear-cache` y las demás que aparezcan al revisar `web.php` y `api.php`;<br>• **la prueba envuelve cada petición en una transacción que se revierte** en las conexiones propias (`mysql`, `cti`, `central`), como segunda barrera;<br>• pruebas de caracterización de `JMetodos.php` y de la conversión numérica de 3–5 importadores Excel. Prueban la lectura y el cálculo, sin guardar;<br>• login JWT de la API |
| 0.6 | **CI en GitLab:** `php -l` con 7.3 y 8.2, compilación de las vistas Blade, `composer validate` y las pruebas de 0.5 |
| 0.7 | **Limpieza del repositorio:**<br>• corregir `.gitignore` y dejar de versionar los `.DS_Store`;<br>• eliminar los respaldos de CLAUDE.md §5 y los de la sección 3.5. Antes, confirmar con `grep` que ninguna ruta ni vista los referencia;<br>• arreglar los 4 archivos rotos (sección 3.5) |
| 0.8 | **Rutas:**<br>• resolver los 13 nombres duplicados y el doble `Auth::routes()`. Antes, buscar cada `route('nombre')` para mantener el destino actual: Laravel usa la última definición;<br>• comprobar que `php artisan route:cache` funcione |
| 0.9 | **Arreglos de seguridad que no cambian funcionalidad:**<br>• inyección SQL en los 4 exports `CO*`;<br>• sacar la contraseña SMTP de `config/mail.php`, y las credenciales de `ConexionAWS` y `JMail`, al `.env`;<br>• quitar el provider y el alias de Debugbar de `config/app.php`, para que se registre solo por auto-discovery en desarrollo;<br>• confirmar que producción no tiene `APP_DEBUG=true`.<br>**Rotar las credenciales expuestas:** SMTP, AWS e IMAP |

**Cómo se comprueba:** la prueba de humo y la lista del 0.4 pasan en local, y la CI queda en verde.

#### Avance de la fase 0 (22/10/2026, rama `feature/migracion-laravel10`)

| Tarea | Estado | Commit |
|---|---|---|
| 0.2 | **Hecho.** `.env` local con `MAIL_DRIVER=log` y `MAIL_MAILER=log` (la línea original quedó comentada). Hallazgo: el PHP global de Herd pasó a 8.3, y `ctc` no tiene versión aislada. Mientras no se haga `herd isolate`, los comandos de artisan se corren con `php74` | — (`.env` no se versiona) |
| 0.3 | **Hecho.** `.env.example` con todas las claves del proyecto, sin secretos | `97fffaa2` |
| 0.7 | **Hecho:**<br>• `.gitignore` corregido; los `.DS_Store` dejan de versionarse;<br>• **`.old_env` estaba versionado desde el 15/12/2023, con 10 credenciales de producción:** deja de versionarse, pero siguen en el historial (ver 0.9);<br>• 40 respaldos eliminados tras verificar que nada los referencia. Declaraban la misma clase que el archivo vigente, y con el autoloader optimizado Composer podía cargar el respaldo;<br>• `RegaliasTipoDocumentoCliente` vivía en `ExpoUnidadesInspeccion.php.php` y solo cargaba con el mapa de clases optimizado: se renombró;<br>• `app/Models/LugarDevolucion.php` (bytes corruptos) reparado: rompía `GET api/v1/contenedores/lugardevolucion/{ige}`;<br>• dos vistas `exdefinicion*` ya compilan | `97fffaa2`, `9c5be17b`, `6f014c79` |
| 0.8 | **Hecho.** 21 nombres duplicados quitados (11 nombres). Cada nombre quedó en la última definición, la que Laravel ya usaba, y ningún `route()` los usa. Se desactivó `ex_datosordenes/{oc}`, cuyo controlador no existe. `route:cache` y `route:list` ya funcionan. El `Auth::routes()` doble se deja: quitarlo cambiaría `GET /register` | `9012f5d0` |
| 0.9 | **Parcial:**<br>• `MAIL_PASSWORD` sin valor por defecto en el código;<br>• credenciales de `JMail` a `services.jmail`;<br>• Debugbar por auto-discovery;<br>• `ConexionAWS` ya leía de `config()`: la infracción de CLAUDE.md §6 estaba resuelta.<br>Los 4 exports `CO*` con SQL concatenado son código muerto (nadie los usa y `session('Compania')` no se asigna): no se tocan. Falta la tarea 0.10 y la rotación de credenciales | `f176e318` |
| 0.10 | **Hecho (decidido 22/10/2026).** Inyección SQL por concatenación directa de datos del request (H1):<br>• `JMetodos` `Filtros`/`EXFiltros` (54 inserciones): escapadas con `sqlTexto()`/`sqlLike()` (`PDO::quote`), y los ids forzados a entero;<br>• `DeclaracionCambio`, `ExDeclaracionCambio` y `Mensajes`: parámetros enlazados;<br>• `ReportesController`: el filtro de fechas en sesión va escapado.<br>**Caracterización:** 114 de 126 combinaciones generan SQL idéntico. Las 12 restantes son filtros de fecha sin etapa, que antes generaban SQL inválido (`a.id_etapa=)`, error 500) y ahora `a.id_etapa=0`. Una entrada maliciosa queda neutralizada. Las 6 líneas de KPI con `txtFiltro` estaban comentadas | `43490863` |
| 0.11 | **Hecho (H7):** valores sustituidos en plantillas SQL guardadas en la base:<br>• `sqlFecha()` deja pasar sin cambios un valor vacío o con formato `aaaa-mm-dd` (hora opcional), y corta cualquier otro con un 422. Se aplica en KPI, KPI detalle, `ExDashboard`, `ExReportes`, `ReporteKpi` (exports de Cargill), `IndicadoresV2`, `ReportesEspecificos`, `ImportacionesGraph`, `DetalleGraph` y `EasyReport`. Todos los campos de fecha de esas pantallas son `type="date"`;<br>• `?Valor` de los detalles de KPI y de Indicadores pasa por `sqlTexto()`;<br>• `IndicadoresV2` y `ReportesEspecificos` tomaban el importador y `?idGrupoEmpresarial` de propiedades públicas, e `IndicadoresV2` buscaba el nit sin filtrar por cliente. Ahora salen de la sesión, y el importador elegido debe pertenecer al cliente;<br>• `EasyReport`: `id_cliente` se recalcula desde la sesión, y los valores de los filtros van escapados.<br>Sin riesgo y sin cambios: los valores que vienen de la sesión, de la base o de constantes, `ImusaAbastecedores` (arma HTML, no SQL) y `ReporteDinamicoCommand` (corre desde consola) | `cb0a2db4` |
| 0.12 | **Hecho (23/10/2026):** inyección SQL **indirecta**, es decir, una variable que recibe un dato del request y luego se concatena en el SQL. Se encontraron 111 líneas en 12 archivos con un escáner propio (sin contar falsos positivos del query builder ni código comentado):<br>• **`WebServiceController` (rutas `ws/*`, sin sesión):** `tokenValido()`, `usuarioPeticion()` e `index()` concatenaban el token y el usuario. **Con el token `' OR '1'='1`, la consulta devolvía los 474 usuarios y se saltaba la autenticación del servicio (comprobado).** Además, los `UPDATE users`, los 26 `INSERT` de `wspost` y las búsquedas de catálogos. Todo pasó a parámetros enlazados;<br>• **`ExPOController`, API v1 (`APIRestController`), `CompaniaController`, `POController`, `ReprteGlobalBrinsa`, `ExDocumentos`, `ExPedido`, `Reportes`, `Solicitud` y `(Ex)DeclaracionCambio`:** incluye `UPDATE`/`INSERT` sobre `ex_pedido_detalle`, `ex_material`, `ex_po_lotes`, `documentos` y `ex_visualizaciones`;<br>• `documento_leido` (`ExDocumentos`), `casillasDeclaracion` (`WebService`).<br>Con valores normales, el SQL es el mismo. Un valor con comillas, que antes rompía la sentencia, ahora funciona.<br>**También (1.3 indirecta):** 9 comparaciones `$x == 0` con datos del request pasan a `esCero()`. Entre ellas `$nuevaclave == 0` del servicio web: en PHP 8, una clave nueva vacía habría cambiado la clave del usuario | `c0b5051a`, `8e4f8ae4` |
| H2 | **Hecho.** `PoImportYara.php`, la versión que corre en producción (confirmado 22/10/2026), queda como `PoImportAcepalma.php`, y se borra la otra copia | `9b13da4b` |
| 0.10 (cont.) | **Inyección SQL sin autenticar en el login (crítica):** `login.blade.php` (`/`) y `auth/login.blade.php` (`/login`) concatenaban en el SQL el subdominio, que sale del encabezado `Host` o de `?dom=`. Ahora va como parámetro enlazado. También se corrigieron `login2` y `loginanterior` | `8707d90b` |
| 0.4 | **Hecho:** [MigracionListaPruebas.MD](MigracionListaPruebas.MD). Tiene los flujos transversales y los 149 módulos del menú (generados de la tabla `menus`), con la cobertura automática de cada uno: 139 cubiertos, 3 que ya fallan hoy y 10 manuales | — |
| 0.5 | **Hecho:** `php artisan migracion:humo` (`app/Console/Commands/Migracion/PruebaHumoCommand.php`):<br>• 377 rutas, una por proceso;<br>• excluye 130 rutas que escriben;<br>• transacción revertida en todas las conexiones y `max_statement_time`.<br>**Línea base L8.76.2 / PHP 7.4:** 316 con 200, 2 con 302, 57 con 500 que ya existen hoy y 2 manuales. Está en `storage/migracion/` (no se versiona) | — |
| 0.6 | **Hecho:** `.gitlab-ci.yml` con lint en PHP 7.3 (solo `main`) y 8.3, más un job `laravel` que arranca la aplicación **sin bases**, cachea las rutas y compila las vistas. Probado en local simulando la falta de bases. **Falta comprobar que el proyecto de GitLab tenga runners disponibles** | — |

**Credenciales por rotar** (siguen en el historial de git aunque se saquen del código):
- las 10 de `.old_env`: contraseñas de `asercolg_po`, `asercol` (websystem), `asercolg_bitacora` (cti) y `comex` (websimex), la llave secreta de AWS, SMTP, IMAP, `JWT_SECRET`, `APP_KEY` y Pusher;
- la contraseña SMTP que estaba en `config/mail.php`;
- la cuenta de Gmail de `JMail`.

Rotar `JWT_SECRET` invalida los tokens de la API, y rotar `APP_KEY` invalida las sesiones y los datos cifrados con `encrypt()`. Conviene hacerlo en el corte (fase 5).

**Hallazgos que no se corrigen en la fase 0, porque cambiarían el comportamiento** (requieren decisión):

| # | Hallazgo | Riesgo |
|---|---|---|
| H1 | **SQL armado con datos del request:** 38 líneas en 8 archivos (27 en `JMetodos.php`; también `KpiController`, `ExKpiController`, `ReportesController`, `MovilKpiController`, `DeclaracionCambioController`, `ExDeclaracionCambioController` y `MensajesController`). Ejemplos: `JMetodos.php:410` (`= '$request->cboTrans'`) y `DeclaracionCambioController.php:205`. Es inyección SQL explotable por cualquier usuario autenticado | Alto. Propuesta: tarea 0.10, convertir a parámetros enlazados (`?`) sin cambiar el resultado de las consultas, con caracterización de `JMetodos` antes y después |
| H2 | **`PoImportYara.php` declara `PoImportAcepalma`,** igual que `PoimportAcepalma.php`, pero con otra lógica (código de material y cantidad). Con el autoloader optimizado, cuál carga depende del orden de los archivos. En Linux sin mapa de clases optimizado, `App\Imports\PoImportAcepalma` no se encuentra, porque el archivo es `PoimportAcepalma.php` | Alto en el servidor nuevo: hay que definir cuál es la versión correcta del importador de Acepalma |
| H3 | **Nombres de archivo que difieren de la clase solo en mayúsculas:** `ExPoContenedor.php` (`ExPOContenedor`), `PoimportAcepalma.php`, `PoimportAgrosan.php`, `ProcesosTiposTramitesWebsimex.php`. Funcionan en macOS y con el mapa optimizado. En Linux sin mapa optimizado fallan según cómo se escriba la referencia | Medio: renombrar los archivos antes del corte, y desplegar siempre con `composer install --optimize-autoloader` |
| H4 | **249 rutas apuntan a métodos que no existen.** Casi todas son `Route::resource` con métodos sin implementar (`create`, `show`, `edit`…). También `api/postData`, `ws/infotablas/{ge}`, `ws/envioFacturas`, `statusexcel`, `factorconversion`, `doanuladoimusa` y `rexpo/{rexpo}/edit` | Bajo: responden 500 si se visitan. Se puede limitar con `->only([...])` en la fase 2 |
| H5 | Las vistas `ExTorreControl` abren `/rexdatosordenes/{oc}`, que no tiene ruta (404). Probablemente debía apuntar a `ExDatosOrdenesController` | Funcional: confirmar con el negocio |
| H7 | **Plantillas SQL guardadas en la base con marcadores** (`?FechaI`, `?FechaF`, `?Filtro`, `?Valor`, `?company`…). Se reemplazan con `str_replace` en 34 archivos, varios con valores que vienen del request a través de variables intermedias: `KpiController`, `ExKpiController`, `MovilKpiController`, `KpiDetalleController`, `ExReportesController`, exports de Cargill y `ReporteDinamicoCommand` | Alto. Propuesta 0.11: validar el formato de cada valor antes de sustituirlo (fechas `Y-m-d`, ids enteros). Así se conserva la plantilla y el resultado |
| H9 | **Aislamiento entre clientes en Livewire:** 49 componentes guardan la lista de importadores del cliente (`$compania`) o el grupo (`$idGrupoEmpresarial`) en **propiedades públicas**, y 43 las usan en sus consultas. En Livewire 2, el navegador puede modificar las propiedades públicas, así que un usuario podría consultar los datos de otro cliente. Es el patrón que recomienda CLAUDE.md para crear módulos (sembrar `$this->compania` en `mount()`). 7 componentes del Dashboard ya lo resuelven con el trait `ConImportadorValidado` | **Alto (fuga entre clientes).** Propuesta: tarea aparte, aplicar `ConImportadorValidado` o recalcular desde la sesión en cada consulta. Livewire 3 (fase 7) permite además marcar propiedades como `#[Locked]`. Actualizar también el patrón de CLAUDE.md |
| H10 | **Servidor MariaDB de producción:** `Can't create/write to file '/tmp/#sql-temptable…' (Errcode: 30 "Read-only file system")`. Las consultas que necesitan una tabla temporal en disco fallan, unas veces sí y otras no. Afecta al menos a 4 pantallas de exportaciones y a `pedido/procesows`, y explica caídas intermitentes de otras (medido 22/10/2026) | **Alto, de infraestructura y ajeno a la migración.** Reportar a quien administra la base: revisar el espacio y el montaje de `tmpdir` |
| H12 | **Servicio web `wspost`:** busca la PO por número **sin filtrar por grupo empresarial** (`SELECT * FROM PO WHERE po = ?`). Si otro cliente tiene una OC con el mismo número, los adicionales y las actividades se cuelgan a la OC ajena. Además, `GET ws/vererror` (sin sesión) **inserta filas fijas en `actividad` cada vez que se visita**, y `GET ws/u/{usu}/{cla}` muestra sin sesión cómo se calculan las claves y los tokens del servicio | Alto. Requiere decisión funcional: filtrar por `$ige` y retirar o proteger esas dos rutas |
| H13 | **Accesos sin validar al dueño del registro (IDOR):** métodos que leen o modifican por id sin comprobar el grupo empresarial. Por ejemplo, `ExPOController::buscaMaterial($id)`, `eliminaLote($id)` y `vertarifas($id)`, `POController` (material por id), y `CompaniaController::guardarVisualizacion`, que **escribe la configuración de vistas del `idcompania` que llegue en el request** | Alto. Revisar junto con H9 |
| H14 | **Clave de API de Ternium escrita en el código**, dentro de un bloque comentado de `ExDocumentosController::documento_leido` (`Ocp-Apim-Subscription-Key`) | Agregar a la rotación de credenciales |
| H8 | `EjecutarSqlController` ejecuta cualquier SQL que reciba. No tiene ruta ni nadie lo llama: es código muerto | Bajo. Se puede eliminar |
| H6 | Las pantallas `exdefinicionreportes` y `exdefiniciontableropo` usan variables que el controlador no envía (`$datTiporeporte`, `$datCamporelacion`) | Funcional: hoy no funcionan |

### Fase 1 — Código compatible con PHP 8 (sigue en L8.76 / PHP 7.3)

Todo cambio debe funcionar igual en PHP 7.3 y en 8.2, sin sintaxis exclusiva de PHP 8.

| # | Tarea |
|---|---|
| 1.1 | **Aritmética sobre texto (~125 usos en 49 archivos, sobre todo `app/Imports/`):**<br>• un helper `JMetodos`/`App\Support\Numero::aDecimal($valor)` que reproduzca el comportamiento actual de PHP 7.3 (`"" → 0`, `"12abc" → 12`) y avise en el log cuando el valor no sea numérico;<br>• proteger las ~39 divisiones contra el divisor `0`. Hoy, en PHP 7.3, una división por cero da warning y `false`; se mantiene ese resultado de forma explícita.<br>Orden: primero los 10 archivos con más hallazgos (`ExPOController`, `JMetodos`, `PoimportAcepalma`, `PoImportYara`, `PoImportExportGreenfood`, `EasyReport`, `PoImportExporTenaris`, `PoImportExportAsercol`, `PoImportKoba`, `PoImportGlobal`/`PoImportDisan`) |
| 1.2 | `count()` sobre un valor que puede ser null (5 usos): `count($x ?? [])` |
| 1.3 | Las 14 comparaciones de request contra `0`: hacer explícita la intención de PHP 7.3 (por ejemplo, `!empty()` o `(int) $x !== 0`). Revisar por muestreo los `switch` y los `in_array` sin modo estricto en los módulos críticos |
| 1.4 | Las 8 firmas con parámetro opcional antes de uno obligatorio: reordenarlas o quitar el valor por defecto. Antes, verificar las rutas que llaman esos métodos |
| 1.5 | `utf8_encode`/`utf8_decode` (19 usos): `mb_convert_encoding($s, 'UTF-8', 'ISO-8859-1')` y la conversión inversa |
| 1.6 | Propiedades dinámicas: declarar `$idreport` en `ReportesExport` y `ExReportesExport` |
| 1.7 | `PDO::ATTR_STRINGIFY_FETCHES => true` en todas las conexiones MySQL de `config/database.php`. En PHP 7.3 no cambia nada, y en 8.1+ conserva los resultados como `string`, como hoy |
| 1.8 | Null a funciones nativas (~686 usos): se corrigen **solo en la ruta crítica** que señalen la prueba de humo y el log de deprecaciones de la fase 3. El resto es una deprecación sin efecto funcional; se trata en la fase 6 |

**Cómo se comprueba:** la prueba de humo y las de caracterización dan resultados idénticos a los de antes (en local: no se despliega hasta el hito 1).

#### Avance de la fase 1 (22/10/2026)

| Tarea | Estado | Commit |
|---|---|---|
| 1.1 | **Hecho, con otro enfoque.** Se comprobó con el manejador de errores de Laravel que la aritmética sobre texto (`"" + 1`, `"abc" * 2`) y la división por cero **ya fallan en PHP 7.4** (Laravel convierte el *warning* en `ErrorException`), y que los valores válidos dan el mismo resultado en 7.4 y 8.3. No hay que tocar las ~125 operaciones. Lo que sí cambia es la clase de la excepción: `TypeError`, `DivisionByZeroError` y `ValueError` no heredan de `Exception`. Los **66 `catch (\Exception)` reales** (45 archivos, sobre todo importadores Excel) pasan a `\Exception \| \TypeError \| \ValueError \| \ArithmeticError`. No se usa `\Throwable`, para que los errores reales sigan cortando la ejecución como en PHP 7. Comprobado en 7.4 y 8.3 | `cd5ede58` |
| 1.2 | **Hecho.** `count($x ?? [])` en los 5 casos | `e133d86f` |
| 1.3 | **Hecho.** `esCero()` reproduce `== 0` de PHP 7 (idéntico en 21 casos de borde) en las 14 comparaciones. 12 son `txtId == 0`, que decide si una fila se inserta o se actualiza: sin el cambio, en PHP 8 las filas nuevas (`""`) se irían por la rama de actualización | `45f8cd7f` |
| 1.4 | **Hecho.** Las 8 firmas. Laravel resuelve los mismos argumentos en 7.4 y 8.3 | `7f8e3bb8` |
| 1.5 | **Hecho.** `aLatin1()` y `desdeLatin1()` en las 19 llamadas, con resultado idéntico byte a byte | `e133d86f` |
| 1.6 | **Hecho.** `$idreport` declarado | `7f8e3bb8` |
| 1.7 | **Revertida (23/10/2026): la premisa era falsa.** Laravel usa *prepares* nativos (`ATTR_EMULATE_PREPARES=false`), y con ellos PDO MySQL **ya devuelve `int`/`float` en PHP 7.3/7.4**. El cambio de PHP 8.1 solo afecta a los *prepares* emulados, así que entre 7.3 y 8.x no hay diferencia. `STRINGIFY_FETCHES` convertía esos números en texto: los datos de los gráficos de la Torre de Control salían como `"0"` en vez de `0`. Lo detectó la comparación de HTML contra `main`, no la prueba de humo. **Lección:** la prueba de humo compara códigos de respuesta, no contenido. Para las pantallas críticas hay que comparar también el HTML contra `main` (sección 3 de la lista de pruebas) | `7f8e3bb8` → `9f6774eb` |
| 1.8 | **Se pospone a la fase 10, sin riesgo.** Laravel 8.76 ya **registra** las deprecaciones en el log en vez de lanzar excepción (`HandleExceptions::handleDeprecation`). Esto responde también la tarea 3.2. Se agregó el canal `deprecaciones` (`LOG_DEPRECATIONS_CHANNEL`, `null` por defecto) para revisarlas en la fase 3 | (este commit) |

**Pendiente de comparar:** la prueba de humo contra la línea base, con estos cambios, en PHP 7.4.

**H11.** 25 `catch (Exception $e)` en archivos con `namespace` y sin `use Exception;` buscan la clase `App\…\Exception`, que no existe, así que **nunca han atrapado nada**. Varios protegen envíos de correo (`ProgramacionArribos`, `ReporteDinamicoCommand`) y pantallas Livewire de exportaciones. No se tocan en la migración, porque cambiaría el comportamiento. Si se corrigen, el error se mostraría o se registraría en vez de terminar en un 500.

### Fase 2 — Laravel 8.83 y cambios adelantados (sigue en PHP 7.3)

| # | Tarea |
|---|---|
| 2.1 | `laravel/framework` a `^8.83` (la última de L8, que soporta PHP 8.1) y `nesbot/carbon` a `^2.62.1`, sin versiones fijas. `minimum-stability` a `stable` |
| 2.2 | **Las 54 llamadas `DB::select(DB::raw($sql))`** pasan a `DB::select($sql)`. Funciona igual en L8, L9 y L10. Revisar también las que ocupan varias líneas |
| 2.3 | Quitar `env()` de `ShipsgoService` y dejar solo `config()` |
| 2.4 | `config/mail.php` al formato `mailers` (`MAIL_MAILER`), que L8 ya acepta, conservando `MAIL_DRIVER` como respaldo durante la transición |
| 2.5 | Verificar el valor de `storeAs` en las 3 subidas a S3. Si devuelve `false`, mostrar un error y no armar una URL vacía |
| 2.6 | Reescribir `UserFactory` como factory de clase y eliminar `laravel/legacy-factories` |
| 2.7 | **Reemplazar `consoletvs/charts`** por Chart.js directo (11 clases PHP y 9 vistas). La Torre de Control y los KPI deben verse igual: comparar capturas antes y después |
| 2.8 | Eliminar `realrashid/sweet-alert`, `mallardduck/blade-lucide-icons` y `beyondcode/laravel-dump-server`. Cambiar `fzaninotto/faker` por `fakerphp/faker` |
| 2.9 | Si la versión de L8 lo permite, mover `TrustProxies` a `$proxies`/`$headers` explícitos. Si no, se hace en la fase 4 |

**Cómo se comprueba:** se despliega en producción sobre PHP 7.3, y la prueba de humo pasa, con atención a los gráficos y a los reportes de `EasyReport` y Cargill.

#### Avance de la fase 2 (23/10/2026)

| Tarea | Estado | Commit |
|---|---|---|
| 2.1 | **Hecho.** Laravel 8.83.29, Carbon 2.73 y `minimum-stability: stable`. Composer 2.10 bloquea versiones con avisos de seguridad (Laravel 8 los tiene, por estar fuera de soporte): se instaló con `COMPOSER_NO_SECURITY_BLOCKING=1` solo para este paso local. **Con esto la aplicación arranca también en PHP 8.3.** El autoloader optimizado no tiene clases ambiguas. Nota: el `vendor` local no coincidía con `composer.lock` antes de este paso | `26f74656` |
| 2.2 | **Hecho.** 47 `DB::select(DB::raw($sql))` en 23 archivos (no 54: el análisis inicial contó también el `select` del query builder, donde `DB::raw` sigue siendo válido) | `1416dc77` |
| 2.3 | **Hecho.** Sin `env()` fuera de `config/` | `66e3fbc5` |
| 2.4 | **Hecho.** `config/mail.php` en formato `mailers`, con configuración efectiva idéntica | `66e3fbc5` |
| 2.5 | **Hecho.** Las 3 subidas a S3 verifican `storeAs()` | `7b351867` |
| 2.6, 2.8 | **Hecho.** Factory de clase; fuera `legacy-factories`, `sweet-alert`, `blade-lucide-icons` y `laravel-dump-server`; `fakerphp/faker` | `3e000335` |
| 2.7 | **Hecho, con otro enfoque:** en vez de reescribir con Chart.js, las clases de `consoletvs/charts` 6 que se usan pasaron a `App\\Charts\\Base` (licencia MIT) y se eliminó el paquete. El HTML es idéntico: 8 combinaciones de prueba, y los 12 gráficos de la Torre de Control expo y los 2 de KPI comparados contra `main`. Se eliminó `resources/views/vendor/charts` (v5, sin uso). La CI ya compila las 678 vistas sin exclusiones | `b1d3d392` |
| 2.9 | Pasa a la fase 4 (requiere Laravel 9) | — |

### Fase 3 — Ensayo en PHP 8.1 (solo local)

Sirve para separar los problemas de PHP de los de Laravel: mismo código y mismo framework, solo cambia PHP.

| # | Tarea |
|---|---|
| 3.1 | Correr la aplicación con L8.83 sobre PHP 8.1 en local (Herd), con `LOG_DEPRECATIONS_CHANNEL` o un log equivalente activo |
| 3.2 | **Comprobar si L8.83 convierte las deprecaciones de PHP 8.1 en excepción.** Si lo hace, ninguna pantalla con deprecaciones puede llegar a producción. En ese caso, para L9+ se configura `error_reporting` sin `E_DEPRECATED` en producción y se registran las deprecaciones en el log |
| 3.3 | Recorrer toda la lista del 0.4 y las pruebas automáticas. Corregir lo que falle **en el código compatible** (fases 1 y 2) y desplegar esas correcciones sobre PHP 7.3 |
| 3.4 | Revisar el log de deprecaciones y corregir las de la ruta crítica (tarea 1.8) |

**Cómo se comprueba:** toda la lista del 0.4 pasa sobre PHP 8.1 sin errores 500.

#### Resultado de la fase 3 (23/10/2026)

Se ensayó directamente en **PHP 8.3**, la versión del servidor nuevo (Herd no tiene 8.1). Laravel 8.83 arranca en 8.3 desde la tarea 2.1.

- **Prueba de humo (384 rutas) contra la línea base de PHP 7.4:** 0 rutas empeoraron. 4 mejoraron por correcciones anteriores, y `pedido/procesows` sigue igual (excede 120 s). El comparador tenía su propio error de PHP 8 (`"tiempo" < 500`), corregido en `e5e57e85`.
- **Contenido:** la Torre de Control expo (12 gráficos), KPI impo y expo, y la app móvil generan **el mismo HTML que `main` en PHP 7.4**.
- **3.2 resuelto:** Laravel 8.83 registra las deprecaciones en el log, no las convierte en excepción.
- **Deprecaciones registradas:** 804 entradas. 384 eran de la propia prueba de humo; las demás vienen de 31 ubicaciones únicas en `app/` y 10 vistas, todas `null` en funciones nativas (1.8). Se corrigieron con `?? ''` o `?? 0`, con resultado idéntico en 7.4 y 8.3 (`bda41cc6`). Laravel 8 genera además 2 deprecaciones propias (propiedades dinámicas de `Http\\Client\\Response`), que desaparecen con Laravel 9.

### Fase 4 — Laravel 9 + PHP 8.2 (solo local, sección 4.1)

| # | Tarea |
|---|---|
| 4.1 | `composer.json`:<br>• `php ^8.0.2` (se ejecuta en 8.2) y `laravel/framework ^9`;<br>• `league/flysystem-aws-s3-v3 ^3`, `laravel/ui ^3.4`, `nunomaduro/collision ^6` y `spatie/laravel-ignition` (en lugar de `facade/ignition`);<br>• eliminar `fideloper/proxy` y `fruitcake/laravel-cors`;<br>• `tymon/jwt-auth ^2` o su fork (verificar compatibilidad);<br>• `webklex/laravel-imap` en una versión para L9 (verificar API);<br>• `laravolt/avatar`, `blade-heroicons` y `dompdf` en versiones compatibles |
| 4.2 | **Kernel HTTP:**<br>• `TrustProxies` extiende `Illuminate\Http\Middleware\TrustProxies` y usa `HEADER_X_FORWARDED_FOR \| _HOST \| _PORT \| _PROTO \| _AWS_ELB`;<br>• `\Illuminate\Http\Middleware\HandleCors`;<br>• `PreventRequestsDuringMaintenance` |
| 4.3 | `config/filesystems.php`: `'throw' => false` en los discos, y `FILESYSTEM_DISK` |
| 4.4 | **Correo con Symfony Mailer:** probar el envío por `smtp.gmail.com:587`, los adjuntos de `AsercolMailing` y los `->queue()` de Livewire |
| 4.5 | **JWT:**<br>• actualizar `config/jwt.php`, el `JwtMiddleware` y `User` (`JWTSubject`);<br>• probar el login, el refresco del token y las rutas `jwt.verify` y `auth:api`;<br>• confirmar si los tokens emitidos antes del cambio siguen siendo válidos. Si no, coordinar con la app móvil y con los consumidores de la API v1/v2 |
| 4.6 | IMAP de `POController` con la nueva API de webklex. `JMail` sigue usando `imap_open`, así que hay que confirmar que la extensión `imap` está en PHP 8.2 del servidor |
| 4.7 | `config/app.php`: alias `PDF => Barryvdh\DomPDF\Facade\Pdf`. Revisar los providers y aliases contra el esqueleto de L9, conservando `RouteServiceProvider::$namespace` |
| 4.8 | Revisar los nombres de iconos si `blade-heroicons` cambia de versión mayor (28 usos) |
| 4.9 | Pasar la lista del 0.4 completa en local sobre PHP 8.2 |
| 4.10 | **No se despliega.** Producción sigue en el servidor actual con el código de la fase 2. A partir de aquí, `main` no recibe los cambios de la migración hasta el corte: se trabaja en `feature/migracion-laravel10`, y las correcciones urgentes de producción se hacen en `main` y se pasan a la rama de migración con merge |

**Desde esta fase, en la rama de migración, CLAUDE.md §2 cambia:** el código se puede escribir para PHP 8.0 o superior. En `main` sigue rigiendo PHP 7.3 hasta el corte.

**Cómo se comprueba:** la lista del 0.4 y las pruebas automáticas pasan en local.

#### Avance de la fase 4 (23/10/2026)

| Tarea | Resultado |
|---|---|
| 4.1 | Laravel **9.52.22**, Flysystem 3, Symfony Mailer, `tymon/jwt-auth` **2.3.0** (soporta L9 a L13 con el mismo namespace, así que no hace falta el *fork*), `laravel/ui` 4, `spatie/laravel-ignition`, `collision` 6. Se mantienen las versiones mayores de `blade-heroicons` (1.7 soporta L8 a L13, así que **no hay que renombrar íconos**), `webklex/laravel-imap` 2.4 y `laravolt/avatar` 4.1 |
| 4.2 | `TrustProxies` del framework, con los mismos encabezados (valor 30, igual que `HEADER_X_FORWARDED_ALL`); `HandleCors`; `PreventRequestsDuringMaintenance` |
| 4.3 | `throw => false` y `FILESYSTEM_DISK`. S3 lista y lee con Flysystem 3; `ConexionAWS` funciona |
| 4.4 | Symfony Mailer arma el mismo SMTP (`smtp.mandrillapp.com:587`, STARTTLS) y envía por `log` con adjunto. **Falta el envío real** (lista manual) |
| 4.5 | **JWT 2.3 acepta los tokens emitidos por la 1.0**: la app móvil y los consumidores de la API no tienen que volver a iniciar sesión. `jwt.verify` responde igual que en `main` con token válido, inválido o ausente |
| 4.6 | IMAP con webklex 2.4: la conexión falla igual en `main` (PHP 7.4) que en la rama (`connection setup failed`). Es un problema de la cuenta o de la red local, no de la migración. Queda en la lista manual |
| 4.7 | Alias `PDF` a `Facade\\Pdf` (la clase `Facade` ya no existe en dompdf 2); genera PDF |
| 4.8 | No aplica: se mantiene heroicons 1.7 |

**Prueba de humo de la fase 4 (Laravel 9.52 / PHP 8.2), 23/10/2026: 0 rutas empeoraron y 4 mejoraron.** La corrida completa coincidió con caídas intermitentes de conexión a la base de producción: tardó unas 2 horas y 107 rutas cayeron con `QueryException`. Se repitieron una por una. Las 24 que seguían fallando se ejecutaron intercaladas en la rama y en `main` (Laravel 8 / PHP 7.4), dos rondas: **48/48 ok en cada lado**. El origen es infraestructura, no Laravel 9. El registro de deprecaciones bajó de 804 entradas (fase 3) a 3. Se corrigieron las 2 propiedades dinámicas de Livewire (`b205fed8`); la tercera es el token de ShipsGo, que falta en el `.env` local.

**Error propio detectado en esta fase:** la tarea 1.1 dejó `ConexionAWS.php` con sintaxis inválida (barras dobles en el `catch`). La clase no cargaba, y con ella la subida de documentos a S3. La prueba de humo no lo vio porque ninguna ruta GET la carga. Se corrigió en `45e7a67b`, y desde entonces el lint corre sobre **todo** el código después de cada cambio masivo. La CI de GitLab lo habría detectado en el primer push: conviene subir la rama para que la CI corra en cada commit.

**H15.** Seis exports de Cargill (`KPI2IExport`, `KPI4IExport`, `KPI11IExport`, `KPI12IExport`, `KPI12_IExport`, `KPI13IExport`) declaran en el mismo archivo una clase auxiliar con el mismo nombre (`App\\Exports\\Cargill\\reporteCategoria`). Si dos se cargan en la misma petición, falla con *Cannot declare class*. Hoy no ocurre porque `ReporteKpi` instancia uno solo. Se puede corregir renombrando la clase auxiliar de cada archivo.

**H16 (crítico para el corte, corregido 24/10/2026).** El login dependía de un **cambio hecho a mano en `vendor/laravel/framework/src/Illuminate/Auth/EloquentUserProvider.php`**, que está fuera del repositorio. Ese cambio buscaba la credencial `compania` con `whereJsonContains`, porque `users.compania` es un arreglo JSON de grupos (`[1,2,3]`) y el formulario manda el grupo del subdominio.

**Consecuencias:**
- Los `composer install` y `update` de la migración lo borraron, así que en la rama nadie podía iniciar sesión ("usuario o contraseña errados").
- Desplegar con un `vendor` limpio habría dejado sin acceso a todos los usuarios.

**Corrección:**
- Proveedor propio `App\Auth\ProveedorUsuarios`, registrado como driver `ctc` en `AuthServiceProvider` y en `config/auth.php`.
- Mismo comportamiento: el usuario 14 entra con el grupo 2 y no con un grupo que no está en su arreglo.

**Auditoría de `vendor`:** se comparó el `vendor` de `main` contra lo publicado de cada paquete:
- los 100 paquetes en la versión del lock;
- los 43 restantes, contra la versión exacta instalada, descargada de su URL de distribución.

**No hay ningún otro cambio a mano.**

**H17 (crítico para el corte, corregido 24/10/2026).** Cuando un método de controlador tiene varias rutas, `action('Controlador@metodo')` genera una URL distinta según haya o no caché de rutas:
- **Sin caché (como hoy en producción):** usa la última ruta registrada.
- **Con `php artisan route:cache`:** usa la primera.

Afectaba a 6 acciones. Por ejemplo, `POController@search`: hoy da `/searchpo` y con caché daría `/searchpo/{status}`, que exige un parámetro, así que `/rpo` respondía 500.

Se reemplazaron sus 11 usos (fuera de los archivos de respaldo) por la URL que generan hoy (`url('/searchpo')`…). **Con esto, la caché de rutas en el despliegue (Rendimiento R8) no cambia ninguna pantalla.**

### Fase 5 — Laravel 10 sobre PHP 8.3 y corte al servidor nuevo

| # | Tarea |
|---|---|
| 5.1 | `composer.json`:<br>• `php ^8.1` y `laravel/framework ^10`;<br>• `laravel/ui ^4`, `nunomaduro/collision ^7` y `spatie/laravel-ignition ^2`;<br>• `phpunit ^10` (o seguir en 9.6) y `laravel/tinker ^2.8`;<br>• las demás dependencias, en versiones para L10 |
| 5.2 | Volver a verificar que no quede ningún `DB::raw` pasado directamente a `select()` (lo resuelve la fase 2) |
| 5.3 | Revisar `app/Utils/Collection.php` contra las nuevas firmas de `Illuminate\Support\Collection` |
| 5.4 | Opcional: `$routeMiddleware` → `$middlewareAliases` y mover `resources/lang` a `lang/`. No son obligatorios |
| 5.5 | Lint del código y de las vistas compiladas con PHP 8.3 en la CI. Lista del 0.4 completa en local sobre PHP 8.3, revisando las deprecaciones nuevas de 8.3 |
| 5.6 | **Preparar el servidor nuevo** con los pendientes de la sección 6: extensiones, subdominios, acceso a las bases, `.env`, permisos y despliegue. **Cron de `schedule:run` inactivo** |
| 5.7 | **Ensayo en el servidor nuevo antes del corte:** acceder por la IP o por un subdominio de prueba, y pasar la lista del 0.4 con el grupo 2. Las bases son las de producción, así que se aplican las mismas reglas que en local (0.2) |
| 5.8 | **Corte**, en una ventana de baja carga:<br>1. Desactivar el cron en el servidor actual.<br>2. Dirigir los subdominios al servidor nuevo.<br>3. Activar el cron en el servidor nuevo.<br>4. Pasar la lista del 0.4 en producción.<br>5. Hacer merge de la rama de migración a `main`. |
| 5.9 | **Vuelta atrás:** desactivar el cron en el servidor nuevo, dirigir los subdominios al servidor actual y activar allí el cron. El servidor actual no se modifica hasta cerrar el hito 1 |

**Cómo se comprueba:** la lista del 0.4 pasa en producción, el log no muestra errores nuevos durante 48 horas, y los comandos `sync:*` y los reportes programados se ejecutan una sola vez y con normalidad.

Con esta fase se cumplen el **hito 1** y el objetivo de PHP 8.3. Se observan una o dos semanas el log de errores y el de deprecaciones antes de seguir, y luego se retira el servidor actual.

#### Avance de la fase 5, parte de código (23/10/2026)

| Tarea | Resultado |
|---|---|
| 5.1 | Laravel **10.50.3** sobre PHP 8.3. Composer solo cambió 4 paquetes: `laravel/framework`, `laravolt/avatar` 5.1 (la 4.1 no soporta Laravel 10; el único uso, `Avatar::create()->toBase64()`, funciona), `collision` 7 e Ignition 2. PHPUnit sigue en 9.6 |
| 5.2 | No queda ningún `DB::raw` pasado a `select()` (se resolvió en la fase 2) |
| 5.3 | `app/Utils/Collection.php` carga sin error con las firmas de Laravel 10. Cargan las 760 clases de `app/` |
| 5.4 | No se hizo (es opcional) |
| 5.5 | Lint con PHP 8.3: 0 errores. HTTP, `jwt.verify`, PDF, S3 (Flysystem y `ConexionAWS`), Symfony Mailer y avatar funcionan. Los 16 gráficos de la Torre de Control expo y los KPI generan **el mismo HTML que `main`** (Laravel 8 / PHP 7.4). **Prueba de humo completa (384 rutas, PHP 8.3, 23/10/2026):** detectó **3 regresiones reales de Laravel 10** (`rexcargue`, `rexcargueidx`, `rexpedidoenv`, con `Expression given`). Venían de dos `DB::select(Db::raw($sql))` escritos con otra capitalización, que el script de la 2.2 no reconoció. Se corrigieron en `ExCargueController` y `ExPedidoController`, y las tres vuelven a responder 200; la búsqueda sin distinguir mayúsculas ya no encuentra más. Con eso, **0 rutas empeoran** y 4 mejoran |
| 5.6 a 5.9 | Pendientes: requieren el servidor nuevo (sección 6) |

### Fase 6 — Absorbida por la fase 5

El servidor nuevo ya tiene PHP 8.3, así que el paso de PHP 8.2 a 8.3 sobre Laravel 10 ocurre en el corte. Se conserva la numeración para no cambiar las referencias.

### Fase 7 — Livewire 2 → 3 (sobre Laravel 10 / PHP 8.3)

Livewire 3 es requisito de Laravel 11. Se hace sobre L10, que soporta las dos versiones, para no cambiar de framework y de Livewire a la vez.

| # | Tarea |
|---|---|
| 7.1 | `livewire/livewire ^3` y `php artisan livewire:upgrade`, que reescribe parte de lo automático |
| 7.2 | `wire:model` (650 usos): en Livewire 3 es diferido por defecto. Los 340 que hoy no llevan modificador y deben reaccionar en vivo pasan a `wire:model.live`. Los `.defer` se quitan |
| 7.3 | Eventos: `emit`/`emitTo`/`emitUp` (65) pasan a `dispatch`/`dispatch()->to()`. `dispatchBrowserEvent` (88) pasa a `dispatch` y cambian los listeners de JavaScript. `$listeners` (38 archivos) pasa a `#[On]` o se conserva |
| 7.4 | Layouts y scripts: 118 directivas `@livewireScripts`/`@livewireStyles` en 59 vistas. Livewire 3 inyecta Alpine automáticamente: revisar choques con el jQuery, Bootstrap y Alpine que cargan los layouts (`planti`, `tailwind`) |
| 7.5 | `config/livewire.php` al formato de v3 (layout `layouts.planti`). Revisar paginación, subida de archivos (`WithFileUploads`, S3) y `$queryString` |
| 7.6 | Se migra por áreas (Importaciones, Exportaciones, Dashboard, Configuraciones…). Cada área pasa la lista del 0.4 antes de seguir |

**Cómo se comprueba:** la lista del 0.4 pasa en producción, con atención a los formularios, filtros, modales y gráficos de cada área.

#### Avance de la fase 7, parte de código (24/10/2026)

**Livewire 3.8.11** sobre Laravel 10.50.3 / PHP 8.3. Los componentes se quedan en `App\Http\Livewire` (`class_namespace` en `config/livewire.php`), así que no se movió ningún archivo.

| # | Resultado |
|---|---|
| 7.1 | Conversión con un script propio, no con `livewire:upgrade`, para controlar cada regla: 218 archivos. |
| 7.2 | Se conserva el comportamiento de Livewire 2: los 335 `wire:model` sin modificador pasan a `.live`, los 297 `.defer` quedan sin modificador, `.lazy` pasa a `.blur` y `.debounce` a `.live.debounce`. `@entangle` sin `.defer` pasa a `.live`. |
| 7.3 | `emit`/`emitUp` pasan a `dispatch` y `emitTo` a `dispatch()->to()`. Los 89 `dispatchBrowserEvent` pasan a `dispatch` con argumentos con nombre, para que `event.detail` llegue con la misma forma. En JavaScript, `Livewire.emit(…, a, b)` pasa a `Livewire.dispatch(…, [a, b])` y `livewire:load` a `livewire:initialized`. Los 4 `Livewire.hook('message.processed')` pasan a `morphed`. Ningún evento del navegador comparte nombre con un listener de Livewire (en v3 se cruzarían). |
| 7.4 | Se quitó el Alpine por CDN de las 54 vistas que cargan Livewire, porque Livewire 3 trae el suyo. `@livewireScripts` se conserva. |
| 7.5 | `config/livewire.php` en formato v3: layout `layouts.planti` y `legacy_model_binding` activo, porque `FormControlContenedores` enlaza campos de un modelo Eloquent. |
| — | **Paginación:** en v3 la página la lleva `WithPagination` y no existe la propiedad `$page`. 9 componentes la usaban; en 4 el fallo era silencioso (`$this->page ?? 1` siempre daba 1). Se cambió por `getPage()`/`resetPage()` y se quitó `page` de `$queryString`. |
| — | **Una sola raíz:** `imusa-abastecedores`, `kardexhistorico` y `salidacilindros` tenían modales fuera de la raíz. Se envolvieron, y los modales van en `wire:ignore` para que, como en v2, no se vuelvan a pintar. `po.programacion-arribos` tiene el mismo problema, pero ninguna ruta lo usa: `GET visibilidad` está duplicada y gana la del controlador. |

**Prueba de humo (`humo_fase7_LW3.json`):** 0 regresiones. La primera corrida tuvo 45 rutas por tiempo porque el equipo se suspendió durante la prueba; al repetirlas, todas dieron el mismo estado que en la fase 5. Sin deprecaciones nuevas.

**Lo que la prueba de humo no cubre:** solo prueba el primer render en el servidor. Lo que corre en el navegador (consola, `wire:model.live`, modales y SweetAlert, select2, gráficas que se redibujan) se verifica con la lista del 0.4 por área (7.6). No se automatizó con un navegador porque esa revisión no corre dentro de una transacción revertida.

**Ya fallaban en v2 y siguen igual:** 4 listeners apuntan a métodos que no existen (`RegaliasListar::updateCampoDinamico` y `afectarRegalia`, `ImusaAbastecedores::cargarArchivo`, `Salidacilindros::selccionarTodos`).

### Entre las fases 7 y 8 — Layout nuevo de Tailwind

Decidido el 23/10/2026: al cerrar la fase 7 se ejecuta [MigracionLayout.MD](MigracionLayout.MD) (layout nuevo, salida de los iframes y selector de empresa), y después siguen las fases 8 y 9.

### Fase 8 — Laravel 11 (PHP 8.3)

| # | Tarea |
|---|---|
| 8.1 | `laravel/framework ^11`, `php ^8.2`, y las demás dependencias en versiones para L11. Verificar cada una: JWT (fork), IMAP, dompdf, avatar, heroicons, Excel y `laravel/ui ^4.5` |
| 8.2 | Se conserva la estructura de la aplicación (`Http/Kernel`, `Console/Kernel`, providers). La estructura simplificada de L11 es opcional y no se adopta en esta fase |
| 8.3 | Revisar el resto de cambios de L11 que aplican al CTC: limitación de peticiones, `casts()`, `Model::preventLazyLoading` si se usa, y los defaults de configuración que cambiaron |
| 8.4 | Lista del 0.4 completa en pruebas y despliegue con vuelta atrás |

### Fase 9 — Laravel 12 (PHP 8.3)

| # | Tarea |
|---|---|
| 9.1 | `laravel/framework ^12` y las dependencias en versiones para L12 (verificar cada una). **Si alguna dependencia crítica no tiene versión para L12, el plan se detiene en L11 (sección 2)** |
| 9.2 | **Carbon 3:** `diffIn*` devuelve decimales y con signo (36 usos en 10 archivos). Donde hoy se espera un entero positivo, se fija el resultado: `(int) abs(...)` o el parámetro `absolute`. Prueba de caracterización de esos cálculos (días de ANS, tiempos de tránsito, semáforos de `JMetodos`) antes y después |
| 9.3 | Revisar el resto de cambios de L12 que aplican al CTC |
| 9.4 | Lista del 0.4 completa en pruebas y despliegue con vuelta atrás |

### Fase 10 — Estabilización y cierre

| # | Tarea |
|---|---|
| 10.1 | Una o dos semanas observando el log de errores y el de deprecaciones. Corregir la deprecación de null a funciones nativas que quede (tarea 1.8) |
| 10.2 | Retirar el servidor anterior y los releases viejos |
| 10.3 | Actualizar CLAUDE.md (§2 y stack), `ConocimientoControlTower.MD` y `.env.example` |
| 10.4 | Retomar la rama `feature/oc-clasificacion`: hacerle merge de `main`, ajustar el código a las nuevas versiones (sobre todo los componentes Livewire, que pasan a la v3) y quitar la restricción de PHP 7.3 de su especificación. **Se puede retomar antes, al terminar el hito 1,** si el negocio lo prioriza; en ese caso, sus componentes Livewire nuevos entran también en la fase 7 |

---

## 5. Riesgos

| # | Riesgo | Mitigación |
|---|---|---|
| R1 | No hay tests: una regresión puede pasar inadvertida | Fase 0 (lista por módulo, prueba de humo y caracterización), y en cada fase el despliegue con vuelta atrás |
| R2 | No hay bases de prueba: local y el servidor nuevo trabajan sobre las bases de producción | Configuración local segura (0.2), pruebas automáticas de solo lectura con transacción revertida (0.5), y escrituras solo a mano con el grupo 2 |
| R3 | Los importadores Excel por cliente (44) calculan cantidades y valores con texto | Helper con el comportamiento de PHP 7.3 (1.1) y caracterización con archivos reales (0.5) |
| R4 | Cambio de JWT: invalida tokens o cambia respuestas de la API | Pruebas de la API v1/v2 y coordinación con la app móvil (4.5) |
| R5 | Symfony Mailer y Gmail: cambia la negociación TLS | Prueba de envío real desde local, a un buzón propio (4.4) |
| R6 | Los comandos `sync:*` escriben datos: correrlos en pruebas sin aislar daña datos reales | Desactivarlos en pruebas hasta revisar qué escriben (0.2) |
| R7 | Una extensión de PHP 8.2 falta en el servidor (por ejemplo `imap`) | Inventario 0.1 antes de la fase 4 |
| R8 | La rama de OC y Clasificación se desfasa | Se retoma en el 10.4, o al terminar el hito 1. Mientras tanto no se le hacen cambios |
| R9 | PHP 8.2 queda sin soporte el 31/12/2026, a mitad del plan | El hito 1 sale de PHP 7.3 lo antes posible. El corte (fase 5) llega directo a PHP 8.3, sin pasar por 8.2 en producción |
| R11 | Livewire 3 cambia el comportamiento por defecto de `wire:model` y de los eventos en 165 componentes | Migración por áreas, cada una con su lista de prueba (fase 7) |
| R12 | Una dependencia no tiene versión para Laravel 12 | Se detiene en Laravel 11 / PHP 8.3, que tiene soporte (sección 2) |
| R13 | Carbon 3 cambia los cálculos de días y horas (`diffIn*`) | Caracterización antes y después (9.2) |
| R10 | Otro equipo hace cambios en `main` durante la migración | Hacer merge de `main` a la rama de migración al inicio de cada fase. Exigir que lo nuevo sea compatible con PHP 7.3 y 8.2 (reglas de la fase 1) |

---

## 6. Pendientes

**Resueltos 22/10/2026:** servidor de destino con PHP 8.3 y Composer 2.10.3; el código en producción es igual a `main`; no hay bases de prueba (ver 0.2).

**Para arrancar la fase 0 no falta nada:** solo se trabaja en local y en el repositorio.

**Servidor nuevo (24/10/2026):** es un servidor aparte. Usa cPanel con CloudLinux y permite elegir la versión de PHP por proyecto (`alt-php83`, PHP 8.3.36). El código llega **por subida de archivos**, así que `vendor/` se arma en local con PHP 8.3 (`composer install --no-dev`) y se sube junto con el código.

Estado de PHP 8.3 en ese servidor al 24/10/2026. Se configura en cPanel → *Select PHP Version* → 8.3:

| Qué | Estado | Qué hace falta |
|---|---|---|
| Extensiones obligatorias | ❌ Faltan `mbstring`, `dom`, `xmlreader`, `xmlwriter`, `zip`, `gd`, `fileinfo`, `imap`, `sodium` y `soap` | Activarlas. Sin `mbstring`, Laravel no arranca; sin `sodium`, falla el JWT de la API; las demás las usan Excel, PDF, la subida de archivos y la lectura de correo |
| Extensiones recomendadas | ❌ `opcache`, `intl`, `bcmath` | Activarlas |
| `memory_limit` | 128M | 1024M o el valor del servidor actual |
| `upload_max_filesize` / `post_max_size` | 2M / 8M | 20M / 25M o los valores del servidor actual |
| `max_execution_time` (web) | Sin dato | El valor del servidor actual |

El binario `/opt/alt/php83/usr/bin/php` llamado directamente no carga las extensiones del Selector. Por eso la verificación definitiva se hace desde el dominio con PHP 8.3, en el ensayo (5.7).

**Necesarios antes de la fase 5 (corte al servidor nuevo):**

1. **Extensiones de PHP del servidor nuevo:** la salida de `php -m`. Críticas: `imap` (la usan `JMail` y webklex; en 8.3 se instala aparte en muchas distribuciones), `gd`, `zip`, `xml`, `dom`, `mbstring`, `intl`, `pdo_mysql`, `curl`, `openssl`, `fileinfo`, `bcmath` y `exif`. También `memory_limit`, `max_execution_time`, `upload_max_filesize` y `post_max_size`, comparados con los del servidor actual. Los reportes grandes corren dentro de la petición, porque `QUEUE_CONNECTION=sync`.
2. **Subdominios:** el tenant se resuelve por subdominio. El servidor nuevo debe atender todos los subdominios de clientes, con un certificado comodín o uno por subdominio.
3. **Acceso a las bases desde el servidor nuevo:** su IP debe estar autorizada en los servidores MySQL (`asercolgroup.com`, `awsslave.asercol.com`) para las conexiones `mysql`, `websimex`, `websystem`, `cti` y `central`. El 21/10/2026 la base rechazó la IP de desarrollo.
4. **Servidor web y despliegue:** Nginx o Apache, usuario de PHP-FPM, permisos de `storage/` y `bootstrap/cache/`, cómo llega el código (git pull, CI u otro) y dónde vive el `.env`.
5. **Cron de `schedule:run`:** confirmar que en el servidor nuevo queda **inactivo** hasta el corte (sección 4.1).
6. **Consumidores de la API JWT** (app móvil y terceros): saber si dependen de tokens ya emitidos. El cambio de paquete JWT llega con el corte.
7. **Ventanas de despliegue** para el corte (fase 5) y para las fases 7, 8 y 9.
8. **Rotación de credenciales** (0.9): quién rota la contraseña SMTP, las llaves de AWS y la cuenta IMAP, y en qué momento.
9. Si la rama de OC y Clasificación se retoma al terminar el hito 1 o al final (10.4).
