# Informe: Riesgos de "null pointer" en el backend de desarrolloeneon

**Fecha:** 29 de julio de 2026
**Alcance:** Análisis estático de todo el backend (`app/Http/Controllers`, `app/Services`, `app/Traits`, `app/Models`, `app/Jobs`, `app/DTOs`/`app/DataTransferObjects`) + verificación empírica contra la base de datos local `eneon` (MySQL).
**Tipo de análisis:** Solo lectura. No se ejecutó ninguna instrucción de escritura/borrado en la base de datos ni se modificó código de producción como parte de este informe.

> **Estado (21 sep 2026):** los hallazgos de este informe fueron implementados por la feature
> `specs/013-blindaje-backend-integridad-datos`. El detalle de qué se corrigió, con qué diferencias
> respecto a lo aquí descrito (líneas desplazadas, hallazgos adicionales encontrados durante la
> implementación, decisiones de alcance) está en `specs/013-blindaje-backend-integridad-datos/tasks.md`
> — cada tarea documenta su resolución. Pendiente de esa feature: T031/T038/T045/T051/T059/T065
> (pruebas manuales, requieren entorno con datos reales) y T067 (ejecutar `php artisan
> validate:column-lengths` contra una BD real, no accesible durante la implementación).

---

## Metodología

1. Se conectó a la base de datos local configurada en `.env` (`DB_CONNECTION=mysql`, `DB_DATABASE=eneon`) usando el bootstrap de Laravel (sin exponer credenciales en texto plano), ejecutando únicamente consultas `SELECT` sobre `information_schema` y las tablas de negocio.
2. Se lanzaron 4 exploraciones especializadas en paralelo sobre: Traits, Controllers, Services/Jobs/DTOs y Models, buscando patrones de riesgo de tipo:
   - Accesos encadenados `$modelo->relacion->campo` sin operador null-safe (`?->`) ni verificación previa.
   - Resultados de `first()`/`find()` usados inmediatamente con `->` sin comprobar `null` (a diferencia de `firstOrFail`/`findOrFail`).
   - Acceso a claves de array (`$data['clave']`) sin `isset()`/`??`, especialmente en payloads externos (ADX, Audax, Salesforce, n8n).
   - Relaciones Eloquent (`belongsTo`, `hasOne`) definidas sobre columnas FK que resultan ser nullable en la base de datos real.
3. Se cruzaron los hallazgos de código con estadísticas reales de nulos en las columnas FK involucradas, para distinguir riesgo teórico de riesgo real con evidencia cuantitativa.
4. Se verificaron manualmente en código los 2-3 hallazgos más críticos antes de incluirlos como "confirmados".

---

## 1. Bugs confirmados (P0) — fallan siempre que se dan las condiciones, no dependen de "mala suerte" con los datos

### 1.1 `AnexoCambioPotenciaTitularTraits.php` — `load()` sobre `null` antes de comprobar existencia

```php
// app/Traits/auxiliares/AnexoCambioPotenciaTitularTraits.php:37-59
$propuesta = PropuestaComercial::find($propuestaId);
//cargar todas las relaciones necesarias
$propuesta->load([
    'propuestaComercialCliente',
    'comercializadora',
    'propuestaComercialCliente.contacto',
    'propuestaComercialCliente.cliente.localidad',
    'propuestaComercialCliente.propuestaComercialCups' => function ($query) {
        $query->orderBy('CodCom')->orderBy('CodPro')->orderBy('CodAnePro');
    },
    // ...
]);
if (!$propuesta) {
    // Este chequeo llega DEMASIADO TARDE: el crash ya ocurrió en el ->load() anterior
    // ...
}
```

**Causa:** `find()` devuelve `null` si el ID no existe (propuesta borrada, typo en la URL, doble clic con ID cacheado en el front, carrera entre procesos). La llamada a `->load()` en la línea siguiente revienta con *"Call to a member function load() on null"* **dos líneas antes** de que el `if (!$propuesta)` pueda protegerlo. El chequeo existe en el código, pero es inalcanzable: nunca se ejecuta porque la excepción fatal ocurre antes.

**Cuándo ocurre:** cualquier generación de anexo de cambio de potencia/titular con un `propuestaId` inválido.

**Mismo patrón repetido en:** `app/Traits/AnexoProductoTraits.php` (aprox. líneas 559-573, 727, 830, 963) — mismo `find()` + `load()` inmediato sin guard intermedio.

### 1.2 `IntegracionTraits.php` — acceso a índice `[0]` de colección potencialmente vacía

