# Informe de análisis de validaciones — Frontend (`resources/js/Pages/Eneon`)

**Alcance:** formularios con persistencia (alta/edición) bajo `resources/js/Pages/Eneon`. 
**Método:** para cada campo se compara: (1) validación en el navegador (`required`, `maxLength`, `pattern`, JS previo al envío), (2) reglas del `FormRequest`/`validate()` del backend, y (3) el esquema real de la tabla en MySQL (`information_schema.columns`: `IS_NULLABLE`, `DATA_TYPE`, `CHARACTER_MAXIMUM_LENGTH`), obtenido con consultas `SELECT` de solo lectura contra la BD local `eneon`. No se ejecutó ninguna escritura ni borrado.

> **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 las diferencias
> encontradas durante la implementación respecto a lo aquí descrito, está en
> `specs/013-blindaje-backend-integridad-datos/tasks.md`. Pendiente de esa feature: las pruebas
> manuales (T031/T038/T045/T051/T059/T065) y ejecutar `php artisan validate:column-lengths`
> (T067) contra una BD real — no accesible durante la implementación.

## 0. Hallazgo transversal crítico — por qué esto importa

```sql
SELECT @@sql_mode;
-- ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
```

MySQL corre en **`STRICT_TRANS_TABLES`** (y `config/database.php` tiene `'strict' => true`). Esto significa que si un valor excede la longitud de una columna `VARCHAR`, **no se trunca silenciosamente**: MySQL lanza `1406 Data too long for column '...'`, Laravel lo convierte en `QueryException` y el usuario recibe un **Error 500** (o un JSON con `success:false` si el `catch` genérico lo atrapa, exponiendo el mensaje de la excepción).

Por tanto, cada fila de las tablas siguientes donde "Frontend" y "Backend" permiten **más caracteres** que la columna real de BD es un **bug reproducible**, no una advertencia teórica.

Patrón repetido en casi todos los formularios revisados:

- Ningún `Form.Control`/`<input>` usa el atributo HTML `maxLength` salvo excepciones puntuales (`BloDomFis`, `TelFijConCli`, IBAN de cuenta bancaria, CUPS en `EditarPunto`).
- La "obligatoriedad" casi siempre es JS (listas de campos obligatorios antes del `post`/`put`), nunca `required` HTML.
- Los `FormRequest` de Laravel casi siempre usan `max:255` "por defecto" en vez de reflejar la longitud real de la columna migrada.

---

## 1. Módulo Clientes

### 1.1 `EditarCliente.tsx` (alta y edición) — `resources/js/Pages/Eneon/Clientes/EditarCliente.tsx`

Endpoint: `POST clientes.store` / `PUT clientes.update` → `UpdateClienteRequest` → tabla `T_Cliente`.

