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

**Feature Branch**: `006-contratos-conteo-agrupado`

**Created**: 2026-05-28

**Status**: Draft

**Input**: User description: "Procede con la herramienta de conteo agregado, con la capacidad de contar por cada columna (localidad, dirección de suministro, tarifa, mes, tipo, cliente, etc.). El agente debe contar TODOS los registros existentes (sin truncar a un máximo de filas) y responder agrupando por la columna que pida el usuario. Además quiero que alcance columnas numéricas (consumos, potencias) y tenga capacidad de operaciones matemáticas (suma, promedio, mínimo, máximo)."

> **Constitution (I. Spec-First)**: Este spec MUST ser agnóstico de stack.
> No mencionar Laravel, Angular ni rutas de código. El CÓMO va en `plan.md`.

> **Problema que resuelve**: Hoy, cuando el usuario pide "cuántos contratos por localidad y mes", el asistente **lista** contratos (limitados a un máximo de filas) y luego cuenta/suma sobre ese subconjunto, devolviendo totales **incorrectos** y un aviso de "lista truncada". Esta feature mueve el cálculo a una **operación de agregación en base de datos** que considera todos los registros y devuelve solo el resumen agrupado, soportando tanto **conteos** (COUNT) como **operaciones matemáticas sobre columnas numéricas** (SUM, AVG, MIN, MAX).

> **Término de dominio**: «contrato» = propuesta comercial. El alcance se mantiene en contratos **Unicliente** (`TipProCom = 1` UniPunto y `TipProCom = 2` MultiPuntos), coherente con la feature `005`; `TipProCom = 3` (MultiCliente MultiPunto) queda **fuera de alcance**.

## User Scenarios & Testing *(mandatory)*

### User Story 1 - Contar contratos agrupados por una columna (Priority: P1)

Como operador autenticado, quiero preguntar "¿cuántos contratos hay por [columna]?" (por ejemplo, por localidad, por tarifa o por tipo de contrato) y recibir un cuadro con el **total exacto** por cada valor, contando **todos los registros** existentes.

**Why this priority**: Es el valor central de la feature: sustituir el "listar y contar a mano" (truncado e inexacto) por un conteo agregado exacto.

**Independent Test**: El operador pregunta "¿cuántos contratos por localidad?" y recibe una tabla con cada localidad y su total, cuya suma coincide con el total real de contratos (no con un subconjunto de 50/500 filas).

**Acceptance Scenarios**:

1. **Given** existen más contratos de los que cabrían en un listado limitado, **When** el operador pide el conteo agrupado por una columna, **Then** los totales reflejan **todos** los registros y la respuesta **no** indica "lista truncada".
2. **Given** una columna de agrupación válida, **When** el operador la solicita, **Then** el asistente devuelve un resultado ordenado por total descendente por defecto.
3. **Given** no existen contratos que cumplan los filtros, **When** el operador pregunta, **Then** el asistente indica que no hay registros, sin inventar totales.

---

### User Story 2 - Contar por cualquier columna del resultado (Priority: P1)

Como operador, quiero poder agrupar el conteo por **cualquier columna disponible** del dominio de contratos Unicliente (localidad del punto de suministro, localidad del cliente, provincia, dirección del punto de suministro, mes/año de contrato, tarifa eléctrica o de gas, tipo de energía, tipo de contrato, cliente/NIF, estado), sin tener que conocer una herramienta distinta para cada criterio.

**Why this priority**: El usuario no debe depender de una herramienta por columna; una sola capacidad parametrizada debe cubrir todas las columnas permitidas.

**Independent Test**: El operador prueba varias agrupaciones ("por tarifa", "por tipo de energía", "por provincia") y cada una devuelve totales exactos por valor, usando la misma capacidad.

**Acceptance Scenarios**:

