# Implementation Plan: Contratos Unicliente (UniPunto y MultiPuntos) con detalle de suministro

**Branch**: `005-contratos-unicliente-detalle` | **Date**: 2026-05-28 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/005-contratos-unicliente-detalle/spec.md`

## Summary

Añadir al catálogo `agent_database` una **herramienta especializada en contratos Unicliente** (`TipProCom=1` UniPunto y `TipProCom=2` UniCliente MultiPuntos; `TipProCom=3` queda fuera de alcance). La herramienta lista contratos ordenados por **fecha de propuesta comercial (= fecha de contrato)** descendente, permite **filtrar por cualquier columna del resultado** (cliente, NIF/CIF, dirección, teléfono, correo, punto de suministro, localidad, CUPS, tarifa) y devuelve el **detalle de suministro** (CUPS eléctrico/gas, tarifas, potencias P1–P6, consumos, fechas de activación/vencimiento, direcciones y localidades).

La consulta se implementa con **Eloquent y relaciones de modelos** (no SQL ad hoc en config), portando la SQL de referencia al mapa relacional cuyo nodo central es `PropuestaComercialCups`. A diferencia del resto del catálogo, esta herramienta **NO aplica el tope de 50 filas**: usa un límite dedicado más alto y/o agrupación por contrato para no perder contratos relevantes (FR-008/FR-009).

## Technical Context

**Language/Version**: PHP 8.2+, Laravel 12

**Primary Dependencies**: Eloquent (conexión `agent_db_secondary`), `AgentDatabaseQueryCapabilityHandler`, `AgentCommercialQueryService`, `DatabaseSchemaService`, `LlmChatCompletionContract`, `ConversationHistoryService` (contexto conversacional de la feature 004)

**Storage**: BD secundaria — tablas existentes `T_PropuestaComercial`, `T_Propuesta_Comercial_Clientes`, `T_Propuesta_Comercial_CUPs`, `T_Cliente`, `T_Localidad`; tablas **nuevas a modelar**: `T_PuntoSuministro`, `T_CUPsElectrico`, `T_CUPsGas`, `T_TarifaElectrica`, `T_TarifaGas`, `T_Producto`, `T_AnexoProducto`

**Testing**: PHPUnit (`php artisan test --filter=ContratosUnicliente`)

**Target Platform**: API Laravel existente; sin cambios en Angular (`ChatFront/`)

**Project Type**: Extensión backend del orquestador de agente (brownfield); amplía el catálogo de la feature 002

**Performance Goals**: Respuesta end-to-end < 30 s en local (SC-001)

**Constraints**: Solo lectura (FR-012); `TipProCom` ∈ {1, 2} (excluir 3, FR-001); **sin tope de 50 filas** para esta herramienta (FR-008) — límite dedicado/agrupación documentada

**Scale/Scope**: 7 modelos comerciales nuevos + nuevas relaciones en `PropuestaComercialCups` y `Cliente`; 1 herramienta nueva (`listar_contratos_unicliente`) con modos `listado`/`detalle`

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Principle | Status | Notes |
|-----------|--------|-------|
| **I. Spec-First** | ✅ | Trazado a FR-001–FR-015, US1–US4; spec agnóstica de stack |
| **II. Skinny Controllers** | ✅ | Sin cambios en controladores; lógica en `AgentCommercialQueryService` |
| **III. Contratos IA** | ✅ | Reutiliza handler + `LlmChatCompletionContract`; sin acoplar proveedor |
| **IV. API v1** | ✅ | Reutiliza `POST /api/v1/chat/message`; errores runtime documentados en contrato |
| **V. Contrato API** | ✅ N/A | Feature backend-only; contrato = catálogo de herramienta en `contracts/` |
| **VI. Frontend desacoplado** | ✅ N/A | Sin cambios en `ChatFront/` |
| **VII. Fidelidad al dato** | ✅ | FR-010/FR-011: respuestas solo desde resultados de consulta |
| **VIII. Simplicidad** | ✅ | Una herramienta con parámetro `vista`; reutiliza patrones de la 002 |

**Post-design re-check**: ✅ Todos los gates pasan. La excepción al tope de 50 filas (FR-008) NO es violación de constitución: es un parámetro de configuración acotado y justificado por el caso de uso (listados contractuales completos), documentado en `research.md` (R4). Sin entradas en Complexity Tracking.

## Project Structure

### Documentation (this feature)

```text
specs/005-contratos-unicliente-detalle/
├── spec.md
├── plan.md                              # Este archivo
├── research.md                          # Phase 0
├── data-model.md                        # Phase 1
├── quickstart.md                        # Phase 1
├── contracts/
│   └── contratos-unicliente-tool.md     # Catálogo de la herramienta nueva
└── tasks.md                             # (/speckit-tasks — pendiente)
```

### Source Code (cambios previstos)

```text
app/
├── Models/Commercial/
│   ├── PuntoSuministro.php          # NUEVO — T_PuntoSuministro
│   ├── CupsElectrico.php            # NUEVO — T_CUPsElectrico
│   ├── CupsGas.php                  # NUEVO — T_CUPsGas
│   ├── TarifaElectrica.php          # NUEVO — T_TarifaElectrica
│   ├── TarifaGas.php                # NUEVO — T_TarifaGas
│   ├── Producto.php                 # NUEVO — T_Producto
│   ├── AnexoProducto.php            # NUEVO — T_AnexoProducto
│   ├── PropuestaComercialCups.php   # EDITAR — añadir relaciones suministro/energía
│   └── Cliente.php                  # EDITAR — añadir relación localidadSocial (CodLocSoc)
├── Services/Agent/
│   └── AgentCommercialQueryService.php   # EDITAR — método listar_contratos_unicliente + serializador
config/
└── agent.php                             # EDITAR — registrar herramienta + límite dedicado
tests/
└── Feature/Agent/
    └── ContratosUniclienteQueryTest.php  # NUEVO