| Atributo (frontend) | Validación frontend | Regla backend (`UpdateClienteRequest`) | Campo BD (`T_Cliente`) | Riesgo |
|---|---|---|---|---|
| `RazSocCli` | Sin validación | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — backend permite 5× más que la columna → 500 si el usuario escribe >50 car. |
| `NomComCli` | Sin control en UI (va en el payload) | `sometimes,nullable,max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50, y además la columna es NOT NULL pero la regla es `nullable` |
| `NumCifCli` | Sin validación | `required, max:9` | `varchar(9)`, NOT NULL | OK (backend coincide con BD) |
| `TelFijCli` | Sin validación | `nullable, max:50` | `varchar(14)` | **P1** — mismatch 50 vs 14 |
| `TelMovCli` | Sin validación | `required, max:50` | `varchar(14)` | **P1** — mismatch 50 vs 14 |
| `EmaCli` | `type="email"` | `required, email, max:255` | `varchar(250)`, NOT NULL | **P2** — margen de 5 caracteres, riesgo bajo pero real |
| `EmaCliOpc` | `type="email"` | `nullable, email, max:255` | `varchar(70)` | **P0** — mismatch 255 vs 70 |
| `WebCli` | Sin validación | `nullable, max:255` | `varchar(50)` | **P0** — mismatch 255 vs 50 |
| `ObsCli` | Textarea sin `maxLength` | `nullable, string` (**sin `max`**) | `varchar(200)` | **P0** — backend no limita en absoluto; cualquier observación >200 car. rompe el guardado |
| `CodTipViaFis` | Sin `required` | `required, integer` | `T_Cliente.CodTipViaFis` nullable | Riesgo bajo (int) |
| `NomViaDomFis` | Sin `maxLength` | `required, max:255` | `varchar(100)`, NOT NULL | **P0** — mismatch 255 vs 100 |
| `NumViaDomFis` | Sin `maxLength` | `required, max:50` | `varchar(100)`, NOT NULL | Backend más estricto que BD, sin riesgo de overflow |
| `BloDomFis` | `maxLength={3}` ✔ | `required, max:50` | `varchar(3)` | **P0** — backend permite 50, BD solo 3; el frontend "salva" el caso feliz pero cualquier otro punto de entrada (API, importador) rompe |
| `EscDomFis` | Sin `maxLength` | `required, max:50` | `varchar(10)` | **P1** — mismatch 50 vs 10 |
| `PlaDomFis` | Sin `maxLength` | `required, max:50` | `varchar(10)` | **P1** — mismatch 50 vs 10 |
| `PueDomFis` | Sin `maxLength` | `required, max:50` | `varchar(50)` | OK |
| `CPLocFis` | readonly (derivado) | `required, max:20` | `varchar(50)` | Backend más estricto, sin riesgo |
| `DireccionBBDD` | Sin control | `nullable, max:255` | `varchar(150)` | **P1** — mismatch 255 vs 150 |

**Nota adicional:** `isFormValid` en este componente queda fijo en `true` (nunca se recalcula), es decir, el botón "Guardar" **nunca se deshabilita** por validación de cliente; toda la responsabilidad recae 100% en la respuesta del servidor.

### 1.2 `EditarClienteModal.tsx` (`Dashboard/Editar/`) — mismo backend/BD

Este modal sí usa `required` + `maxLength={3}` en Bloque/Escalera/Planta/Puerta (mejor que la página), pero **repite el mismo problema de fondo**: como el límite real lo impone el backend (`max:255`/`max:50`) y no la BD, campos como `RazSocCli`, `NomViaDomFis`, `DireccionBBDD`, `ObsCli` siguen sin protección efectiva de longitud real.

---

## 2. Módulo Contactos

### 2.1 `CrearContacto.tsx` / `EditarContacto.tsx` — tabla `T_ContactoCliente`

Endpoint: `POST contactos.store` / `PUT contactos.update` → `StoreContactoRequest` / `UpdateContactoRequest`.

| Atributo | Validación frontend | Backend | Campo BD (`T_ContactoCliente`) | Riesgo |
|---|---|---|---|---|
| `NomConCli` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50 |
| `CarConCli` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50 |
| `TelFijConCli` | JS obligatorio, **sin `maxLength={9}`** en la página (sí en el modal) | `required, max:30` | `varchar(9)` | **P0** — tres niveles (∞ front / 30 backend / 9 BD) |
| `TelCelConCli` | igual que arriba | `required, max:30` | `varchar(9)` | **P0** — igual |
| `NIFConCli` | Sin `maxLength` en la página (el modal sí usa `maxLength={20}`) | `nullable, max:20` | `varchar(10)` | **P1** — mismatch 20 vs 10 |
| `nrcomercialacreditado` / `nrcomercialacreditadoadx` | Sin validación | `nullable, max:50` | `varchar(5)` | **P0** — mismatch 50 vs 5 |
| `ObsConC` (Observaciones) | `maxLength={200}` ✔ + JS | `nullable, max:200` | `varchar(200)` | OK — único campo con los 3 niveles alineados |
| `NumIBAN` (`iban`) | JS: solo exige `length <= 34` tras limpiar espacios (no exige `ES` ni 24) | `nullable, max:34` | `varchar(26)` | **P0** — un IBAN internacional de 27-34 car. (válido en la UE) pasa frontend y backend y **rompe en BD** |
| `NomViaDomFis` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(100)` | **P0** — mismatch 255 vs 100 |
| `NumViaDomFis` | JS obligatorio, sin `maxLength` | `required, max:50` | `varchar(100)` | Backend más estricto, sin riesgo |
| `BloPunSum`/`EscPunSum`/`PlaPunSum`/`PuePunSum` | Sin `maxLength` en la página | `nullable, max:50` | `varchar(100)` en esta tabla | Sin riesgo de overflow (BD más ancha que backend) |