1. **Given** una columna permitida (lista blanca), **When** el operador la indica en lenguaje natural, **Then** el asistente agrupa por ella y devuelve totales por valor.
2. **Given** una columna **no permitida** o inexistente, **When** el operador la solicita, **Then** el asistente lo rechaza de forma segura y ofrece las columnas disponibles, sin ejecutar consultas arbitrarias.
3. **Given** una columna categórica con valores vacíos/nulos, **When** se agrupa por ella, **Then** los registros sin valor se agrupan bajo una etiqueta clara (p. ej. "Sin dato") sin perderse del total.

---

### User Story 3 - Combinar dos dimensiones (Priority: P2)

Como operador, quiero combinar **dos columnas** en el conteo (por ejemplo, "por localidad y por mes"), para obtener un cuadro cruzado con totales exactos.

**Why this priority**: Muchas consultas reales cruzan dos dimensiones (territorio × periodo); es la consulta que originó el problema de truncado.

**Independent Test**: "¿Cuántos contratos por localidad y mes?" devuelve filas (localidad, mes, total) con totales exactos por combinación.

**Acceptance Scenarios**:

1. **Given** dos columnas permitidas, **When** el operador pide agrupar por ambas, **Then** el asistente devuelve totales por cada combinación de valores.
2. **Given** más de dos dimensiones solicitadas, **When** el operador lo pide, **Then** el asistente limita a un máximo de dos (o pide concretar) para mantener legibilidad.

---

### User Story 4 - Manejo de columnas de alta cardinalidad (Priority: P2)

Como operador, quiero que al agrupar por una columna con muchísimos valores distintos (por ejemplo, dirección de suministro o cliente) el asistente me muestre un **Top-N por total** y me permita acotar, en lugar de una lista inmanejable, **sin** dejar de contar todos los registros.

**Why this priority**: Agrupar por columnas casi únicas (una fila por punto de suministro o por cliente) reproduce el problema de volumen; el conteo debe seguir siendo total pero la presentación acotada.

**Independent Test**: "¿Cuántos contratos por dirección de suministro?" devuelve los primeros N por total (orden desc) e indica que hay más, manteniendo el conteo basado en todos los registros.

**Acceptance Scenarios**:

1. **Given** una columna de alta cardinalidad, **When** el operador la solicita sin filtro, **Then** el asistente devuelve un Top-N por total descendente e indica que existen más valores.
2. **Given** un filtro previo (p. ej. una localidad concreta), **When** el operador lo añade, **Then** el conteo agrupado se restringe a ese filtro.
3. **Given** un parámetro de límite explícito, **When** el operador pide "top 20", **Then** el asistente respeta ese límite en la presentación.

---

### User Story 5 - Aplicar filtros antes de contar (Priority: P2)

Como operador, quiero poder **filtrar** (por cliente, tipo de contrato, tipo de energía, rango de fechas, localidad) y que el conteo agrupado se calcule **sobre el subconjunto filtrado**, manteniendo totales exactos.

**Why this priority**: El conteo suele acompañarse de un criterio ("contratos de gas por provincia", "contratos de 2026 por mes").

**Independent Test**: "¿Cuántos contratos de gas por provincia en 2026?" aplica filtros de energía y fecha y agrupa por provincia con totales exactos.

**Acceptance Scenarios**:

1. **Given** filtros válidos, **When** el operador los combina con una agrupación, **Then** el total por grupo refleja solo los registros que cumplen los filtros.
2. **Given** filtros que no dejan resultados, **When** el operador pregunta, **Then** el asistente informa que no hay registros para ese criterio.

---

### User Story 6 - Operaciones matemáticas sobre columnas numéricas (Priority: P1)

Como operador, quiero aplicar **operaciones matemáticas** (suma, promedio, mínimo, máximo) sobre columnas numéricas del contrato —consumo eléctrico, consumo de gas y potencias contratadas P1–P6— opcionalmente **agrupadas** por una dimensión, calculadas sobre **todos** los registros.

