# Implementation Plan: Cambio de Titular de Puntos de Suministro con Trazabilidad y Sincronización Contractual

**Feature Branch**: `015-cambio-titular-puntosuministro`

**Created**: 2026-10-08

**Status**: Draft

**Input**: `specs/015-cambio-titular-puntosuministro/spec.md`

> **Constitution (II. Skinny Controllers, IV. Inertia.js Frontera)**: Este plan define CÓMO se implementará la funcionalidad en Laravel 12 + React + TypeScript + Tailwind CSS, respetando arquitectura limpia, servicios de responsabilidad única (SRP), controladores delgados (<15 líneas por acción) y contratos estrictos en TypeScript.

---

## User Review Required

> [!IMPORTANT]
> **Decisiones de diseño y arquitectura para validación del usuario:**
> 1. **Tabla de Histórico:** Se creará la tabla `T_HISTORICO_CAMBIO_TITULAR` para registrar inmutablemente los movimientos con campos `CodPunSum`, `CodCliAnterior`, `CodCliNuevo`, `FecCambio`, `procesado` (boolean) y `CodUsu`.
> 2. **Criterio de Contrato Previo Vigente (Caso Diferido):** Se considerará vigente aquel contrato en `T_Propuesta_Comercial_CUPs` donde `FecVenCUPs >= FecCambio` (o fecha actual) y `EstConCups = 1`. Si no existe contrato previo vigente para heredar el vencimiento, se calculará `FecVenCUPs = FecCambio + 1 año` (365 días) por defecto.
> 3. **Exclusión en Asignación Masiva (`isDatosComunes`):** Toda fila con `cambioTitular === true || cambioTitular === '1'` mantendrá sus fechas y quedará inmune a la sobreescritura de `fechaInicio`/`fechaFin` tanto en contratos Tipo 2 (`UniClienteMultiPunto`) como Tipo 3 (`MultiClienteMultiPunto`).
> 4. **Resaltado Visual en Tablas:** Se añadirá un badge identificativo (*"Cambio de titular activo"*) y sombreado ámbar suave (`bg-amber-50/50 dark:bg-amber-950/20 border-l-4 border-l-amber-500`) en las tablas de CUPS.

---

## Open Questions

- *Ninguna pregunta bloqueante identificada.* Los requerimientos y tipologías (`TipProCom = 1, 2` vs `TipProCom = 3` para representantes legales) están claramente delimitados en la especificación.

---

## Proposed Changes

### Database & Models

#### [NEW] `database/migrations/2026_10_08_180000_create_t_historico_cambio_titular_table.php`
- Creación de tabla `T_HISTORICO_CAMBIO_TITULAR`:
  - `id` (bigIncrements)
  - `CodPunSum` (unsignedBigInteger, index, fk -> `T_PUNTOSUMINISTRO.CodPunSum`)
  - `CodCliAnterior` (unsignedBigInteger, index, fk -> `T_CLIENTE.CodCli`)
  - `CodCliNuevo` (unsignedBigInteger, index, fk -> `T_CLIENTE.CodCli`)
  - `FecCambio` (dateTime)
  - `procesado` (boolean, default 0, index)
  - `CodUsu` (unsignedBigInteger, nullable, fk -> `users.id`)
  - `tenant_id` (unsignedBigInteger, nullable, index)
  - `timestamps`

#### [NEW] `app/Models/HistoricoCambioTitular.php`
- Modelo Eloquent `HistoricoCambioTitular` mapeando tabla `T_HISTORICO_CAMBIO_TITULAR`.
- Casts: `FecCambio => datetime`, `procesado => boolean`.
- Relaciones:
  - `puntoSuministro()`: `belongsTo(PuntoSuministro::class, 'CodPunSum', 'CodPunSum')`
  - `clienteAnterior()`: `belongsTo(Cliente::class, 'CodCliAnterior', 'CodCli')`
  - `clienteNuevo()`: `belongsTo(Cliente::class, 'CodCliNuevo', 'CodCli')`
  - `usuario()`: `belongsTo(User::class, 'CodUsu', 'id')`
- Scopes:
  - `scopePendientes($query, int $codPunSum)`: filtra por `CodPunSum` y `procesado = false`.