**Hallazgo aparte:** el campo `iban` de `T_ContactoCliente` (contacto personal) tiene una cobertura mucho peor que el `NumIBan` de `T_CuentaBancaria` (cuenta de cobro), a pesar de representar el mismo tipo de dato. Ver §3.

---

## 3. Módulo Cuentas Bancarias (`Dashboard/Editar/CrearCuentaBancariaModal.tsx` / `EditarCuentaBancariaModal.tsx`)

Endpoint: `POST/PUT clientes.cuenta-bancaria.store|update` → `UpdateCuentaBancariaRequest` → tabla `T_CuentaBancaria`.

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `NumIBan` | `required` + `maxLength={24}` + validación de formato vía `api.data.iban.validate` | `required, max:24` | `varchar(24)`, NOT NULL | **OK** — único flujo de la app con los 3 niveles perfectamente alineados |
| `ObserCuenBan` | Sin `maxLength` | `nullable, max:255` | `varchar(100)` | **P1** — mismatch 255 vs 100 |

Este módulo es la referencia de buena práctica del proyecto: debería replicarse el patrón (tope exacto + validación de formato) en el IBAN de Contactos (§2.1).

---

## 4. Módulo Puntos de Suministro y CUPS embebidos

### 4.1 `EditarPunto.tsx` / `CrearPuntoSuministroModal.tsx` / `EditarPuntoSuministroModal.tsx`

Endpoint: `POST/PUT puntos-suministro.store|update` → `SavePuntoSuministroRequest` → tabla `T_PuntoSuministro` + `T_CUPsElectrico`/`T_CUPsGas`.

| Atributo | Frontend | Backend (`SavePuntoSuministroRequest`) | Campo BD | Riesgo |
|---|---|---|---|---|
| `NomViaPunSum` | JS obligatorio, sin `maxLength` | `required, string` (**sin `max`**) | `T_PuntoSuministro.NomViaPunSum varchar(100)`, NOT NULL | **P0** — ni frontend ni backend limitan; cualquier valor >100 car. rompe el alta |
| `NumViaPunSum` | Sin `maxLength` | `nullable, string` (sin `max`) | `varchar(100)` | **P1** |
| `BloPunSum` | Página: sin `maxLength`. Modales: `maxLength={3}` + `slice(0,3)` ✔ | `nullable, string` (sin `max`) | `varchar(4)` | **P0** en la página (sin ningún tope); modales OK |
| `EscPunSum` | Página: sin `maxLength`. Modales: `maxLength={3}` | `nullable, string` (sin `max`) | `varchar(5)` | **P1** en la página |
| `PlaPunSum` / `PuePunSum` | Igual patrón | `nullable, string` (sin `max`) | `varchar(5)` cada uno | **P1** en la página |
| `RefCasPunSum` | Sin control | `nullable, string` (sin `max`) | `varchar(20)` | **P1** |
| `ObsPunSum` | Sin `maxLength` | `nullable, string` (sin `max`) | `varchar(200)` | **P1** |
| `CPLocSoc` | readonly | `nullable, string` (sin `max`) | `varchar(50)` | Riesgo bajo (derivado, no editable) |
| `TelPunSum` | `maxLength={9}` ✔ | `nullable, string` (sin `max`) | `varchar(9)` | OK gracias solo al frontend; el backend no lo respalda |
| `cupsElectrico.*.cups` | `EditarPunto.tsx`: valida `ES` + `length>=20` + `maxLength={22}`. Modales del Dashboard: valida `ES`+`>=20` **sin** `maxLength` | `nullable, string, max:50` | `T_CUPsElectrico.CUPsEle varchar(24)`, NOT NULL | **P0** — backend permite 50 car., BD solo 24. Los modales del Dashboard no acotan el máximo en el navegador |
| `cupsGas.*.cups` | Mismo patrón (`ES`+`>=20`) | `nullable, string, max:50` | `T_CUPsGas.CupsGas varchar(20)` | **P0** — backend permite 50, BD solo 20; un CUPS de gas real con sufijo puede llegar a 22 car. y ya rompería la columna |
| `EstPunSum` | Select | `nullable, integer` | `int`, **NOT NULL**, default `1` | Riesgo bajo (tiene default), pero si se envía `null` explícito, backend lo permite y la BD lo rechaza |
| `DimPunSum` | Sin control (numérico en teoría) | `nullable, string` | `int(10)` | **P2** — tipo de dato inconsistente (backend valida como string, columna es entero) |