```php
// app/Traits/IntegracionTraits.php:511-521
$propuestaCliente = $propuesta->propuestaComercialCliente[0];
$contacto = null;

if ($propuesta->TipProCom == 1 || $propuesta->TipProCom == 2) {
    $contacto = $propuestaCliente?->cliente()->first();
} else {
    $propuestaCliente = $propuesta->propuestaComercialCliente[0];
    $contacto = Contacto::where('CodConCli', $propuestaCliente->CodCli)->first();
}
```

**Verificado con datos reales:** de **14 286 propuestas comerciales** (`T_PropuestaComercial`), **1 no tiene ningún cliente asociado** en `T_Propuesta_Comercial_Clientes`. Es un caso raro (0,007%) pero real — y el flujo de tramitación de contratos (integración Audax/ADX) es precisamente el de mayor impacto de negocio si falla a mitad de proceso.

---

## 2. Causa raíz transversal (la que más volumen de riesgo genera)

**El `??` protege el valor final de una cadena de acceso, no los eslabones intermedios.** Este patrón se repite decenas de veces en el código de integración/generación de PDF:

```php
$cups->comercializadora->Servicio_Integracion ?? $default    // seguro solo si "comercializadora" ya existe
$cups->puntoSuministro->localidad->provincia->DesPro ?? ''    // 3 eslabones, un único "??" al final
```

Cuando el eslabón intermedio (`comercializadora`, `puntoSuministro`, `localidad`) es `null` —no el atributo final—, PHP 8 suele emitir solo un *warning* silencioso en la mayoría de estos casos concretos (porque el `??` efectivamente engloba toda la expresión de lectura de propiedad). Sin embargo, este patrón es **frágil**: se convierte en error fatal en cuanto:
- alguien reemplaza un `->propiedad` intermedio por una llamada `->metodo()` (fatal inmediato, no warning), o
- se "simplifica" el código quitando el `??` en un refactor futuro (ya ha ocurrido, ver hallazgo 1.1).

### Evidencia cuantitativa — frecuencia real de FKs nulas

**`T_Propuesta_Comercial_CUPs` (21 555 filas):**

| Columna (FK) | Relación en el modelo | % nulos | Impacto |
|---|---|---|---|
| `CodCom` | `comercializadora()` | **47,95%** | `Servicio_Integracion`, `RazSocCom` usados en casi todo el flujo de anexos/PDF/integración |
| `CodPro` | `producto()` | **47,09%** | determina plantilla PDF y tipo de oferta |
| `CodAnePro` | `anexoProducto()` | **49,12%** | `DesAnePro` usado en subtarifa/nombre de anexo |
| `CodTar` | tarifa eléctrica/gas | 0,06% | bajo, condicionado por tipo (luz/gas) |
| `CodPunSum` | `puntoSuministro()` | 0,06% | bajo |
| `CodCup` | `cupsElectrico()`/`cupsGas()` | ~0% | bajo |

**Conclusión clave: casi la mitad de las líneas de propuesta comercial no tienen comercializadora, producto o anexo asignado.** Cualquier código que asuma que esas relaciones siempre existen tiene ~48% de probabilidad de toparse con `null` en producción.

**Otras tablas verificadas:**

| Tabla.Columna | % nulos | Relación afectada |
|---|---|---|
| `T_Contrato.CodProComCup` | **83,66%** | `Contrato::propuestaComercialCups()` — la mayoría de contratos no enlaza a una línea CUPS específica |
| `T_CuentaBancaria.CodBan` | **43,38%** | `CuentaBancaria::banco()` |
| `T_CUPsGas.CodTarGas` | 16,36% | `CUPsGas::tarifaGas()` |
| `T_CUPsElectrico.CodTarElec` | 3,08% | `CUPsElectrico::tarifaElectrica()` |
| `T_Cliente.CodLocFis` | 0,29% | `Cliente::localidad()` |
| `T_PuntoSuministro.CodLoc` | 0% | bajo riesgo real |
| `T_Propuesta_Comercial_Clientes.CodCli` | 0% | bajo riesgo real |

---

## 3. Hallazgos por capa (detalle completo)

### 3.1 Traits (mayor densidad de riesgo — lógica de negocio e integraciones externas)

