chore: sync from fn-registry agent

This commit is contained in:
fn-registry agent
2026-06-14 11:27:02 +02:00
commit 5ec90b5bd3
2 changed files with 188 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
apps/*/
analysis/*/
vaults/*
!vaults/.gitkeep
!vaults/vault.yaml
@@ -0,0 +1,183 @@
# Report 0001 — Plan: Dashboard de progresión de Tickets, Venta y Ticket Medio (total y por categorías)
- **Fecha:** 10/06/2026
- **Autor:** Claude (sesión meta_bigq)
- **Ámbito:** Metabase `reports.autingo.es` (DB 6 `DCBigQuery`) + BigQuery `autingo-159109.psql_dcpublic`
- **Estado:** plan (no ejecutado — pendiente de validar decisiones abiertas)
## Resumen
Dashboard para vigilar la **evolución temporal** de tres KPIs de venta del negocio aurgi/autingo
(recambios y servicios de automoción, multi-centro + ecommerce + call center) y, sobre todo, para
**detectar caídas**: cuándo baja la venta, cuándo baja el número de tickets y cuándo baja el ticket
medio, tanto a nivel total como desglosado por categoría de producto. El objetivo no es solo "ver
los números" sino responder *dónde y cuándo se está cayendo y por qué* (menos tickets vs. ticket más
bajo vs. una categoría concreta arrastrando al total).
## Contexto descubierto (fuente de verdad del modelo)
El contexto se ha reconstruido clonando los análisis del proyecto aurgi alojados en Gitea
(`dataforge/venta_web`, `dataforge/sale_prices_comprobation`, `dataforge/presupuestos_callcenter`).
No existe un repo paraguas `dataforge/aurgi`; cada análisis es su propio sub-repo. El código de la
web del negocio (GitHub) no es necesario: el modelo analítico vive en BigQuery y en las cards de
Metabase ya construidas.
**Plataforma:**
- Metabase: `https://reports.autingo.es`, API key en `pass metabase/aurgi-api-key`.
- Database analítica: id **6** (`DCBigQuery`, engine bigquery-cloud-sdk).
- Proyecto/Dataset BigQuery: `autingo-159109.psql_dcpublic` (réplica del Postgres del TPV/ERP).
**Tablas núcleo de venta (dataset `psql_dcpublic`):**
| Tabla | Rol en el modelo |
|---|---|
| `tpv_orders_order` | Cabecera de pedido: `id`, `customer_id`, `vehicle_id`, `terminal_id`, `total_cost`, `created_at` |
| `tpv_orders_invoice` | Factura (venta consumada). Match con order por `order_id` |
| `tpv_orders_orderitem` | Líneas de pedido: `order_id`, `product_id`, importe, `tax` → base del desglose por categoría |
| `tpv_orders_quote` | Presupuesto (no es venta; relevante para conversión, fuera de alcance v1) |
| `products` | Catálogo de producto: `id`, `nav_id`, `channel_id`, `show_on_ecommerce`, referencia normalizada |
| `ecommerce_categories` | Catálogo de categorías (candidato a fuente de "categoría") |
| `channels` | Canal de venta (`id`, `name`) — p. ej. ecommerce vs. TPV |
| `centers` + `tpv_terminals` | Centro físico (terminal → center). **159 y 162 = CALL CENTER** (excluir o segmentar) |
| `companies`, `tpv_customers`, `tpv_vehicles_vehicle` | Cliente / empresa / vehículo (matrícula) |
**Patrón de dashboard ya probado en este Metabase** (`presupuestos_callcenter/create_dashboard_v2.py`):
- Cards SQL nativo con **field-filters parametrizados** `[[AND {{filtro}}]]` compartidos por todo el tab.
- Colección de trabajo (presupuestos usó `collection_id 559`); dashboard de KPIs ya existente (nº 999).
- Field-ids ya identificados que reutilizaremos donde apliquen: `quote.created_at=16588`,
`center.id=17327`, `tpvuser.id=17965`, `company.id=17157`, `product.id=16698`.
## Definiciones de métricas (propuesta — a confirmar)
- **Ticket** = una **venta facturada** = 1 fila de `tpv_orders_invoice` (o `tpv_orders_order` con
factura asociada). Nº de tickets = `COUNT(DISTINCT invoice.id)`.
- **Venta (€)** = importe facturado. Propuesta: `SUM(tpv_orders_order.total_cost)` a nivel total;
para el desglose por categoría se suma a nivel **línea** (`tpv_orders_orderitem`). Hay que fijar si
el importe es **con o sin IVA** (existe campo `tax`) — debe ser coherente entre total y categorías.
- **Ticket medio (€)** = `Venta / Nº tickets`.
- **Caída** = variación negativa de cualquiera de los tres KPIs frente al **periodo anterior
comparable** (mes vs. mes anterior, y mismo mes año anterior — interanual, por la estacionalidad
fuerte del recambio: ITV, neumáticos por temporada, balizas V16, etc.).
## Arquitectura del dashboard
Un único dashboard, **3 secciones** (filtros globales compartidos arriba):
**Filtros globales (field-filters):** Rango de fechas (sobre la fecha de venta), Centro, Canal,
Categoría, Compañía. Todos mapeados a todas las cards vía el patrón `[[AND {{...}}]]`.
### Sección A — KPIs cabecera (fila de scalars con variación)
Tres `smartscalar` (o `scalar` + comparación): **Venta total**, **Nº tickets**, **Ticket medio**,
cada uno con su **variación % vs. periodo anterior** y un mini-sparkline. Aquí se ve "de un vistazo"
si hay caída global.
### Sección B — Progresión temporal (series)
- **Línea/área**: Venta por mes (con línea del año anterior superpuesta para comparar).
- **Línea**: Nº tickets por mes.
- **Línea**: Ticket medio por mes.
- **Barras + línea combinada**: Venta (barras) y Ticket medio (línea) en el mismo eje temporal —
permite ver si una caída de venta viene de menos tickets o de ticket más bajo.
### Sección C — Desglose por categoría (dónde está la caída)
- **Barras horizontales**: Venta por categoría en el periodo, ordenada desc.
- **Tabla con variación**: por categoría → Venta, Nº tickets, Ticket medio, **Δ% vs. periodo
anterior**, con formato condicional (rojo = caída). Esta es la tabla clave para "analizar caídas".
- **Heatmap / pivot** (mes × categoría) de la venta o del Δ% — detecta qué categoría empezó a caer y
cuándo.
- **Opcional**: mismo desglose por **centro** y por **canal** (misma estructura), si se quiere cruzar
la caída por ubicación/canal.
## SQL base (esqueleto parametrizado)
Una sola CTE de "ventas" reutilizada por casi todas las cards, con los field-filters inyectados. La
tabla cuya columna alimenta un field-filter va **sin alias** (convención del patrón existente):
```sql
WITH ventas AS (
SELECT
`psql_dcpublic.tpv_orders_invoice`.id AS ticket_id,
`psql_dcpublic.tpv_orders_order`.total_cost AS importe,
DATE_TRUNC(DATE(`psql_dcpublic.tpv_orders_order`.created_at), MONTH) AS mes,
`psql_dcpublic.centers`.id AS centro_id
-- categoria_id / canal_id segun la fuente que se confirme
FROM `psql_dcpublic.tpv_orders_order`
JOIN `psql_dcpublic.tpv_orders_invoice`
ON `psql_dcpublic.tpv_orders_invoice`.order_id = `psql_dcpublic.tpv_orders_order`.id
LEFT JOIN `psql_dcpublic.tpv_terminals`
ON `psql_dcpublic.tpv_orders_order`.terminal_id = `psql_dcpublic.tpv_terminals`.id
LEFT JOIN `psql_dcpublic.centers`
ON `psql_dcpublic.tpv_terminals`.center_id = `psql_dcpublic.centers`.id
WHERE 1=1
-- AND COALESCE(`psql_dcpublic.centers`.id,0) NOT IN (159,162) -- excluir call center si procede
[[AND {{fecha}}]]
[[AND {{centro}}]]
[[AND {{canal}}]]
[[AND {{categoria}}]]
)
SELECT mes, SUM(importe) AS venta, COUNT(DISTINCT ticket_id) AS tickets,
SAFE_DIVIDE(SUM(importe), COUNT(DISTINCT ticket_id)) AS ticket_medio
FROM ventas
GROUP BY mes ORDER BY mes;
```
Para el desglose por categoría se baja el grano a `tpv_orders_orderitem` (JOIN a `products` y a la
fuente de categoría) y se agrupa por `categoria`. **Gotcha de doble conteo**: al unir líneas, el
`COUNT(DISTINCT ticket_id)` sigue siendo correcto, pero la venta por categoría debe sumarse a nivel
línea, no de cabecera, para no duplicar `total_cost`.
## Detección de caídas (lo que de verdad pide el encargo)
1. **Variación vs. periodo anterior** en cada KPI (smartscalar con `previousValue` sobre breakout
mensual; ver gotcha smartscalar más abajo).
2. **Tabla por categoría con Δ%** y formato condicional rojo para Δ% < 0 → ranking de "quién cae".
3. **Comparativa interanual** (mismo mes del año pasado) como segunda serie en las líneas, por la
estacionalidad del sector.
4. **Descomposición de la caída**: al poner Venta, Tickets y Ticket medio juntos en el tiempo, se
distingue si la venta cae por *menos clientes* (tickets) o por *gastan menos* (ticket medio).
5. *(Futuro, opcional)* alerta por email/Slack de Metabase cuando Δ% mensual de venta o de una
categoría top supere un umbral negativo.
## Plan de ejecución por fases
1. **Exploración (barata, sin coste BQ relevante)** — confirmar contra `INFORMATION_SCHEMA` y el
metadata de Metabase DB6: (a) campo de fecha de venta correcto (`order.created_at` vs.
`invoice.created_at`), (b) si importe lleva IVA, (c) fuente real de "categoría" (`ecommerce_categories`
vs. familia NAV vs. `CASE` manual ya usado en `venta_web`), (d) field-ids de los filtros nuevos.
2. **Cards base** — crear las cards SQL parametrizadas (KPIs, series, desglose) en una colección
propia del dashboard. Validar cada query con `dry_run`/límite antes de guardar.
3. **Dashboard** — crear dashboard, añadir filtros globales, colocar cards en las 3 secciones,
copiar los `parameter_mappings` (patrón `metabase_dashboard_append_row` / donante).
4. **Variaciones y formato** — smartscalars con comparación, formato condicional de la tabla por
categoría, formato moneda EUR y porcentaje.
5. **Validación** — cuadrar el total del dashboard contra un número de control conocido (p. ej. venta
de un mes ya cerrado) y revisar visualmente las caídas detectadas.
## Decisiones abiertas (necesito tu confirmación antes de construir)
1. **¿Ticket = factura (`invoice`) o pedido (`order`)?** ¿Cuentan los pedidos sin factura?
2. **¿Importe con o sin IVA?** ¿`total_cost` es el bueno o hay que sumar líneas/base imponible?
3. **¿Qué es "categoría"?** ¿`ecommerce_categories`, la familia NAV del producto, o la clasificación
manual tipo `CASE` que ya aparece en `venta_web` (Balizas V16, etc.)?
4. **Call center (centros 159/162):** ¿se excluyen, se incluyen, o van como segmento aparte?
5. **Canales:** ¿separar ecommerce vs. TPV físico, o todo junto con filtro?
6. **Granularidad temporal:** ¿mensual (propuesto), o también semanal/diaria?
7. **¿Dashboard nuevo o ampliar uno existente** (p. ej. el 999 de presupuestos)? Recomiendo nuevo.
## Gaps / riesgos / gotchas
- **Sin confirmar todavía**: el join exacto producto→categoría y el campo de fecha/IVA (fase 1). El
SQL de arriba es esqueleto, no definitivo.
- **Coste BigQuery**: las tablas TPV son grandes. Usar `dry_run` antes de cada query nueva,
particionar por fecha en el `WHERE`, y evitar `SELECT *`. Metabase cachea, pero el primer render de
cada card paga su escaneo.
- **Smartscalar v0.59**: la comparación `anotherColumn` falla si la query devuelve varias filas;
para "vs. periodo anterior" hace falta breakout temporal + `previousValue` (documentado en el skill
meta_bigq). Alternativa: card SQL de 2 filas (actual/anterior) con `metabase_smartscalar_kpi_*`.
- **Doble conteo** al desglosar por categoría (líneas vs. cabecera) — sumar venta a nivel línea.
- **Definición de "caída"**: sin acordar el periodo de comparación (MoM vs. interanual) el dashboard
puede dar falsas alarmas por estacionalidad. Propuesta: mostrar ambas.
## Próximo paso
En cuanto confirmes las **decisiones abiertas** (sobre todo 1, 2 y 3), arranco la **fase 1
(exploración barata)** para fijar campos exactos y luego construyo cards + dashboard.