### 4.2 `EditarCups.tsx` (grid independiente de CUPS) — **el peor caso del informe**

Endpoint: `POST cups.store` / `PUT cups.update/{id}/{tipo}` → `CupsController::store/update` → `CupsTraits::storeCupsElectricoTrait/storeCupsGasTrait/updateCupsElectricoTrait/updateCupsGasTrait`.

```php
// app/Traits/CupsTraits.php
$cups->fill($request->only(['CodPunSum','CodDis','CUPsEle', ...]));
$cups->save(); // sin FormRequest, sin ->validate(), sin reglas de ningún tipo
```

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `CUPsEle` | **SIN VALIDACIÓN** (ni `required` ni `maxLength`) | **SIN VALIDACIÓN** (`Request` plano, `$request->only()`) | `varchar(24)`, NOT NULL | **P0 crítico** — ruta de escritura totalmente desprotegida; un CUPS vacío, duplicado o >24 car. llega directo a `save()` y solo la BD lo detiene (con un 500 que además expone `$e->getMessage()` en el JSON de respuesta) |
| `CupsGas` | **SIN VALIDACIÓN** | **SIN VALIDACIÓN** | `varchar(20)`, nullable | **P0 crítico** — mismo problema |
| `CodPunSum` | Typeahead, sin validar existencia antes de enviar | Sin validación (`exists:` ausente) | `int`, NOT NULL, FK lógica a `T_PuntoSuministro` | **P0** — se puede enviar un `CodPunSum` inexistente y solo lo detiene una FK real en BD (si existe) o queda huérfano si no hay FK física |
| `PotConP1..P6`, `PotMaxBie`, `ConAnuCup`, `DerAccKW` | `type="number"` (solo tipo HTML) | Sin validación de rango | `decimal(10,2/3)` | **P1** — sin `min`/`max`, se puede enviar un decimal fuera de rango que desborda el `decimal(10,x)` |
| `caudaldiario` | `type="number"` | Sin validación | `int` | **P2** |

**Este es el único formulario del alcance donde ni el frontend ni el backend aplican ninguna regla**, ni siquiera de obligatoriedad. Se recomienda crear `StoreCupsElectricoRequest`/`StoreCupsGasRequest` (o unificado) con, como mínimo: `CUPsEle` `required|string|max:24|regex:/^ES\d{16}[A-Z]{2}\d{0,2}$/` y `CodPunSum` `required|exists:T_PuntoSuministro,CodPunSum`.

---

## 5. Módulo Contratos — Alta (UniClienteUniPunto / UniClienteMultiPunto / MultiClienteMultiPunto)

Endpoints: `POST contratos.save-multi-punto` (unipunto y multipunto) y `POST contratos.save-contrato-multi-cliente-multi-punto` → `SaveContratoUniClienteMultiPuntoRequest` / `SaveContratoMultiClienteMultiPuntoRequest`.

Confirmado por los tres subagentes de Contratos: **ningún** `Form.Control` de estos flujos usa `required`/`maxLength`/`pattern` HTML. `validacionFormulario.ts` solo verifica "campo no vacío", nunca longitud ni formato.