- **P0:** `AnexoCambioPotenciaTitularTraits.php` (`find()`+`load()`, línea 37-39), repetido en `AnexoProductoTraits.php`.
- **P1:** cadenas `$cliente->localidad->provincia->DesPro`, `$cup->puntoSuministro->cliente->NomComCli` sin `?->` en `AnexoProductoTraits.php` (~L1352, L1675, L1693) y en `AnexoCambioPotenciaTitularTraits.php` (~L451, L708-727).
- **P1:** `IntegracionTraits.php` — `$cliente = $punto?->cliente` está protegido con null-safe, pero se usa sin `?->` unas líneas después (~L632, L714-716, L737-744): si el CUPS no tiene punto de suministro o cliente, revienta igual.
- **P2:** `CargaGlobalAudax.php` — un `catch` traga la excepción de `create()` de `PuntoSuministro` y el código sigue usando `$direccionCups` como si existiera (~L755-814); `first()` de tarifa sin comprobar null (~L813-856); resolución de código postal/tipo de vía de contacto sin garantizar asignación (~L958-967).
- **P2:** `ContratoTraits.php` — claves de payload del frontend (`$cup['tarifa']`, `$cup['consumo_kw']`) sin `??`; `Auth::user()->id` sin `?->` (~L279-280, preferible `auth()->id()`).
- **P2:** `PdfFormFillable.php` — acceso a configuración de plantillas sin `isset` para ciertos tipos de anexo (~L818-823, ~L704-712).

### 3.2 Controllers

- **Patrón dominante:** el helper `handleTraitRedirect`/`processTraitResponse` hace `json_decode()` y luego usa `$data->success` / `$data->data->CodX` sin comprobar `null` primero. Se repite en `CupsController`, `ClientesController`, `ComercializadoraController`, `ProductoController`, `AnexoProductoController`, `UploadFileController`, `PuntoSuministroController` (activar/suspender), `ContactosController`.
- `ContratosController::okCommercialContract` — `PropuestaComercial::find($propuestaId)` usado antes de validar correctamente (~L394-400).
- `auth()->user()->name` / `->tenant_id` sin `?->` en `ContratosController` (~L448), `IntegracionController` (~L57, L112), `Api\DataController` (~L494), `UsersController` (~L53) — bajo riesgo si la ruta va detrás de middleware `auth`, pero frágil si el mismo código se reutiliza desde consola/cola.
- `OrderController` (módulo ecommerce legacy) — `Orders::find($request->id)->update(...)` sin comprobar null (mitigado por `catch` genérico, pero es un NPE real).
- `TarifaController` y `CupsHistorialController` están razonablemente bien protegidos (`findOrFail`, `?->`) — sirven de referencia de buen patrón.

### 3.3 Services / Jobs / DTOs

- **Buena noticia:** los servicios nuevos (`AdxSipsService`, `AdxTarifasService`, `AdxContratarService`, `N8nChatService`, `AdxContratarPayloadBuilder`, DTOs `AdxTarifaDto`/`SendChatMessageDto`/`AdxContratarRequest`) están bien defendidos: validan `is_array($body)`, usan `?->` y `??` correctamente anidados, comprueban `successful()` antes de leer el cuerpo.
- El riesgo real está en el código **legado** que estos servicios nuevos todavía no han sustituido: `IntegracionTraits.php` (ver sección 3.1) y `AudaxEnergyService.php` — acceso a `$this->serviciosDisponibles[$servicio]['url']` sin validar que `$servicio` sea una clave conocida (~L41-42); mismo patrón en `IntegracionTraits.php` ~L944 (`['contratar']`).
- `ProcesarCargaGlobalAudaxJob.php` — `$body['data']['error_file_url'] ?? null` no protege si `data` existe pero es explícitamente `null` (el `??` no evita el *offset on null* intermedio); se recomienda `data_get($body, 'data.error_file_url')`.
- `AdxTarifasService::mapRequestPayload` — acceso a `$requestData['tipo_contrato']` sin `??` antes del operador `?:` (~L137-140).
- `GenerarContratoIntegracionJob` y `OkCommercialContractJob` manejan bien los casos de `find()`/`first()` con comprobación inmediata — el riesgo real que arrastran está en el trait que invocan (`IntegracionTraits`), no en el Job en sí.

### 3.4 Models — mapa de relaciones opcionales sin `?->`