#### [MODIFY] `app/Models/PuntoSuministro.php`
- Añadir relación `historicoCambiosTitular()`: `hasMany(HistoricoCambioTitular::class, 'CodPunSum', 'CodPunSum')->orderByDesc('FecCambio')`.

---

### Backend Services & Controllers (Laravel)

#### [MODIFY] `app/Services/PuntoSuministroTitularService.php`
- Refactorizar método `cambiarTitular(int $codPunSum, int $nuevoCodCli, bool $actualizarPropuestas = true): array`:
  1. Validar que `$nuevoCodCli !== $punto->CodCli`.
  2. Registrar movimiento inmutable en `HistoricoCambioTitular` con `procesado = false`.
  3. Actualizar `$punto->CodCli = $nuevoCodCli`.
  4. Sincronización Inmediata (`Caso A`):
     - Si `$actualizarPropuestas`: buscar propuestas en estado pendiente (`NrContratoAudax` nulo o '0') vinculadas al punto.
     - Si existen:
       - Para cada propuesta: marcar `CambioTitular = true`, `FecActCUPs = $fecCambio->toDateString()`.
       - Verificar tipo de propuesta (`TipProCom`): si es Tipo 1 o 2, reasignar `propuestaComercialCliente->CodCli = $nuevoCodCli`; si es Tipo 3 (Representante Legal), **NO** modificar `CodCli` de la propuesta comercial.
       - Marcar el registro histórico como `procesado = true`.
  5. Retornar payload estandarizado con advertencias formateadas.
- Añadir método `resolverCambioTitularDiferido(int $codPunSum, int $codCup, int $tipCups, array $datosContrato): array`:
  - Utilizado por el flujo de generación de contratos (`Caso B`).
  - Consulta si existe registro en `HistoricoCambioTitular::pendientes($codPunSum)->first()`.
  - Si existe:
    - Busca el último contrato previo en `PropuestaComercialCups` para ese CUPS (`where('CodCup', $codCup)->where('TipCups', $tipCups)`), ordenado por `FecActCUPs DESC` que esté vigente (`FecVenCUPs >= now()`).
    - Si existe previo vigente:
      - `FecActCUPs` = fecha del cambio de titular.
      - `FecVenCUPs` = fecha de vencimiento del contrato previo vigente.
      - `CambioTitular` = 1.
    - Si no existe previo vigente:
      - `FecActCUPs` = fecha del cambio de titular.
      - `FecVenCUPs` = calculada con duración estándar (365 días).
      - `CambioTitular` = 1.
    - Marca el registro histórico como `procesado = true`.
    - Retorna las fechas y flag actualizados.

#### [MODIFY] `app/Traits/ContratoTraits.php`
- Inyectar / resolver `PuntoSuministroTitularService` en los métodos de inserción de CUPS (`procesarCupsElectrico` y `procesarCupsGas`).
- Antes de ejecutar `PropuestaComercialCups::create([...])`:
  - Invocar `resolverCambioTitularDiferido` pasando el `CodPunSum`, `CodCup` y tipo de CUPS (1 o 2).
  - Si devuelve ajuste de cambio de titular, asignar los valores calculados de `FecActCUPs`, `FecVenCUPs` y `CambioTitular = 1`.

#### [MODIFY] `app/Http/Controllers/PuntoSuministroController.php`
- Mantener acción `cambiarTitular` delgada (<15 líneas): validar request, delegar en `PuntoSuministroTitularService->cambiarTitular(...)` y retornar JSON.
- Añadir acción `historicoTitular(int $id, PuntoSuministroTitularService $titularService)` (<10 líneas): retornar histórico paginado o lista de movimientos de titularidad para el punto de suministro.

#### [MODIFY] `routes/web.php`
- Agregar ruta `Route::get('/puntoSuministro/{id}/historico-titular', [PuntoSuministroController::class, 'historicoTitular'])->whereNumber('id')->name('puntos-suministro.historico-titular');`.

---

### API Contracts & Types (TypeScript)

