# Plan de blindaje del backend — Priorización y estimación de horas

**Fecha:** 18 de septiembre de 2026 (revisado el mismo día tras verificación de código)
**Basado en:** `INFORME_ANALISIS_NULLPOINTER_BACKEND.md` (29 jul 2026) e `INFORME_ANALISIS_VALIDACIONES_FRONTEND.md`
**Objetivo:** convertir ambos diagnósticos en un plan de actividades ejecutable, con estimación de horas por tarea, para blindar el backend ante crashes por `null` y ante `QueryException` por overflow de longitud (BD en `STRICT_TRANS_TABLES`).
**Alcance de la estimación:** horas de desarrollo + prueba manual del propio autor del cambio. No incluye QA formal de terceros, despliegue ni gestión de release.

> **Nota de verificación (18 sep 2026):** antes de fijar este plan se hizo una pasada de verificación leyendo el código actual (no solo los informes). Resultado: 15 de 16 hallazgos muestreados se confirmaron exactos línea por línea; 1 se reclasificó de P0 a P1 (ver Fase 2, actividad 2.7) por no ser un crash garantizado; y se encontraron **2 puntos nuevos no incluidos en el informe original**, ya incorporados en la Fase 1 (actividades 1.1b y ajuste de 1.2). No se pudo re-verificar contra la BD real los porcentajes de nulos ni las longitudes `varchar(N)` citadas (MySQL local no estaba accesible en el momento de la verificación) — esas cifras se dan por buenas tal como constan en los informes originales, obtenidos por consulta SQL directa.

---

## 0. Resumen ejecutivo

Los dos informes describen el **mismo problema de fondo desde dos ángulos**: el backend confía en que los datos (relaciones Eloquent, payloads externos, inputs de formulario) siempre tienen la forma esperada, cuando la evidencia empírica contra la BD real (`eneon`) demuestra que no es así (hasta 48-84% de nulos en FKs clave, mismatches de longitud de hasta 25× entre `FormRequest` y columna real).

- **Ningún hallazgo es teórico.** Los P0 de ambos informes están confirmados con evidencia de código y/o consultas SQL reales.
- **El patrón más caro de arreglar no es el bug puntual, sino la ausencia de un guard sistemático** (tras `find()`, tras leer un array externo, o entre `FormRequest` y columna de BD). Por eso el plan invierte en dos "arreglos de infraestructura" (Fase 8) que evitan que el problema reaparezza.
- **Estimación total: ~185-190 horas** (~23-24 jornadas de 8h para una persona; ~12-13 jornadas repartidas entre dos). Ver desglose por fase en la sección 2 y totales en la sección 3.

---

## 1. Criterio de priorización

| Prioridad | Significado |
|---|---|
| **P0** | Falla de forma determinista (100% de las veces) o con frecuencia alta y confirmada por datos reales (≥40% de los casos). Rompe un flujo de negocio activo (alta de cliente, generación de anexo, integración Audax/ADX). |
| **P1** | Falla con frecuencia baja-media o requiere una condición adicional (ej. un refactor futuro que quite un `??`). Impacto de negocio real si ocurre. |
| **P2** | Riesgo real pero de baja probabilidad o de impacto acotado (UX, nombre de archivo, log). |
| **P3** | Deuda técnica barata de corregir, sin evidencia de fallo real hasta la fecha. |

---

## 2. Plan de actividades por fase

Las fases están ordenadas por relación riesgo/esfuerzo, no por dependencia estricta — pueden reordenarse o paralelizarse entre 2 desarrolladores (backend / frontend) sin bloqueos importantes, salvo donde se indica.