| Atributo | Frontend | Backend | Campo BD apuntado | Riesgo |
|---|---|---|---|---|
| `documento_fiscal` (NIF/CIF) | JS obligatorio, sin `maxLength` | `required, string` (**sin `max`**) | `T_Cliente.NumCifCli varchar(9)` (vía lookup) | **P0** — aunque normalmente viene de un Typeahead ya existente, si el usuario edita el texto libre puede superar 9 car. |
| `razon_social` | JS obligatorio (solo en multipunto/multicliente; en unipunto ni eso), sin `maxLength` | `required, string` (sin `max`) | `T_Cliente.RazSocCli varchar(50)` | **P0** |
| `comentarios` / `comentarioGas` (por fila CUPS) | **SIN VALIDACIÓN**, textarea libre | No existe regla para este campo en el `FormRequest` (no está en `rules()`) | `T_Propuesta_Comercial_CUPs.ObsCup varchar(5000)` | **P1** — margen amplio en BD (5000), pero al no validarse en ningún lado, un pegado masivo de texto (>5000) sigue rompiendo el insert |
| `corpoGo` | **SIN VALIDACIÓN**, input texto editable | Sin regla en `rules()` | `T_Propuesta_Comercial_CUPs.CorpoGo decimal(26,2)` | **P1** — se valida como string libre pero la columna es numérica; un valor no numérico falla el cast/insert |
| `Id_Tarifa_Automatica` / `nombretarifa` / `productCode` / `version` | **SIN VALIDACIÓN**, vienen del modal de precios | Sin regla en `rules()` de alta (sí se valida como `nullable|string` en `updatePropuestaComercialCups`, ver §6) | `varchar(100)/varchar(100)/varchar(50)/varchar(50)` respectivamente | **P1** — mismo dato, mejor protegido en edición que en alta |
| `cups_electrico` / `cupsGas` (código CUPS) | JS obligatorio (no vacío), sin `pattern`/`maxLength` | `required, string` (sin `max`) | `T_CUPsElectrico.CUPsEle varchar(24)` / `T_CUPsGas.CupsGas varchar(20)` | **P1** — normalmente viene de un Typeahead sobre CUPS ya existentes, pero nada impide construir el payload con un valor distinto |
| `fechaFin` | JS obligatorio + `after:fechaInicio` (solo unipunto) | `required, date, after:...` | `T_Propuesta_Comercial_CUPs.FecVenCUPs date` | OK en unipunto; en multipunto/multicliente **no se exige `after`**, riesgo lógico de fechaFin < fechaInicio |
| `anexo` | Opcional en los 3 flujos custom (a diferencia del hook genérico) | No es `required` en ninguna variante | `T_Propuesta_Comercial_CUPs.CodAnePro` nullable | Riesgo de negocio (contrato sin anexo comercial), no de crash |

---

## 6. Módulo Contratos — Edición (CUPS inline, documentos, comercial/canal)

### 6.1 Edición inline de CUPS del contrato (`CupsTableRow.tsx` + `useContratoEdition.ts` + `useContratoHandlers.ts`)

Endpoint: `PUT propuesta-comercial-cups.actualizar/{id}` → `ContratosController::updatePropuestaComercialCups` (`validate()` inline, no `FormRequest`) → tabla `T_Propuesta_Comercial_CUPs`.

```php
$validated = $request->validate([
    'Id_Tarifa_Automatica' => 'nullable|string',   // sin max
    'nombretarifa'         => 'nullable|string',   // sin max
    'productCode'          => 'nullable|string',   // sin max
    'version'              => 'nullable|string',   // sin max
    ...
]);
```

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `Id_Tarifa_Automatica` | Sin `maxLength` (viene del `ModalPrecios`) | `nullable|string` (sin `max`) | `varchar(100)` | **P1** |
| `nombretarifa` | Sin `maxLength` | `nullable|string` (sin `max`) | `varchar(100)` | **P1** |
| `productCode` | Sin `maxLength` | `nullable|string` (sin `max`) | `varchar(50)` | **P1** |
| `version` | Sin `maxLength` | `nullable|string` (sin `max`) | `varchar(50)` | **P1** |
| `FecActCUPs` / `FecVenCUPs` | Sin `required` en el date picker | `required|date` | `date` | Riesgo bajo (backend cubre) |
| `EstConCups` | Select cerrado | `required|integer|in:1,2,3,4` | `int` default 1 | OK |
| `GDO`/`Permanencia`/`CambioTitular`/`CambioPotencia` | Checkbox `'1'`/`'0'` | `nullable|in:0,1,true,false` | `tinyint(3)` | OK |

Aunque el margen de BD (100/100/50/50) es razonable, **nada en todo el pipeline (frontend → backend) impone un tope**, por lo que basta con que la fuente de estos valores (integración con tarifario externo/Audax) entregue un texto más largo de lo esperado para romper el `PUT`.

### 6.2 `comercial` / `canal` (`promptComercialCanal.ts`, usado en `EditarContrato`, `EnviarDocumentoComercializadoraModal`, generación de PDFs/OKCOM)

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `comercial` | JS: `trim()` + obligatorio (prompt Swal), **sin `maxLength`** | Varía por endpoint destino (algunos ni siquiera lo validan, va por query string) | No persiste en columna propia; se usa para nombre de fichero/metadatos en generación de documentos | **P2** — riesgo de nombre de archivo/ruta excesivamente largo más que de BD |
| `canal` | Igual | Igual | Igual | **P2** |