- **Máximo riesgo:** `PropuestaComercialCups` — relaciones `producto()`, `anexoProducto()`, `comercializadora()`, y el par condicional `cupsElectrico()`/`cupsGas()` + `tarifaElectrica()`/`tarifaGas()` según el valor de `TipCups` (una de las dos siempre será `null` según el tipo de suministro).
- **Relación mal definida:** `PropuestaComercialCliente::contacto()` usa `CodCli` como *foreign key* apuntando a `CodConCli` de `Contacto` — son dominios de identificador distintos, por lo que casi siempre resuelve a `null` o a una fila incorrecta.
- **Nombre engañoso:** `PuntoSuministro::provincia()` en realidad está definida como `belongsTo(Localidad::class, 'CodLoc')`, no apunta a un modelo `Provincia`.
- **Definición inconsistente:** `PropuestaComercial::comercializadora()` está declarada como `hasOne` cuando semánticamente (FK en el propio modelo) debería ser `belongsTo`.
- **Otras relaciones opcionales de riesgo alto:** `AnexoProducto::tipoComision()`, `CuentaBancaria::banco()` (43% nulos confirmado), `Contrato::propuestaComercialCups()` (84% nulos confirmado), `ProcessHistory::usuario()`, `LeadFicha::caes()`.
- **Casts que enmascaran `null`:** flags booleanos (`EstRen`, `RenMod`, `servicioBasico`, `servicioPremium`, etc.) convierten `null` → `false` de forma transparente, mezclando conceptualmente "sin dato" con "explícitamente desactivado". No es un NPE, pero es una fuente de bugs de lógica de negocio silenciosos.

---

## 4. Priorización de arreglos (para cuando se decida actuar — no se ha tocado nada)

| Prioridad | Qué | Por qué primero |
|---|---|---|
| **P0** | `find()`/`first()` seguido de `->método()` o `->load()` sin `if (!$x) return/throw` inmediatamente después. Empezar por `AnexoCambioPotenciaTitularTraits.php` L37-39 y sus réplicas en `AnexoProductoTraits.php` | Falla el 100% de las veces con ID inválido, no depende de la distribución de datos |
| **P1** | Cadenas `$a->b->c->d` en Traits de generación de anexos/PDF (comercializadora, producto, anexoProducto, localidad→provincia): usar `?->` en cada eslabón, no solo `??` al final | ~48% de las líneas CUPS no tienen comercializadora/producto/anexo asignado (dato confirmado) |
| **P1** | Helper `handleTraitRedirect` en Controllers: centralizar un guard `if (!$data \|\| !($data->success ?? false))` antes de leer `$data->data->CodX` | Afecta alta/edición en 6+ módulos (Cups, Clientes, Comercializadora, Producto, Anexo, Upload) |
| **P2** | `IntegracionTraits.php` L511/L520: comprobar `->propuestaComercialCliente->isEmpty()` antes de indexar `[0]` | Raro (1 de 14 286 propuestas) pero de alto impacto: rompe la integración de contratos a mitad de proceso |
| **P2** | Corregir la relación `PropuestaComercialCliente::contacto()` (FK incorrecta) y renombrar/redefinir `PuntoSuministro::provincia()` | Deuda técnica que genera `null` silencioso o datos incorrectos, no solo crashes |
| **P3** | `auth()->user()->` → `auth()->user()?->` en los puntos señalados en Controllers | Bajo riesgo real (rutas ya protegidas por middleware `auth`), pero barato de corregir |

---

## 5. Lo que ya está bien (para no perder de vista al priorizar)

`N8nChatService`, los servicios `Adx*` de nueva generación (`AdxSipsService`, `AdxTarifasService`, `AdxContratarService`, `AdxActualizarEstadoService`, `AdxInsertDocumentacionService`), `AdxContratarPayloadBuilder`, `OkCommercialContractJob`, `GenerarContratoIntegracionJob`, `CupsTraits` y `ComercializadoraTraits` ya siguen el patrón correcto: `find()`/`first()` seguido de comprobación inmediata antes de usar el resultado, y validación de `is_array()`/`successful()` antes de leer respuestas HTTP externas.

**El código nuevo está claramente mejor blindado que el legado.** El legado (`IntegracionTraits.php`, `CargaGlobalAudax.php`, `AnexoProductoTraits.php`, `AnexoCambioPotenciaTitularTraits.php`, los helpers `handleTraitRedirect` de varios Controllers) es donde se concentra la mayor parte del riesgo real identificado en este informe.

---

## Anexo: consultas de verificación ejecutadas

Todas las consultas fueron `SELECT` de solo lectura contra la base de datos local `eneon`, ejecutadas a través de scripts PHP temporales que bootstrapean Laravel (para no exponer credenciales en texto plano) y eliminados inmediatamente después de su uso. No se realizó ninguna modificación de esquema ni de datos.