### Fase 0 — Quick wins (barato, alto valor, sin dependencias)

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 0.1 | `auth()->user()->` → `auth()->user()?->` en `ContratosController`, `IntegracionController`, `Api\DataController`, `UsersController` | NP §3.2, P3 | 2.5h |
| 0.2 | `AudaxEnergyService::$serviciosDisponibles[$servicio]` e `IntegracionTraits` (~L944): validar clave antes de indexar | NP §3.3 | 2h |
| 0.3 | `ProcesarCargaGlobalAudaxJob`: sustituir `$body['data']['error_file_url'] ?? null` por `data_get($body, 'data.error_file_url')` | NP §3.3 | 1h |
| 0.4 | `AdxTarifasService::mapRequestPayload`: corregir orden de acceso `??`/`?:` en `tipo_contrato` | NP §3.3 | 1h |
| 0.5 | Sanitizar `catch (\Throwable $e)` que devuelven `$e->getMessage()` al cliente (`CupsTraits` y patrones similares): loguear detalle, responder mensaje genérico | FV Recom. 6 | 4h |
| 0.6 | `AdxSipsPanel`/`AudaxSipsPanel`: `maxLength={22}` en input CUPS | FV §8 | 0.5h |
| 0.7 | `UpdateCuentaBancariaRequest`: `ObserCuenBan` `max:255`→`max:100` (BD real) | FV §3 | 0.5h |
| | **Subtotal Fase 0** | | **~11.5h** |

### Fase 1 — P0: crashes deterministas en Traits (findLoad, índice de colección)

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 1.1 | `AnexoCambioPotenciaTitularTraits.php` L37-59: mover `if (!$propuesta)` **antes** de `->load()` | NP §1.1 | 3h |
| 1.1b | `AnexoGenerarContratoDual.php` L37-39: mismo patrón exacto (`find()`+`load()` sin guard previo) — **hallazgo nuevo, no incluido en el informe original**, detectado en la verificación de código | Verificación 18 sep | 2h |
| 1.2 | `AnexoProductoTraits.php`: mismo patrón en **6 puntos** — los 4 citados por el informe (~L559-573, 727, 830, 961) más **2 adicionales encontrados en la verificación**: L1205 y L1573 | NP §1.1 + Verificación 18 sep | 7h |
| 1.3 | `IntegracionTraits.php` L511-521: comprobar `->propuestaComercialCliente->isEmpty()` antes de indexar `[0]`, y reforzar accesos relacionados (~L632, 714-716, 737-744) | NP §1.2, §3.1 | 7h |
| 1.4 | `OrderController` (ecommerce legacy): guard tras `find()` antes de `update()` | NP §3.2 | 1.5h |
| 1.5 | Prueba manual de generación de anexos (potencia/titular, producto, contrato dual) e integración Audax/ADX con IDs inválidos y con el caso real de propuesta sin cliente | — | 7h |
| | **Subtotal Fase 1** | | **~27.5h** |

> `ContratosController::okCommercialContract` (antes actividad 1.4) se retiró de esta fase: la verificación de código mostró que entre el `find()` y el guard `if (!$propuesta)` solo hay un acceso de *propiedad* protegido con `??`, no una llamada a método — en PHP 8 eso es un warning, no un fatal. Se reclasificó como P1 y pasa a la Fase 2 (actividad 2.7).

### Fase 2 — P1: cadenas sin null-safe + guard centralizado en Controllers

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 2.1 | Reemplazar cadenas `$a->b->c->d` por `?->` en cada eslabón: `AnexoProductoTraits` (~L1352, 1675, 1693) y `AnexoCambioPotenciaTitularTraits` (~L451, 708-727) | NP §3.1 | 6h |
| 2.2 | Diseñar y crear un guard único reutilizable para el patrón `handleTraitRedirect`/`processTraitResponse` (`if (!$data || !($data->success ?? false)) {...}`) | NP §3.2, P1 | 4h |
| 2.3 | Aplicar el guard en los 8 controllers afectados: `CupsController`, `ClientesController`, `ComercializadoraController`, `ProductoController`, `AnexoProductoController`, `UploadFileController`, `PuntoSuministroController`, `ContactosController` | NP §3.2 | 6h |
| 2.4 | `CargaGlobalAudax.php`: no tragar la excepción de `create()` de `PuntoSuministro` (~L755-814), validar `first()` de tarifa (~L813-856), asegurar resolución de CP/tipo de vía (~L958-967) | NP §3.1 | 6h |
| 2.5 | `ContratoTraits.php`: `??` en claves de payload (`$cup['tarifa']`, `$cup['consumo_kw']`); `Auth::user()->id` → `auth()->id()` | NP §3.1 | 2h |
| 2.6 | `PdfFormFillable.php`: `isset()` en configuración de plantillas (~L818-823, 704-712) | NP §3.1 | 2h |
| 2.7 | `ContratosController::okCommercialContract` (~L394-400): guard tras `find()` — reclasificado de P0 a P1 tras verificación (ver nota en Fase 1) | NP §3.2, reclasificado 18 sep | 2h |
| | **Subtotal Fase 2** | | **~28h** |