### 6.3 `UploadDocumentoModal.tsx` — `observaciones`

| Atributo | Frontend | Backend (`StoreFileRequest`) | Campo BD | Riesgo |
|---|---|---|---|---|
| `observaciones` | Textarea sin `maxLength` (si vacío se envía `'-'`) | `required|string|max:500` | Depende del destino final (log/registro documental) | **P2** — backend ya acota a 500; verificar que la columna destino sea ≥500 |

---

## 7. Módulo Comerciales (Comercializadoras / Productos / Anexos de Producto)

Ningún `Form.Control` de los 3 formularios usa `required`/`maxLength`/`pattern`. La obligatoriedad es 100% JS (`validacionFormularioMaestro.ts`), y solo cubre 2-4 campos por formulario.

### 7.1 `EditarComercializadora.tsx` — `UpdateComercializadoraRequest` → `T_Comercializadora`

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `RazSocCom` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50 |
| `NomComCom` | Sin validación | `nullable, max:255` | `varchar(50)` | **P0** — mismatch 255 vs 50 |
| `NumCifCom` | JS obligatorio, sin `maxLength` | `required, max:50` | `varchar(10)` | **P0** — mismatch 50 vs 10 |
| `NomViaDirCom` | Sin validación | `nullable, max:255` | `varchar(30)` | **P0** — mismatch 255 vs 30 |
| `NumViaDirCom` | Sin validación | `nullable, max:50` | `varchar(3)` | **P0** — mismatch 50 vs 3 |
| `BloDirCom` | Sin validación | `nullable, max:50` | `varchar(3)` | **P0** |
| `EscDirCom` / `PlaDirCom` | Sin validación | `nullable, max:50` | `varchar(2)` cada uno | **P0** — mismatch 50 vs 2, el más extremo del informe |
| `PueDirCom` | Sin validación | `nullable, max:50` | `varchar(4)` | **P0** |
| `TelFijCom` | Sin validación | `nullable, max:50` | `varchar(9)` | **P1** |
| `EmaCom` | `type="email"` | `nullable, email, max:255` | `varchar(50)` | **P0** |
| `PagWebCom` | Sin validación | `nullable, max:255` | `varchar(50)` | **P0** |
| `NomConCom` / `CarConCom` | Sin validación | `nullable, max:255` | `varchar(50)` cada uno | **P0** |
| `ObsCom` | Textarea sin `maxLength` | `nullable, string` (**sin `max`**) | `varchar(200)` | **P0** |
| `CPLoc` | Sin validación (Typeahead) | `nullable, max:20` | `varchar(20)` | OK |

### 7.2 `EditarProducto.tsx` — `StoreProductoRequest`/`UpdateProductoRequest` → `T_Producto`

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `DesPro` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50 |
| `ObsPro` | Textarea sin `maxLength` | `nullable, max:1000` | `varchar(200)` | **P0** — mismatch 1000 vs 200 |
| `CodigoServicio` | Sin validación | `nullable, string` (store: `max:50`; update: **sin `max`**) | `varchar(2)` | **P0 grave** — mismatch 50 vs 2 (o sin tope en update); prácticamente cualquier valor no vacío rompe la columna |

### 7.3 `EditarAnexoProducto.tsx` — `StoreAnexoProductoRequest`/`UpdateAnexoProductoRequest` → `T_AnexoProducto`

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `DesAnePro` | JS obligatorio, sin `maxLength` | `required, max:255` | `varchar(50)`, NOT NULL | **P0** — mismatch 255 vs 50 |
| `DocAnePro` | Textarea sin `maxLength` | store: `max:255`; update: `max:500` | `varchar(255)` | **P1** en `update` (500 vs 255); OK en `store` |
| `ObsAnePro` | Textarea sin `maxLength` | `nullable, max:1000` | `varchar(200)` | **P0** — mismatch 1000 vs 200 |

---

## 8. Módulo Sips (consulta de suministros)

`AdxSipsPanel.tsx` / `AudaxSipsPanel.tsx` → `POST sips.consumo` (consulta a servicio externo, **no persiste directamente** en las tablas de negocio; menor severidad que los módulos de escritura).