#### [MODIFY] `resources/js/interfaces.ts`
- Añadir interface `HistoricoCambioTitular`:
  ```typescript
  export interface HistoricoCambioTitular {
      id: number;
      CodPunSum: number;
      CodCliAnterior: number;
      CodCliNuevo: number;
      FecCambio: string;
      procesado: boolean;
      CodUsu?: number | null;
      clienteAnterior?: { CodCli: number; RazSocCli: string; NumCifCli: string };
      clienteNuevo?: { CodCli: number; RazSocCli: string; NumCifCli: string };
      usuario?: { id: number; name: string };
      created_at: string;
  }
  ```

---

### Frontend Pages & Components (React + Inertia)

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteMultiPunto/hooks/useCupsElectricosManagement.ts` y `useCupsGasManagement.ts`
- En el bloque condicional `if (isDatosComunes)`:
  - Al propagar `fechaInicio` o `fechaFin`:
    ```typescript
    updatedRows = updatedRows.map((row) => {
        const tieneCambioTitular = row.cambioTitular === true || row.cambioTitular === '1' || row.cambioTitular === 1;
        if (tieneCambioTitular) {
            // Preservar fechas específicas del cambio de titular
            return row;
        }
        return { ...row, fechaInicio: value, fechaFin };
    });
    ```

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/MultiClienteMultiPunto/ts/useContratoMultiClienteMultiPunto.ts`
- Aplicar la misma regla de exclusión al replicar campos cuando `isDatosComunes` esté activo para `data.electrico` y `data.gas`:
  - Si `row.cambioTitular` está activo, omitir actualización de `fechaInicio` y `fechaFin`.

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/UniClienteMultiPunto/components/CupsElectricosTable.tsx` y `CupsGasTable.tsx`
- Resaltado visual en filas con `cambioTitular`:
  - Agregar clase o estilos: fondo temático ámbar suave y borde distintivo en la fila `<tr>`.
  - Agregar indicador / badge visual junto al CUPS: `Badge` con icono y texto *"Cambio titular"* para alertar al usuario.

#### [MODIFY] `resources/js/Pages/Eneon/Contratos/MultiClienteMultiPunto/components/CupsElectricosTable.tsx` y `CupsGasTable.tsx`
- Replicar el mismo resaltado visual (badge y sombreado) en las tablas de contratación multipunto multicliente (Tipo 3).

#### [MODIFY] `resources/js/Pages/Eneon/Dashboard/Editar/CambioTitularModal.tsx`
- Incorporar pestaña o botón para visualizar el histórico de cambios de titular anteriores del punto de suministro consultando la nueva ruta `puntos-suministro.historico-titular`.

---

## Verification Plan

### Automated Tests
1. **Pest / PHPUnit:**
   - Test unitario/feature `tests/Feature/PuntoSuministroTitularTest.php`:
     - Test cambio de titular con creación de registro en `T_HISTORICO_CAMBIO_TITULAR`.
     - Test sincronización inmediata en propuestas pendientes (Tipos 1/2 reasignan `CodCli`, Tipo 3 no reasigna `CodCli`, ambas marcan `CambioTitular = 1` y actualizan `FecActCUPs`).
     - Test aplicación diferida al crear contrato: herencia de `FecActCUPs` y `FecVenCUPs` de contrato previo vigente y marcado de `procesado = true`.
   - Comando: `php artisan test --filter=PuntoSuministroTitularTest`

### Manual Verification
1. **Cambio de Titular desde Dashboard:**
   - Abrir un punto de suministro con propuesta pendiente.
   - Realizar cambio de titular: comprobar aviso y verificar en base de datos que `CambioTitular = 1`, `FecActCUPs = fecha_cambio` y el log tiene `procesado = 1`.
2. **Caso Diferido (Nuevo Contrato):**
   - Cambiar titular de un punto sin propuestas pendientes (log `procesado = 0`).
   - Crear un nuevo contrato para ese CUPS: verificar que toma automáticamente la fecha de cambio y la fecha fin del contrato vigente previo.
3. **Asignación Masiva en Frontend:**
   - Abrir contratación multipunto con 3 CUPS (1 con cambio de titular, 2 normales).
   - Activar "Datos Comunes" y cambiar fecha de inicio masiva: verificar que los 2 CUPS normales se actualizan y el CUPS con cambio de titular mantiene intactas sus fechas.
   - Comprobar que la fila del CUPS con cambio de titular muestra el distintivo visual.