### Fase 3 — Deuda de modelos y relaciones Eloquent

> Fase de mayor riesgo de regresión: cambiar el tipo de una relación (`hasOne`→`belongsTo`) o su FK puede alterar el resultado de queries existentes en producción. Requiere revisión cuidadosa de todos los usos antes de tocar el modelo.

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 3.1 | Corregir `PropuestaComercialCliente::contacto()` (usa `CodCli` contra `CodConCli`, dominios de ID distintos) | NP §3.4 | 3h |
| 3.2 | Renombrar/redefinir `PuntoSuministro::provincia()` (en realidad apunta a `Localidad`) | NP §3.4 | 2h |
| 3.3 | Cambiar `PropuestaComercial::comercializadora()` de `hasOne` a `belongsTo` + auditar todos los usos existentes | NP §3.4 | 3.5h |
| 3.4 | Añadir `?->` sistemático donde se usa `PropuestaComercialCups->comercializadora/producto/anexoProducto` (recordar: ~48% son `null`) | NP §2 | 6h |
| 3.5 | Revisar casts booleanos que enmascaran `null` como `false` (`EstRen`, `RenMod`, `servicioBasico`, `servicioPremium`) — documentar o migrar a `nullable boolean` explícito | NP §3.4 | 3h |
| 3.6 | Prueba de regresión de PDFs/anexos y listados que dependan de estas relaciones | — | 5h |
| | **Subtotal Fase 3** | | **~22.5h** |

### Fase 4 — Validaciones: Clientes, Contactos, Cuentas Bancarias

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 4.1 | `UpdateClienteRequest`: ajustar `max:` a la columna real (`RazSocCli`, `NomComCli`, `TelFijCli`, `TelMovCli`, `EmaCliOpc`, `WebCli`, `ObsCli` [añadir `max` inexistente], `NomViaDomFis`, `BloDomFis`, `EscDomFis`, `PlaDomFis`, `DireccionBBDD`) | FV §1.1 | 3h |
| 4.2 | `EditarCliente.tsx` + `EditarClienteModal.tsx`: espejar `maxLength` en cada campo; corregir `isFormValid` fijo en `true` | FV §1.1-1.2 | 4h |
| 4.3 | `StoreContactoRequest`/`UpdateContactoRequest`: ajustar `max:` (`NomConCli`, `CarConCli`, `TelFijConCli`, `TelCelConCli`, `NIFConCli`, `nrcomercialacreditado(adx)`) | FV §2.1 | 3h |
| 4.4 | `CrearContacto.tsx`/`EditarContacto.tsx` + modal: `maxLength` espejo; unificar el `iban` de contacto con el patrón ya validado de `T_CuentaBancaria` (mismo endpoint `api.data.iban.validate`, tope real 26) | FV §2.1, §3, Recom. 5 | 4h |
| 4.5 | Prueba manual alta/edición de Clientes y Contactos (valores límite y por encima del límite) | — | 3h |
| | **Subtotal Fase 4** | | **~17h** |