| Atributo | Frontend | Riesgo |
|---|---|---|
| `cups` | JS: obligatorio, debe empezar por `ES` y tener `length >= 20`, **sin tope superior** | **P2** — al ser una consulta de solo lectura hacia un servicio externo, el riesgo es un error de la API externa, no de la BD propia. Aun así conviene acotar a 22 para evitar llamadas inútiles |
| `tipo` | JS obligatorio | OK |

`ComercializadoraSelect.tsx` no persiste nada (solo estado local de UI).

---

## 9. Módulo Caes

`ConsultaCaes.tsx` es de solo lectura. `CaesCardsView.tsx` filtra en cliente (`globalQuery`) sin llamada a backend. **Sin riesgo de integridad de datos** en el alcance analizado.

---

## 10. Módulo Cups (grid de búsqueda) — filtros

`CupsCardsView.tsx` envía filtros (`CUPsName`, `Cups_RazSocCli`, `Cups_Cif`, `NomTarGas`) por `GET cups.load-more` (query string, sin `maxLength`). Al ser parámetros de **lectura** (`WHERE ... LIKE`), el riesgo no es de integridad sino, como máximo, de URLs muy largas (`414 URI Too Long`) si se pegan textos extensos. Prioridad **P3**.

---

## 11. Módulo Cargas Masivas (`CargasMasivas/PuntosSuministro/Index.tsx`)

Endpoint: `POST configuracion.carga-masiva.puntos-suministro` → `CargaGlobalAudaxRequest` (solo valida el **archivo**: `required|file|max:50240|mimes:xlsx,xls,csv`) → `CargaGlobalAudax.php` (trait de procesamiento fila a fila).

| Columna del Excel | Campo BD destino | Validación de longitud en algún punto del pipeline |
|---|---|---|
| `CIF_Cliente` | `T_Cliente.NumCifCli varchar(9)` | **Ninguna** — solo se hace `trim()` |
| `Nombre_Via` | `T_PuntoSuministro.NomViaPunSum varchar(100)` | **Ninguna** |
| `Bloque` / `Escalera` / `Planta` / `Puerta` | `varchar(4)/varchar(5)/varchar(5)/varchar(5)` | **Ninguna** — columnas muy cortas, altísima probabilidad de overflow con datos reales de Excel (p. ej. "Bajo Izquierda" en Puerta) |
| `Codigo_Postal` | `CPLocSoc varchar(50)` (vía lookup de localidad) | **Ninguna** |
| `Observaciones` | `ObsPunSum varchar(200)` | **Ninguna** |

**Riesgo P0 de mayor impacto en volumen:** al ser una carga masiva (decenas/cientos de filas por archivo), un solo valor largo en una columna corta (Bloque/Escalera/Planta/Puerta) puede abortar el procesamiento de esa fila (o del lote, según cómo esté implementado el `try/catch` por fila) sin que el usuario tenga ninguna pista previa en el frontend — solo sube el archivo y confía en el resultado final.

---

## 12. Módulo Usuarios y Perfil

### 12.1 `Users/Crear.tsx` / `Users/Editar.tsx` — `StoreUserRequest`/`UpdateUserRequest` → tabla `users`

| Atributo | Frontend | Backend | Campo BD | Riesgo |
|---|---|---|---|---|
| `name` | Sin `required`/`maxLength` HTML | `required, max:255` | `varchar(255)`, NOT NULL | OK (backend = BD) |
| `email` | `type="email"` nativo, sin `maxLength` | `required, email, max:255, unique` | `varchar(255)`, NOT NULL | OK |
| `password` | JS: fuerza (≥8, mayúscula, número, especial); **sin tope superior** | `required, min:8, confirmed` (**sin `max`**) | `varchar(255)` (se guarda el hash, no el texto plano) | **P3** — sin impacto en BD porque se hashea antes de persistir, pero conviene acotar (`max:72`, límite práctico de bcrypt) para evitar DoS con contraseñas absurdamente largas |
| `password_confirmation` | **No se compara explícitamente contra `password` en JS** | `confirmed` (si difiere, error del backend) | — | **P2 UX** — el usuario solo se entera del desajuste tras el roundtrip al servidor |

### 12.2 `Perfil/CambiarPassword.tsx` — sin `FormRequest` visible en este flujo (usa el `PasswordController` estándar de Breeze/Jetstream)

