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

**Feature**: `006-contratos-conteo-agrupado` | **Date**: 2026-05-28

Resuelve los NEEDS CLARIFICATION del plan. Cada decisión incluye Decisión / Justificación / Alternativas.

---

## R1 — Agregación en BD vs listar y contar en memoria

**Decisión**: Calcular `COUNT`/`SUM`/`AVG`/`MIN`/`MAX` con `GROUP BY` directamente en la base de datos (Eloquent `selectRaw` + `groupBy`), devolviendo solo el resumen agrupado.

**Justificación**: El bug reportado ("respuesta truncada") ocurre porque el LLM recibe un listado limitado (50/500 filas) y agrega sobre ese subconjunto, dando totales incorrectos y avisando de truncado. Agregar en BD considera **todos** los registros y el resultado (decenas de grupos) cabe sin truncar (FR-001, FR-002, SC-001, SC-002).

**Alternativas**: (a) Subir el límite de filas y seguir agregando en el LLM → sigue siendo inexacto y caro. (b) Materializar vistas → sobre-ingeniería para el alcance.

---

## R2 — Granularidad: contar contratos vs filas de CUPS (CRÍTICO)

**Decisión**: La base de la consulta es `PropuestaComercialCups` (una fila por punto de suministro). Para **contar contratos** se usa `COUNT(DISTINCT CodProCom)`. Para **medidas numéricas** (consumo, potencia) se agrega sobre las filas de CUPS (`SUM/AVG/... ` directo), porque esas magnitudes existen a nivel de punto de suministro.

**Justificación**: Un contrato MultiPuntos (`TipProCom=2`) tiene varias filas de CUPS; `COUNT(*)` sobre CUPS sobre-contaría contratos. `COUNT(DISTINCT CodProCom)` cuenta contratos reales. En cambio, "consumo eléctrico total por localidad" debe sumar el consumo de **cada** punto de suministro, por lo que la fila-CUPS es la granularidad correcta para medidas.

**Alternativas**: Basar todo en `T_PropuestaComercial` → impediría medidas por punto de suministro y dimensiones de suministro (localidad/dirección/tarifa). Rechazada.

**Implicación**: Cuando se agrupa por una dimensión **de suministro** (localidad de suministro, tarifa, dirección) un contrato MultiPuntos puede aparecer en varios grupos; el `COUNT(DISTINCT CodProCom)` por grupo es correcto a nivel de grupo, pero la suma de grupos puede exceder el número de contratos distintos. Se documenta: el "total general" para COUNT se reporta como contratos distintos globales y, en dimensiones de suministro, se aclara que un contrato puede contar en varias localidades. (Se refleja en quickstart SC-001 usando dimensiones por-contrato para la verificación de suma exacta.)

---

## R3 — Lista blanca de dimensiones (`agrupar_por`)

**Decisión**: Conjunto cerrado de dimensiones, cada una mapeada a columna/relación o expresión de fecha:

| Clave (`agrupar_por`) | Origen | Cardinalidad | Nivel |
|-----------------------|--------|--------------|-------|
| `localidad_suministro` | `puntoSuministro.localidad.DesLoc` | media | suministro |
| `localidad_cliente` | `cliente.localidadSocial.DesLoc` | media | contrato |
| `provincia_suministro` | `puntoSuministro.localidad.provincia.DesPro` | baja | suministro |
| `provincia_cliente` | `cliente.localidadSocial.provincia.DesPro` | baja | contrato |
| `direccion_suministro` | `puntoSuministro.NomViaPunSum` | **alta** | suministro |
| `mes_contrato` | `YEAR(FecProCom)`+`MONTH(FecProCom)` | baja | contrato |
| `anio_contrato` | `YEAR(FecProCom)` | baja | contrato |
| `tarifa_electrica` | `tarifaElectrica.NomTarEle` | baja | suministro |
| `tarifa_gas` | `tarifaGas.NomTarGas` | baja | suministro |
| `tipo_energia` | `TipCups` (1=eléctrico, 2=gas) | baja | suministro |
| `tipo_contrato` | `TipProCom` (1=UniPunto, 2=MultiPuntos) | baja | contrato |
| `cliente` | `cliente.NumCifCli` (+ `NomComCli`) | **alta** | contrato |
| `estado` | `propuesta.EstProCom` | baja | contrato |
| `bucket_consumo_electrico` | rangos de `ConCup` (ver R6) | baja | suministro |
| `bucket_consumo_gas` | rangos de `CauDiaGas` (ver R6) | baja | suministro |

**Justificación**: Cubre "contar por cada columna" del usuario sin permitir SQL arbitrario (FR-003, FR-004). Máximo **2** dimensiones combinables (FR-005).