**Why this priority**: El usuario no solo quiere "cuántos", sino "cuánto": consumos y potencias agregados son métricas de negocio centrales (p. ej. "consumo eléctrico total por localidad", "potencia P1 media por tarifa").

**Independent Test**: "¿Cuál es el consumo eléctrico total por localidad?" devuelve, por localidad, la **suma** del consumo eléctrico sobre todos los contratos, más el total general; y "¿potencia P1 media por tarifa?" devuelve el **promedio** por tarifa.

**Acceptance Scenarios**:

1. **Given** una columna numérica permitida y una operación (suma/promedio/mín/máx), **When** el operador la solicita, **Then** el asistente devuelve el valor agregado calculado sobre todos los registros que cumplan los filtros.
2. **Given** una operación numérica **con** dimensión de agrupación, **When** el operador la pide, **Then** el asistente devuelve el valor agregado por cada valor de la dimensión más el agregado general.
3. **Given** valores nulos en la columna numérica, **When** se agrega, **Then** los nulos se tratan de forma consistente (no cuentan como cero en promedios; se documenta el criterio) sin romper el cálculo.
4. **Given** una columna **no numérica**, **When** el operador pide una operación matemática (p. ej. suma de localidad), **Then** el asistente lo rechaza y propone una operación válida (conteo) o columnas numéricas disponibles.
5. **Given** se piden varias medidas a la vez (p. ej. suma y promedio de consumo), **When** el operador lo solicita, **Then** el asistente devuelve cada medida en la misma respuesta.

---

### Edge Cases

- **Sin truncado en conteos**: el resultado de un conteo agrupado MUST reflejar todos los registros; nunca debe marcarse como "lista truncada".
- **Columna no permitida**: rechazo seguro con sugerencia de columnas disponibles; nunca ejecución de consultas arbitrarias.
- **Valores nulos/vacíos** en la columna agrupada: se agrupan bajo etiqueta clara sin perderse del total.
- **Alta cardinalidad** (dirección de suministro, cliente): Top-N por total + aviso de "hay más"; el conteo sigue siendo total.
- **Columnas numéricas como medida vs. dimensión**: por defecto, las columnas numéricas (consumos, potencias) se tratan como **medidas** sobre las que aplicar operaciones (SUM/AVG/MIN/MAX). Para **agrupar** por una columna numérica (distribución), se usan **rangos/buckets** definidos, no su valor crudo (detalle en `plan.md`).
- **Operación incompatible con tipo de columna**: pedir una operación matemática sobre una columna no numérica (o conteo "promedio" sin sentido) se rechaza con una alternativa válida.
- **Nulos en medidas numéricas**: se excluyen del promedio (no como cero) y se documenta el criterio; el COUNT general sigue reflejando todos los contratos.
- **Precisión/escala numérica**: los importes/consumos/potencias se devuelven con escala coherente (sin redondeos que falseen sumas).
- **Dos dimensiones**: máximo dos; más de dos se acota o se pide concretar.
- **Solicitud sobre `TipProCom = 3`**: fuera de alcance; el asistente lo indica.
- Operador sin sesión válida: no accede a la capacidad (reutiliza autenticación existente).

## Requirements *(mandatory)*

### Functional Requirements

