# Research: Contratos Unicliente (UniPunto y MultiPuntos)

**Feature**: `005-contratos-unicliente-detalle` | **Connection**: `agent_db_secondary`

Todas las NEEDS CLARIFICATION quedaron resueltas con el operador en sesión de aclaración (2026-05-28).

## R1 — Mapeo de `TipProCom`

- **Decisión**: `TipProCom = 1` → **Unicliente UniPunto**; `TipProCom = 2` → **UniCliente MultiPuntos**; `TipProCom = 3` → **MultiCliente MultiPunto** (FUERA DE ALCANCE). La herramienta cubre `TipProCom IN (1, 2)` (equivalente al `WHERE TipProCom != 3` de la SQL de referencia).
- **Rationale**: Confirmado explícitamente por el operador. La feature 002 solo contemplaba `TipProCom` 2 y 3; esta feature incorpora el valor 1 al dominio modelado.
- **Alternativas consideradas**: Distinguir Unicliente/MultiPuntos por número de CUPs (rechazada: el operador confirmó que es un valor de `TipProCom`, no un conteo).

## R2 — Resolución de titular

- **Decisión**: Para `TipProCom` 1 y 2, el titular es **siempre un cliente empresa** (`T_Cliente`), igual que el `join T_Cliente` de la SQL de referencia. La resolución vía `Contacto` (`T_ContactoCliente`) aplica solo a `TipProCom=3`, fuera de alcance.
- **Rationale**: La SQL une directamente `T_Propuesta_Comercial_Clientes` → `T_Cliente`; el operador confirmó que en estos contratos el titular vive en `T_Cliente`.
- **Compatibilidad**: No colisiona con la lógica de la 002 (que mapea 2→Cliente, 3→Contacto); el valor 1 se suma con resolución contra Cliente.

## R3 — Nodo central y granularidad

- **Decisión**: El nodo central de la consulta es **`PropuestaComercialCups`** (`T_Propuesta_Comercial_CUPs`), que produce **una fila por punto de suministro (CUPS)**. La herramienta soporta dos vistas:
  - `vista = listado` (default): agrupado por contrato (`CodProCom`), con sus CUPS anidados; ordenado por `FecProCom` desc.
  - `vista = detalle`: una fila por CUPS (reproduce la SQL original 1:1).
- **Rationale**: El operador eligió "que dependa de la pregunta" (listados = por contrato; detalle = por CUP).
- **Alternativas consideradas**: Solo por contrato (pierde detalle de cada punto) o solo por CUP (listados ruidosos). Rechazadas en favor del parámetro `vista`.

## R4 — Sin tope de 50 filas (FR-008)

- **Decisión**: Esta herramienta **NO** usa `agent.agent_database.max_results` (50). Se introduce una clave dedicada `agent.agent_database.contratos_unicliente.max_rows` con un valor alto (propuesta: **500**, configurable por env). En `vista=listado` se agrupa por contrato para mantener legibilidad; si se supera el límite dedicado, se marca `truncated: true` y el asistente lo indica (FR-009).
- **Rationale**: El tope de 50 fue pensado para consultas resumidas; en listados contractuales ocultaría contratos válidos. El operador pidió explícitamente eliminar ese tope para esta herramienta.
- **Alternativas consideradas**:
  - Sin límite alguno (rechazada: riesgo de respuestas enormes y coste de tokens del LLM).
  - Reutilizar 50 (rechazada: contradice FR-008).
- **Salvaguarda**: El límite dedicado evita degradación; la agrupación por contrato reduce el volumen percibido sin descartar contratos.

## R5 — Joins eléctrico vs gas según `TipCups`

- **Decisión**: Relaciones condicionadas por `TipCups` en `PropuestaComercialCups`:
  - `TipCups = 1` (eléctrico): `cupsElectrico()` (`CodCup` → `T_CUPsElectrico.CodCupsEle`) y `tarifaElectrica()` (`CodTar` → `T_TarifaElectrica.CodTarEle`). Aporta `CUPsEle`, `NomTarEle`, `PotEleConP1..P6`, `ConCup` (consumo eléctrico).
  - `TipCups = 2` (gas): `cupsGas()` (`CodCup` → `T_CUPsGas.CodCupGas`) y `tarifaGas()` (`CodTar` → `T_TarifaGas.CodTarGas`). Aporta `CupsGas`, `NomTarGas`, `CauDiaGas` (consumo gas).
- **Rationale**: Reproduce los `left join ... and tpcc2.TipCups = 1|2` de la SQL de referencia. Los campos no aplicables a la energía de la línea quedan vacíos (FR-006, edge cases).

## R6 — Localidad del cliente (domicilio social)

- **Decisión**: `direccionCliente` = `T_Cliente.NomViaDomSoc`; `localidadCliente` = `T_Localidad.DesLoc` vía `T_Cliente.CodLocSoc`. Se añade relación **`localidadSocial()`** en `Cliente` (`CodLocSoc` → `CodLoc`), distinta de la `localidad()` existente que usa `CodLocFis`.
- **Rationale**: La SQL usa `tc.CodLocSoc` y `tc.NomViaDomSoc` (domicilio social), no el domicilio físico. El modelo `Cliente` actual solo tiene la relación física.

## R7 — `T_Producto` (inner) y `T_AnexoProducto` (left)

- **Decisión**: Modelar `producto()` (belongsTo `T_Producto` por `CodPro`) y `anexoProducto()` (belongsTo `T_AnexoProducto` por `CodAnePro`, opcional). El `join T_Producto` de la SQL es **inner** y no aporta columnas de salida; se reproduce fielmente exigiendo producto existente vía `whereHas('producto')`. `T_AnexoProducto` es `left join` (opcional), sin restringir filas.
- **Rationale**: Fidelidad a la SQL de referencia. El inner join sobre producto es intencional en la consulta original.
- **Riesgo / nota**: Si existieran CUPS sin producto asociado, el inner join los excluye (igual que la SQL original). Documentado para revisión en pruebas; si en datos reales se observan exclusiones no deseadas, reconsiderar a `leftJoin` en una iteración.

## R8 — Reutilización de infraestructura existente

- **Decisión**: Reutilizar `AgentDatabaseQueryCapabilityHandler` (protocolo JSON de 2 turnos), `DatabaseSchemaService` (catálogo desde config) y el contexto conversacional de la feature 004 (`ConversationHistoryService`) para seguimientos (FR-013).
- **Rationale**: Minimiza superficie de cambio (Constitución VIII). Solo se añade una herramienta y modelos; no se toca el flujo del orquestador.