### Fase 5 — Puntos de Suministro y CUPS (incluye el caso más crítico del informe frontend)

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 5.1 | `SavePuntoSuministroRequest`: añadir `max:` donde hoy no existe ninguno (`NomViaPunSum`, Bloque/Escalera/Planta/Puerta, `RefCasPunSum`, `ObsPunSum`) y corregir `CUPsEle`/`CupsGas` a 24/20 (hoy `max:50`) | FV §4.1 | 4h |
| 5.2 | `EditarPunto.tsx` + 2 modales del Dashboard: `maxLength` espejo en todos los campos listados | FV §4.1 | 4h |
| 5.3 | Crear `StoreCupsElectricoRequest`/`StoreCupsGasRequest` (o unificado): `CUPsEle required|max:24|regex`, `CodPunSum required|exists:T_PuntoSuministro,CodPunSum` | FV §4.2, Recom. 3 | 6h |
| 5.4 | Aplicar las nuevas reglas en `CupsController`/`CupsTraits::store/updateCupsElectricoTrait/GasTrait` (hoy sin ninguna validación) | FV §4.2 | 3h |
| 5.5 | `EditarCups.tsx`: añadir `required` + `maxLength` + patrón CUPS en el frontend | FV §4.2 | 3h |
| 5.6 | Prueba manual: alta/edición de puntos de suministro y CUPS, incluyendo `CodPunSum` inexistente y CUPS mal formado | — | 4h |
| | **Subtotal Fase 5** | | **~24h** |

### Fase 6 — Contratos (alta y edición)

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 6.1 | Añadir `max:`/tipo a `documento_fiscal`, `razon_social`, `comentarios`/`comentarioGas`, `corpoGo` (validar numérico), campos de tarifario, en los 3 `FormRequest` de alta | FV §5 | 4h |
| 6.2 | Añadir `after:fechaInicio` en `fechaFin` para los flujos multipunto/multicliente (hoy solo cubierto en unipunto) | FV §5 | 1h |
| 6.3 | Frontend: `maxLength`/`pattern` en los 3 flujos (`UniClienteUniPunto`, `UniClienteMultiPunto`, `MultiClienteMultiPunto`) — ninguno usa hoy validación HTML | FV §5 | 6h |
| 6.4 | `updatePropuestaComercialCups` (`validate()` inline): añadir `max:` a `Id_Tarifa_Automatica`, `nombretarifa`, `productCode`, `version` | FV §6.1 | 1h |
| 6.5 | `UploadDocumentoModal` / prompts `comercial`/`canal`: `maxLength` preventivo | FV §6.2-6.3 | 1h |
| 6.6 | Prueba manual de los 3 flujos de alta de contrato + edición inline de CUPS | — | 5h |
| | **Subtotal Fase 6** | | **~18h** |

### Fase 7 — Comerciales (Comercializadora/Producto/AnexoProducto) y Cargas Masivas

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 7.1 | `UpdateComercializadoraRequest`: ajustar `max:` a columna real en prácticamente todos los campos (caso extremo: `EscDirCom`/`PlaDirCom`, 50 vs 2) | FV §7.1 | 3h |
| 7.2 | `EditarComercializadora.tsx`: `maxLength` espejo | FV §7.1 | 2h |
| 7.3 | `StoreProductoRequest`/`UpdateProductoRequest`: corregir `CodigoServicio` (BD `varchar(2)`, hoy sin tope real en `update`) y `ObsPro` (1000 vs 200) | FV §7.2 | 1.5h |
| 7.4 | `StoreAnexoProductoRequest`/`UpdateAnexoProductoRequest`: ajustar `ObsAnePro` (1000 vs 200) y `DocAnePro` en `update` (500 vs 255) | FV §7.3 | 1.5h |
| 7.5 | `EditarProducto.tsx`/`EditarAnexoProducto.tsx`: `maxLength` espejo | FV §7.2-7.3 | 2h |
| 7.6 | Cargas masivas (`CargaGlobalAudax.php`): validar longitud/tipo por fila **antes** del insert (CIF, Bloque/Escalera/Planta/Puerta son las columnas más cortas y con mayor probabilidad de overflow real), devolver reporte de filas rechazadas en vez de abortar en silencio | FV §11, Recom. 4 | 8h |
| 7.7 | Prueba manual: alta/edición Comerciales + carga masiva con Excel de prueba (incluir fila con overflow deliberado) | — | 5h |
| | **Subtotal Fase 7** | | **~23h** |