**Alternativas**: Permitir cualquier nombre de columna del LLM → riesgo de inyección/errores; rechazada por Principio III/VIII.

---

## R4 — Lista blanca de medidas numéricas y operaciones

**Decisión**: Operaciones `count | suma | promedio | minimo | maximo`. Para todas menos `count` se requiere una **medida** de la lista blanca:

| Clave (`medida`) | Columna | Descripción |
|------------------|---------|-------------|
| `consumo_electrico` | `ConCup` | Consumo eléctrico del punto |
| `consumo_gas` | `CauDiaGas` | Caudal/consumo de gas |
| `potencia_p1` … `potencia_p6` | `PotEleConP1` … `PotEleConP6` | Potencias contratadas por periodo |

**Justificación**: Da la "capacidad de operaciones matemáticas" pedida sobre las columnas numéricas reales del dominio (FR-016). `count` no necesita medida (cuenta contratos, R2).

**Alternativas**: Permitir operar sobre cualquier columna → SUM/AVG sobre texto o IDs no tiene sentido; se rechaza con sugerencia (FR-018).

---

## R5 — Varias medidas a la vez y total general

**Decisión**: Soportar un parámetro `metricas[]` (lista de `{operacion, medida}`) además del par simple `operacion`/`medida`, devolviendo todas las medidas por grupo. Siempre se incluye un **total/agregado general** (sin agrupar) junto a los grupos.

**Justificación**: Consultas como "suma y promedio de consumo por tarifa" en una sola respuesta (FR-019); el total general permite verificar coherencia (FR-015).

**Alternativas**: Una medida por llamada → más turnos y peor UX.

---

## R6 — Agrupar POR una columna numérica (buckets)

**Decisión**: Para usar una medida numérica como **dimensión** (distribución), se agrupa por **rangos predefinidos**, no por el valor crudo. Buckets por defecto (configurables a futuro):

- `bucket_consumo_electrico` (`ConCup`, kWh): `[0–1.000)`, `[1.000–5.000)`, `[5.000–15.000)`, `[15.000–50.000)`, `≥50.000`, `Sin dato`.
- `bucket_consumo_gas` (`CauDiaGas`): `[0–500)`, `[500–2.000)`, `[2.000–10.000)`, `≥10.000`, `Sin dato`.

**Justificación**: Agrupar por el valor numérico crudo produciría miles de grupos casi únicos; los buckets dan una distribución legible (FR-021). Las potencias P1–P6 se tratan por defecto como **medidas** (no como dimensión bucketizada) salvo extensión futura.

**Alternativas**: Buckets dinámicos (cuantiles) → más complejo; se difiere.

---

## R7 — Manejo de nulos

**Decisión**: En `AVG`/`SUM`/`MIN`/`MAX`, los nulos se **excluyen** del cálculo (comportamiento estándar SQL; no cuentan como cero). En dimensiones, los valores nulos/vacíos se agrupan bajo la etiqueta **"Sin dato"**. El `COUNT(DISTINCT CodProCom)` general nunca excluye contratos por nulos en columnas de medida.

**Justificación**: Un promedio que trate nulos como cero estaría sesgado; agrupar nulos como "Sin dato" evita perder registros del recuento (FR-008, FR-020).

---

## R8 — Top-N para alta cardinalidad (sin truncar el cálculo)

**Decisión**: Para dimensiones de alta cardinalidad (`direccion_suministro`, `cliente`), ordenar por el agregado desc y limitar la **presentación** con `limite` (default p. ej. 20), indicando "hay más". El cálculo agrega **todos** los registros y `truncated` permanece `false`.

**Justificación**: Evita reproducir el problema de volumen sin sacrificar exactitud del conteo (FR-006, US4, SC-005).

**Alternativas**: Devolver todos los grupos → respuesta inmanejable y vuelta al truncado.

---

## R9 — Enrutado e integración

**Decisión**: Reutilizar `AgentDatabaseQueryCapabilityHandler` + protocolo de 2 turnos. Añadir pistas en `IntentClassifierService` para "cuántos/distribución de contratos por…", "consumo/potencia total/medio por…". Invalidar `agent_database_queries_catalog` tras editar config.

**Justificación**: Coherencia con 002/005; sin nuevos endpoints (FR-012).

---

## R10 — Alcance de tipos de contrato

**Decisión**: `TipProCom ∈ {1, 2}` (Unicliente UniPunto y MultiPuntos), igual que la feature 005. `TipProCom = 3` (MultiCliente MultiPunto) fuera de alcance.

**Justificación**: Reutiliza el mapa relacional de la 005; el caso 3 tiene una consulta distinta (pendiente de otra feature).
