# Implementation Plan: Agregación de contratos por cualquier columna (conteo + operaciones matemáticas)

**Branch**: `006-contratos-conteo-agrupado` | **Date**: 2026-05-28 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `/specs/006-contratos-conteo-agrupado/spec.md`

## Summary

Añadir al catálogo `agent_database` una **herramienta de agregación** sobre contratos Unicliente (`TipProCom ∈ {1,2}`) que calcula resultados **en la base de datos** (no listando y contando en memoria), soportando:

- **COUNT** de contratos agrupado por **cualquier columna de una lista blanca** (localidad de suministro, localidad de cliente, provincia, dirección de suministro, mes/año de contrato, tarifa eléctrica/gas, tipo de energía, tipo de contrato, cliente, estado).
- **Operaciones matemáticas** (`SUM`, `AVG`, `MIN`, `MAX`) sobre **columnas numéricas** (consumo eléctrico, consumo de gas, potencias P1–P6), opcionalmente agrupadas por una dimensión.
- Combinación de **hasta dos dimensiones** (p. ej. localidad + mes).
- **Top-N** por total para dimensiones de alta cardinalidad, **sin** dejar de calcular sobre todos los registros.

Esto resuelve el problema de "respuesta truncada": hoy el LLM agrupa/suma sobre un listado limitado (50/500 filas), produciendo totales incorrectos. La nueva herramienta hace `GROUP BY` + agregación en SQL vía Eloquent y **nunca** marca el resultado como truncado (el límite solo acota la *presentación* de grupos).

Reutiliza el mapa relacional de la feature **005** (modelos `PropuestaComercialCups`, `PuntoSuministro`, `CupsElectrico/Gas`, `TarifaElectrica/Gas`, `Cliente.localidadSocial`).

## Technical Context

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

**Primary Dependencies**: Eloquent (conexión `agent_db_secondary`), `AgentDatabaseQueryCapabilityHandler`, `AgentCommercialQueryService`, `DatabaseSchemaService`, `LlmChatCompletionContract`, `ConversationHistoryService` (contexto 004). Modelos y relaciones de la feature **005** (dependencia directa).

**Storage**: BD secundaria — tablas existentes `T_PropuestaComercial`, `T_Propuesta_Comercial_Clientes`, `T_Propuesta_Comercial_CUPs`, `T_Cliente`, `T_Localidad`, `T_Provincia`, `T_PuntoSuministro`, `T_TarifaElectrica`, `T_TarifaGas`. Sin tablas ni migraciones nuevas.

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

**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/005

**Performance Goals**: Respuesta end-to-end < 30 s en local (SC-001); la agregación es una sola consulta `GROUP BY` indexable

**Constraints**: Solo lectura (FR-011); `TipProCom ∈ {1, 2}` (excluir 3); **sin tope de filas** en el cálculo (FR-002) — el límite solo acota presentación Top-N; dimensiones y medidas restringidas a **lista blanca** (FR-004, FR-018)

**Scale/Scope**: 1 herramienta nueva (`agregar_contratos_unicliente`) parametrizable; ~13 dimensiones y ~8 medidas numéricas en lista blanca; reutiliza modelos de la 005 (cero modelos nuevos, posible relación `provincia` reutilizada)

## 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-021, US1–US6; 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 |
| **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-009/FR-010: respuestas solo desde agregados calculados; sin invención |
| **VIII. Simplicidad** | ✅ | UNA herramienta parametrizada (lista blanca) en vez de una por columna |

**Post-design re-check**: ✅ Todos los gates pasan. La lista blanca de dimensiones/medidas (FR-004/FR-018) garantiza que el LLM no construye SQL arbitrario, respetando III y VIII. La ausencia de tope en el cálculo (FR-002) no es violación: la agregación se hace en BD y solo se acota la presentación. Sin entradas en Complexity Tracking.

## Project Structure

### Documentation (this feature)

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

### Source Code (cambios previstos)

```text
app/
├── Models/Commercial/
│   └── Localidad.php                     # EDITAR (si falta) — relación provincia() para dimensión provincia
├── Services/Agent/
│   └── AgentCommercialQueryService.php   # EDITAR — método agregarContratosUnicliente() + whitelists + buckets
config/
└── agent.php                             # EDITAR — registrar herramienta agregar_contratos_unicliente
tests/
└── Feature/Agent/
    └── ContratosAgregadoQueryTest.php    # NUEVO
```