```

**Structure Decision**: Feature exclusivamente backend. Los modelos nuevos viven en `app/Models/Commercial/` con `$connection = 'agent_db_secondary'`, `$timestamps = false`, siguiendo el patrón de los modelos de la feature 002.

## Architecture

```mermaid
sequenceDiagram
    participant U as Operador
    participant O as AgentOrchestrator
    participant H as AgentDatabaseQueryCapabilityHandler
    participant S as AgentCommercialQueryService
    participant DB as agent_db_secondary
    participant L as LLM

    U->>O: POST /chat/message ("lista contratos Unicliente de Madrid")
    O->>H: capability agent_database (+ historial 004)
    H->>L: turn 1 — elegir herramienta
    L-->>H: {query_name: "listar_contratos_unicliente", parameters: {...}}
    H->>S: execute(listar_contratos_unicliente, params)
    S->>DB: Eloquent PropuestaComercialCups + with(...) WHERE TipProCom IN (1,2)
    DB-->>S: filas (sin tope de 50; límite dedicado/agrupado)
    S-->>H: JSON filas (vista listado o detalle)
    H->>L: turn 2 — interpretar resultados (Markdown ES)
    L-->>H: respuesta natural
    H-->>U: mensaje chat
```

## Implementation Phases (for /speckit-tasks)

### Phase A — Modelos comerciales nuevos (prerequisito)

1. Crear 7 modelos en `app/Models/Commercial/` (`PuntoSuministro`, `CupsElectrico`, `CupsGas`, `TarifaElectrica`, `TarifaGas`, `Producto`, `AnexoProducto`) con `$connection`, `$table`, `$primaryKey`, `$timestamps = false` y casts de fechas.
2. Añadir relaciones en `PropuestaComercialCups`: `puntoSuministro()`, `cupsElectrico()`, `cupsGas()`, `tarifaElectrica()`, `tarifaGas()`, `producto()`, `anexoProducto()`.
3. Añadir relación `localidadSocial()` en `Cliente` (`CodLocSoc` → `T_Localidad`) para `direccionCliente`/`localidadCliente` (domicilio social), distinta de la `localidad()` existente (`CodLocFis`).

**FR**: FR-006, FR-014

### Phase B — Servicio de consulta

1. Implementar `listarContratosUnicliente(array $parameters)` en `AgentCommercialQueryService`, añadirlo al `match()` de `execute()`.
2. Base: `PropuestaComercialCups` con `whereHas('propuestaComercialCliente.propuestaComercial', fn($q) => $q->whereIn('TipProCom', [1,2]))` y `with([...])` (cliente + localidadSocial, puntoSuministro.localidad, cupsElectrico, cupsGas, tarifaElectrica, tarifaGas).
3. Resolver datos eléctricos vs gas según `TipCups` (1=eléctrico, 2=gas).
4. Filtros parametrizados (ver contrato): `tipo`, `termino`, `direccion_suministro`, `cups`, `tarifa`, `tipo_energia`, `localidad`, `fec_desde`, `fec_hasta`, `orden`, `limite`, `vista`.
5. Orden por `FecProCom` desc por defecto (FR-003).
6. **Sin tope de 50**: aplicar `agent.agent_database.contratos_unicliente.max_rows` (alto) y, en `vista=listado`, agrupar por `CodProCom` (CUPs anidados); marcar `truncated` solo si se supera el límite dedicado (FR-008/FR-009).
7. Serializador con las columnas de salida de FR-006 (alias del negocio).

**FR**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009

### Phase C — Handler y config

1. Registrar `listar_contratos_unicliente` en `config/agent.php` → `allowed_queries` (description + parameters; sin SQL).
2. Añadir clave de límite dedicada `agent.agent_database.contratos_unicliente.max_rows`.
3. Reforzar pistas de enrutado: en `IntentClassifierService::systemPrompt()` y, opcional, patrón determinista en `AgentDatabaseQueryCapabilityHandler::tryForceAnalyticQueryFromMessage()` para "unicliente", "unipunto", "multipuntos".
4. Asegurar que `interpretResultsAndRespond()` formatea bien filas anidadas (vista listado) y filas planas (vista detalle).
5. Invalidar cache: `php artisan cache:forget agent_database_queries_catalog`.

**FR**: FR-002, FR-013, FR-015

### Phase D — Tests y verificación

1. Tests PHPUnit del servicio: filtros por cada columna (cliente, NIF, dirección, teléfono, correo, punto de suministro, localidad, CUPS, tarifa), eléctrico vs gas, `TipProCom` 1 y 2 incluidos / 3 excluido, orden por fecha, sin resultados, y verificación de que **no** se trunca a 50.
2. Manual: escenarios de `quickstart.md`.

**FR**: SC-001, SC-002, SC-003, SC-004, SC-005, SC-006

## Tool Catalog (summary)

Detalle completo en [contracts/contratos-unicliente-tool.md](./contracts/contratos-unicliente-tool.md).

| Tool | Priority | User Story |
|------|----------|------------|
| `listar_contratos_unicliente` (modo `listado`) | P1 | US1, US2 |
| `listar_contratos_unicliente` (modo `detalle`) | P2 | US3 |

## Phase 0 / Research

Ver [research.md](./research.md) — decisiones de mapeo `TipProCom`, titular, granularidad, ausencia de tope de 50 filas, joins eléctricos/gas y localidad social.

## Phase 1 / Design

- Entidades y relaciones: [data-model.md](./data-model.md)
- Contrato de la herramienta: [contracts/contratos-unicliente-tool.md](./contracts/contratos-unicliente-tool.md)
- Pruebas manuales: [quickstart.md](./quickstart.md)

## Complexity Tracking

> Sin violaciones de constitución que requieran justificación. La exención del tope de 50 filas es un parámetro de configuración acotado (R4), no una violación.

## Next Steps

1. `/speckit-tasks` → generar `tasks.md` ordenado por dependencias
2. `/speckit-implement` → modelos, servicio, config, tests