- **FR-001**: El sistema MUST ofrecer una capacidad de **agregación** de contratos Unicliente (`TipProCom ∈ {1, 2}`) que calcule los resultados **en la base de datos** sobre **todos** los registros que cumplan los filtros, soportando como operaciones al menos: **COUNT** (conteo), **SUM** (suma), **AVG** (promedio), **MIN** (mínimo) y **MAX** (máximo).
- **FR-002**: La agregación MUST NOT aplicar ningún tope de filas a los registros considerados ni marcar el resultado como truncado; el límite solo aplica a la **presentación** de grupos de alta cardinalidad (Top-N).
- **FR-003**: El sistema MUST permitir **agrupar por cualquier columna de una lista blanca** del dominio de contratos: localidad del punto de suministro, localidad del cliente, provincia, dirección del punto de suministro, mes y/o año de contrato, tarifa eléctrica, tarifa de gas, tipo de energía, tipo de contrato (UniPunto/MultiPuntos), cliente (NIF/CIF o nombre) y estado de propuesta.
- **FR-004**: El sistema MUST **rechazar** cualquier columna de agrupación que no esté en la lista blanca; MUST NOT ejecutar agrupaciones arbitrarias definidas por el modelo.
- **FR-005**: El sistema MUST permitir **combinar hasta dos** columnas de agrupación (p. ej. localidad + mes) y devolver el total por cada combinación.
- **FR-006**: El sistema MUST ordenar los grupos por **total descendente** por defecto y MUST permitir un parámetro de **límite (Top-N)** para columnas de alta cardinalidad, indicando cuando existan más grupos de los mostrados.
- **FR-007**: El sistema MUST permitir aplicar **filtros** (cliente, tipo de contrato, tipo de energía, rango de fechas de contrato, localidad) antes de agregar, de forma coherente con la herramienta de listado de la feature 005.
- **FR-008**: Para valores nulos o vacíos en la columna agrupada, el sistema MUST agruparlos bajo una etiqueta clara sin excluirlos del total.
- **FR-009**: Las respuestas MUST basarse exclusivamente en los totales calculados; el asistente MUST NOT inventar valores ni grupos.
- **FR-010**: Cuando no haya registros para el criterio, el asistente MUST informarlo claramente.
- **FR-011**: El sistema MUST operar en modo **solo lectura**.
- **FR-012**: El clasificador de intención MUST enrutar a esta capacidad las preguntas de tipo "cuántos contratos por [columna]", "número de contratos por [columna]", "distribución de contratos por [columna]".
- **FR-013**: La consulta de agregación MUST derivarse del **mapa de relaciones del dominio** (contrato → cliente / punto de suministro / tarifas / localidad); MUST NOT definirse fuera de ese mapa.
- **FR-014**: El asistente MUST NOT exponer detalles técnicos internos (nombres de herramientas, columnas físicas o consultas) al operador.
- **FR-015**: El resultado MUST incluir el **agregado general** (total/medida global) además de los valores por grupo, para permitir verificación de coherencia.
- **FR-016**: El sistema MUST permitir **operaciones matemáticas** (SUM, AVG, MIN, MAX) sobre una lista blanca de **columnas numéricas** del dominio: consumo eléctrico, consumo de gas y potencias contratadas P1, P2, P3, P4, P5 y P6.
- **FR-017**: El sistema MUST permitir combinar una operación numérica con una **dimensión de agrupación** (p. ej. suma de consumo por localidad) y devolver la medida por cada valor de la dimensión más el agregado general.
- **FR-018**: El sistema MUST **rechazar** operaciones incompatibles con el tipo de columna (p. ej. SUM/AVG sobre una columna no numérica) y proponer una alternativa válida, sin ejecutar la operación.
- **FR-019**: El sistema MUST permitir solicitar **varias medidas** en una misma consulta (p. ej. suma y promedio del consumo) y devolverlas juntas.
- **FR-020**: En operaciones numéricas, los valores **nulos** MUST excluirse del cálculo (no contar como cero en promedios) y el criterio MUST ser consistente y documentado; la precisión/escala de los valores devueltos MUST preservar la exactitud de las sumas.
- **FR-021**: Para **agrupar por una columna numérica** como dimensión (distribución), el sistema MUST usar **rangos/buckets** definidos en lugar del valor numérico crudo.

### Key Entities