| Atributo | Frontend | Riesgo |
|---|---|---|
| `current_password` | **Sin ninguna validación** | **P2** — depende 100% de la regla `current_password` del backend estándar de Laravel |
| `password` / `password_confirmation` | **Sin ninguna validación** (el texto de ayuda "mínimo 8 caracteres" no se aplica en código) | **P2** |

---

## 13. Tabla consolidada de hallazgos priorizados

| # | Prioridad | Módulo | Problema | Campo(s) BD afectado(s) |
|---|---|---|---|---|
| 1 | **P0** | Cups (grid) | Alta/edición de CUPS **sin ninguna validación** en frontend ni backend | `T_CUPsElectrico.CUPsEle`, `T_CUPsGas.CupsGas` |
| 2 | **P0** | Cargas Masivas | Sin validación de longitud de ninguna columna del Excel antes de insertar | `T_PuntoSuministro.*` (Bloque/Escalera/Planta/Puerta especialmente) |
| 3 | **P0** | Clientes | `RazSocCli`/`NomComCom`/`WebCli`/`EmaCliOpc`/`ObsCli`/`NomViaDomFis` — backend permite 255/1000, BD permite 50-200 | `T_Cliente.*` |
| 4 | **P0** | Comercializadoras | Prácticamente todos los campos de texto — mismatch sistemático (hasta 25× en `EscDirCom`/`PlaDirCom`) | `T_Comercializadora.*` |
| 5 | **P0** | Productos | `CodigoServicio` — BD `varchar(2)` vs backend `max:50` (o sin `max` en update) | `T_Producto.CodigoServicio` |
| 6 | **P0** | Puntos de Suministro | `NomViaPunSum` y Bloque/Escalera/Planta/Puerta sin `max` en el `FormRequest` | `T_PuntoSuministro.*` |
| 7 | **P0** | Contactos | IBAN personal (`iban`) sin formato/tope real: front ≤34, backend ≤34, BD `varchar(26)` | `T_ContactoCliente.iban` |
| 8 | **P1** | Contratos (alta/edición) | Campos de tarifario (`Id_Tarifa_Automatica`, `nombretarifa`, `productCode`, `version`, `comentarios`) sin tope en ningún nivel | `T_Propuesta_Comercial_CUPs.*` |
| 9 | **P1** | Anexo de Producto | `ObsAnePro` backend `max:1000` vs BD `varchar(200)` | `T_AnexoProducto.ObsAnePro` |
| 10 | **P2** | Usuarios | `password_confirmation` no se compara en cliente antes de enviar | `users.password` (indirecto) |
| 11 | **P2** | Sips | CUPS de consulta sin tope superior (bajo impacto: es solo lectura externa) | — |
| 12 | **P3** | Cups (filtros) / Caes | Filtros de búsqueda sin `maxLength` (riesgo de URL larga, no de integridad) | — |

---

## 14. Recomendaciones

1. **Regla de oro a instaurar:** todo `FormRequest` debe fijar `max:` **igual a `CHARACTER_MAXIMUM_LENGTH` de la columna real** (no un valor genérico de 255). Se puede automatizar con un comando Artisan que lea `information_schema` y genere/verifique las reglas.
2. **Espejar en el frontend** el mismo tope con `maxLength` en cada `Form.Control`/`textarea`, para dar feedback inmediato (regla de Jakob Nielsen de prevención de errores) en vez de depender del roundtrip al servidor.
3. **Cerrar la brecha más grave primero:** crear `StoreCupsElectricoRequest`/`StoreCupsGasRequest` para `CupsController` (§4.2), hoy sin ninguna validación.
4. **Cargas masivas:** añadir una etapa de validación por fila (longitud + tipo) antes del `insert`/`update`, devolviendo un reporte de filas rechazadas en vez de dejar que la excepción de BD interrumpa el procesamiento.
5. **Unificar el campo IBAN:** aplicar al `iban` de `T_ContactoCliente` la misma validación de formato/longitud que ya existe para `T_CuentaBancaria.NumIBan` (§3), incluyendo el mismo endpoint de verificación (`api.data.iban.validate`).
6. **Mensajes de error:** los `catch (\Throwable $e)` que devuelven `$e->getMessage()` al cliente (p. ej. `CupsTraits.php`) deberían registrar el detalle en el log y devolver un mensaje genérico al usuario, evitando fugas de información de esquema de BD.