**Structure Decision**: Feature exclusivamente backend, sin modelos ni migraciones nuevas (reutiliza los de la 005). La lógica de agregación vive en `AgentCommercialQueryService`; las listas blancas de dimensiones/medidas y los buckets numéricos se definen como mapas privados del servicio (no en config con SQL).

## 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 ("¿cuántos contratos por localidad y mes?")
    O->>H: capability agent_database (+ historial 004)
    H->>L: turn 1 — elegir herramienta
    L-->>H: {query_name: "agregar_contratos_unicliente", parameters: {operacion:"count", agrupar_por:["localidad_suministro","mes_contrato"]}}
    H->>S: execute(agregar_contratos_unicliente, params)
    S->>DB: Eloquent GROUP BY (dims whitelist) + COUNT(DISTINCT CodProCom) / SUM(medida) WHERE TipProCom IN (1,2)
    DB-->>S: grupos + agregados (TODOS los registros)
    S-->>H: {rows:[...], total_general, truncated:false}
    H->>L: turn 2 — interpretar agregados (Markdown ES)
    L-->>H: respuesta natural (sin "lista truncada")
    H-->>U: mensaje chat
```

## Key design decisions (detalle en research.md)

- **Granularidad del COUNT (crítico)**: la base de agregación es `PropuestaComercialCups` (una fila por punto de suministro). Para **contar contratos** se usa `COUNT(DISTINCT CodProCom)` (no `COUNT(*)` de CUPS), evitando sobre-conteo en contratos MultiPuntos. Para **medidas numéricas** (consumo/potencia, que viven a nivel de CUPS) la agregación se hace sobre las filas de CUPS, que es lo correcto.
- **Lista blanca de dimensiones** (`agrupar_por`, máx. 2) → cada clave mapea a una columna/relación concreta y/o expresión de fecha. Claves fuera de la lista se rechazan (FR-004).
- **Lista blanca de medidas numéricas** (`medida`) + **operaciones** (`count|suma|promedio|minimo|maximo`). Operación numérica sobre columna no numérica → rechazo (FR-018).
- **Nulos**: excluidos de AVG/SUM (no como cero); el COUNT de contratos siempre refleja todos los contratos del criterio (FR-020).
- **Dimensión numérica por buckets**: agrupar **por** una medida numérica usa rangos predefinidos (no el valor crudo) (FR-021); buckets por defecto en research.md.
- **Top-N**: para dimensiones de alta cardinalidad (`direccion_suministro`, `cliente`) se ordena por el agregado desc y se limita la **presentación** (`limite`), marcando "hay más"; el cálculo cubre todos los registros y `truncated` permanece `false` (FR-006).
- **Total general**: además de los grupos, se devuelve el agregado global para verificación (FR-015).

## Implementation Phases (for /speckit-tasks)

### Phase A — Soporte de modelo (prerequisito mínimo)

1. Verificar/añadir relación `provincia()` en `Localidad` y `provincia` accesible desde `puntoSuministro.localidad` y `cliente.localidadSocial` (para dimensiones `provincia_suministro` / `provincia_cliente`). Reutiliza modelos de la 005; **sin** tablas nuevas.

**FR**: FR-003, FR-013

### Phase B — Servicio de agregación

1. Implementar `agregarContratosUnicliente(array $parameters): array` en `AgentCommercialQueryService` y añadirlo al `match()` de `execute()`.
2. Definir mapas privados (whitelists): `dimensionesContratoUnicliente()` (clave → columna/relación o expresión de fecha) y `medidasNumericasContratoUnicliente()` (clave → columna `ConCup`, `CauDiaGas`, `PotEleConP1..6`).
3. Base de consulta sobre `PropuestaComercialCups` con `whereHas(propuestaComercialCliente.propuestaComercial, TipProCom IN [1,2])` + joins/`with` necesarios según dimensiones/filtros pedidos.
4. Resolver `operacion`:
   - `count` → `COUNT(DISTINCT CodProCom)`.
   - `suma|promedio|minimo|maximo` → `SUM/AVG/MIN/MAX(<medida>)` con la medida validada contra la lista blanca; excluir nulos del cálculo.
   - Soportar `metricas[]` (varias operaciones/medidas en una sola consulta — FR-019).
5. `agrupar_por` (0–2 dims): construir `GROUP BY` con las expresiones de la whitelist; etiquetar nulos como "Sin dato"; soportar buckets para dimensión numérica.
6. Filtros previos coherentes con la 005: `tipo`, `termino`, `tipo_energia`, `localidad`, `fec_desde`, `fec_hasta` (FR-007).
7. Orden por agregado desc por defecto; aplicar `limite` (Top-N) solo a la **presentación**; `truncated` siempre `false` en agregados (FR-002, FR-006).
8. Devolver `{ rows, total_general, operacion, medida, agrupar_por, filtros_aplicados, truncated:false, count }`.
9. Rechazos: dimensión/medida no permitida → `agent_query_invalid_params`; operación numérica sin medida o sobre no-numérica → `agent_query_invalid_params` con sugerencia (FR-004, FR-018).

**FR**: FR-001, FR-002, FR-003, FR-004, FR-005, FR-006, FR-007, FR-008, FR-009, FR-015, FR-016, FR-017, FR-018, FR-019, FR-020, FR-021

### Phase C — Handler y config

1. Registrar `agregar_contratos_unicliente` en `config/agent.php` → `allowed_queries` (description + parameters; sin SQL; enumerar dimensiones y medidas válidas en la descripción para el LLM).
2. Reforzar pistas de enrutado: en `IntentClassifierService::systemPrompt()` para "cuántos contratos por…", "número/distribución de contratos por…", "consumo/potencia total/medio por…"; opcional patrón determinista en el handler.
3. Asegurar que `interpretResultsAndRespond()` formatea bien grupos (incl. dos dimensiones) y comunica Top-N ("hay más") y el total general, **sin** lenguaje de "lista truncada".
4. Invalidar cache del catálogo: `php artisan cache:forget agent_database_queries_catalog`.

**FR**: FR-009, FR-010, FR-012, FR-013, FR-014, FR-015

### Phase D — Tests y verificación

1. Tests PHPUnit del servicio (sin BD real, validando construcción/whitelists): registro de la herramienta; rechazo de dimensión no permitida; rechazo de operación numérica sobre columna no numérica; resolución de operaciones (count/suma/promedio/min/max); COUNT por contrato usa DISTINCT `CodProCom`; agrupación por 1 y 2 dimensiones; buckets de medida numérica; `truncated` siempre `false`; etiqueta "Sin dato" para nulos.
2. Manual: escenarios de `quickstart.md` contra BD operativa (verificar que la suma de grupos == total general).

**FR**: SC-001..SC-008

## Tool Catalog (summary)

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

| Tool | Priority | User Story |
|------|----------|------------|
| `agregar_contratos_unicliente` (operacion=count, agrupar_por) | P1 | US1, US2, US3 |
| `agregar_contratos_unicliente` (operacion=suma/promedio/min/max) | P1 | US6 |
| `agregar_contratos_unicliente` (Top-N alta cardinalidad) | P2 | US4 |
| `agregar_contratos_unicliente` (+ filtros) | P2 | US5 |

## Phase 0 / Research

Ver [research.md](./research.md) — granularidad COUNT contrato vs CUPS, lista blanca de dimensiones y medidas, operaciones soportadas, manejo de nulos, buckets numéricos por defecto, Top-N y ausencia de truncado.

## Phase 1 / Design

- Entidades, dimensiones y medidas: [data-model.md](./data-model.md)
- Contrato de la herramienta: [contracts/agregar-contratos-tool.md](./contracts/agregar-contratos-tool.md)
- Pruebas manuales: [quickstart.md](./quickstart.md)

## Complexity Tracking

> Sin violaciones de constitución que requieran justificación. La herramienta parametrizada con lista blanca es más simple y segura que crear una herramienta por columna (VIII).

## Next Steps

1. `/speckit-tasks` → generar `tasks.md` ordenado por dependencias
2. `/speckit-implement` → servicio (whitelists + agregación), config, tests