- **Contrato** (= propuesta comercial, `TipProCom ∈ {1,2}`): unidad que se agrega.
- **Dimensión de agrupación**: columna permitida (lista blanca) por la que se agrupan los contratos; categórica (localidad, tarifa, tipo, estado, cliente), temporal (mes/año de contrato) o numérica **por rangos/buckets**.
- **Medida numérica**: columna numérica permitida (consumo eléctrico, consumo de gas, potencias P1–P6) sobre la que aplicar operaciones matemáticas.
- **Operación de agregación**: COUNT, SUM, AVG, MIN o MAX aplicada a una medida (o COUNT sobre filas).
- **Grupo / fila de resultado**: combinación de hasta dos valores de dimensión + las medidas calculadas (conteo y/o agregados numéricos).
- **Filtro**: criterio aplicado antes de agregar (cliente, tipo, energía, fechas, localidad).

## Success Criteria *(mandatory)*

### Measurable Outcomes

- **SC-001**: En un escenario con más contratos que el límite de listado (50/500), el **100%** de los conteos agrupados devuelven totales cuya suma coincide con el total real de contratos del criterio.
- **SC-002**: **Cero** respuestas de conteo que incluyan el aviso de "lista truncada".
- **SC-003**: Al menos el **90%** de un conjunto de preguntas de conteo por distintas columnas (mínimo 8: localidad, provincia, tarifa, tipo de energía, tipo de contrato, mes, cliente, dirección) devuelven la agrupación correcta.
- **SC-004**: El **100%** de las solicitudes con columna no permitida se rechazan con sugerencia de columnas válidas, sin ejecutar consultas arbitrarias.
- **SC-005**: Para columnas de alta cardinalidad, el **100%** de las respuestas presentan Top-N por total e indican que hay más, manteniendo el total general exacto.
- **SC-006**: Cuando la base de datos no está disponible, el **100%** de los intentos muestran error claro sin datos inventados.
- **SC-007**: Las operaciones numéricas (suma/promedio/mín/máx) sobre consumos y potencias devuelven valores que coinciden con el cálculo sobre **todos** los registros del criterio (no sobre un subconjunto), verificado en al menos 4 escenarios (suma de consumo eléctrico, promedio de potencia P1, mín/máx de consumo de gas).
- **SC-008**: El **100%** de las solicitudes de operación matemática sobre columnas no numéricas se rechazan proponiendo una alternativa válida.

## Assumptions

- El alcance se mantiene en contratos **Unicliente** (`TipProCom` 1 y 2), coherente con la feature `005`; `TipProCom = 3` queda fuera de alcance.
- Las columnas **numéricas** (consumos, potencias) se tratan por defecto como **medidas** sobre las que aplicar SUM/AVG/MIN/MAX; agrupar **por** ellas (como dimensión) se hace mediante **rangos/buckets** definidos en `plan.md`.
- Las medidas numéricas en alcance son: consumo eléctrico (`ConCup`), consumo de gas (`CauDiaGas`) y potencias contratadas `PotEleConP1`–`PotEleConP6`. La definición de buckets por defecto se concreta en `plan.md` (o `/speckit-clarify`).
- La "localidad" puede referirse a la del **punto de suministro** o a la del **cliente**; ambas están en la lista blanca como dimensiones distintas, y el asistente desambigua si la pregunta es genérica.
- El conteo se calcula con agregación en la base de datos (no listando y contando en el modelo), reutilizando la conexión secundaria del producto.
- Solo operadores autenticados pueden usar la capacidad; respuesta en español, sin exponer detalles técnicos.
- Escritura en base de datos fuera de alcance; solo lectura.

## Dependencies

- Feature `005-contratos-unicliente-detalle`: modelos y relaciones del dominio de contratos Unicliente (cliente, punto de suministro, tarifas, localidad) y filtros equivalentes.
- Feature `002-agent-db-clients-contracts`: catálogo de herramientas y patrón de resultados agregados (los conteos no se truncan).
- Feature `004-chat-conversations`: contexto conversacional para seguimientos ("¿y por provincia?", "ahora solo gas").
- Conexión secundaria con datos de contratos poblados para pruebas de aceptación.