### Fase 8 — Infraestructura preventiva y cierre

| # | Actividad | Origen | Horas |
|---|---|---|---|
| 8.1 | Comando Artisan que compare `max:` de cada `FormRequest` contra `information_schema.columns` y reporte discrepancias — evita que el problema reaparezca con nuevos formularios | FV Recom. 1 | 7h |
| 8.2 | Usuarios/Perfil: `max:72` en `password` (límite práctico de bcrypt), comparar `password_confirmation` en cliente antes de enviar, validar `CambiarPassword` | FV §12 | 3h |
| 8.3 | Regresión general end-to-end de todos los módulos tocados (smoke test dirigido, no QA formal) | — | 8h |
| 8.4 | Actualizar ambos informes originales marcando qué hallazgos quedaron resueltos | — | 2h |
| | **Subtotal Fase 8** | | **~20h** |

---

## 3. Totales y estimación de calendario

| Fase | Horas |
|---|---|
| 0 — Quick wins | 11.5h |
| 1 — P0 crashes en Traits | 27.5h |
| 2 — P1 cadenas + guard Controllers | 28h |
| 3 — Modelos/relaciones | 22.5h |
| 4 — Clientes/Contactos/Cuentas | 17h |
| 5 — Puntos de Suministro/CUPS | 24h |
| 6 — Contratos | 18h |
| 7 — Comerciales/Cargas Masivas | 23h |
| 8 — Infraestructura preventiva | 20h |
| **Total** | **~191.5h** |

**Con un margen del 15% para imprevistos** (payloads reales distintos a lo documentado, efectos colaterales de las relaciones tocadas en Fase 3): **~200-220 horas**.

**Calendario orientativo:**
- 1 desarrollador full-time (8h/día): **~24-28 jornadas** (~5 semanas).
- 2 desarrolladores en paralelo (backend puro en Fases 1-3 y 8; validaciones/frontend en Fases 4-7): **~13-15 jornadas** (~2.5-3 semanas), ya que las fases de validación de formularios son independientes de las fases de null-safety de Traits/Modelos.

---

## 4. Orden recomendado de ejecución

1. **Fase 0** primero siempre — coste mínimo, cero riesgo de regresión, valor inmediato.
2. **Fase 1** antes que cualquier otra cosa de negocio crítico — son los únicos hallazgos que fallan el 100% de las veces bajo condiciones ya identificadas.
3. **Fase 5.3-5.4** (CUPS sin ninguna validación) tiene prioridad alta pese a estar en una fase "de validaciones": es la única ruta de escritura del sistema sin ninguna regla, ni siquiera de obligatoriedad.
4. **Fase 3** (relaciones Eloquent) se recomienda dejarla para cuando haya ventana de pruebas más amplia, por su mayor superficie de regresión — no es urgente por sí sola (no genera crashes nuevos), pero es la causa raíz que sostiene buena parte del riesgo P1 de las Fases 1-2.
5. **Fase 8.1** (comando Artisan de verificación) conviene adelantarla si el equipo va a seguir creando formularios en paralelo a este plan, para no seguir acumulando deuda mientras se ejecuta el resto.

---

## 5. Supuestos de la estimación

- Las horas asumen que el desarrollador que ejecuta el cambio conoce ya el módulo (no incluye tiempo de onboarding al código).
- No incluye tiempo de code review de terceros ni de despliegue/CI.
- Las pruebas indicadas son manuales dirigidas (casos límite conocidos), no suites automatizadas nuevas — si se desea cobertura con tests automatizados (PHPUnit/Pest) para los casos P0, añadir ~1.5× el tiempo de prueba manual indicado en cada fase.
- Fase 3 (modelos) es la de mayor incertidumbre real: el esfuerzo de "auditar todos los usos existentes" antes de tocar una relación puede variar según cuántos sitios del código dependan de su comportamiento actual (aunque sea incorrecto).
