# Snapshots de métricas para trends de stock del dashboard admin

- **Fecha:** 2026-07-23
- **Contexto:** [#43](https://github.com/jdredondo/credify/issues/43) — cierre de lo diferido en Fase 2 (trends de métricas de *stock*: patrimonio, mora, cartera).
- **Rama:** `feat/dashboard-metric-snapshots`
- **Estado:** diseño aprobado en brainstorming (Approach A), pendiente de plan de implementación.

## Objetivo

Mostrar la **flecha de tendencia (▲/▼ %)** en los KPIs de *stock* del dashboard **admin** — Patrimonio, Caja, Mora y la card de Cartera (Saldo por Cobrar) — que la Fase 2 dejó sin flecha porque no son derivables de datos transaccionales. La comparación es **semana-sobre-semana** (hoy vs hace 7 días), y la flecha debe **reconciliar exactamente** con la cifra mostrada.

## Por qué hace falta una nueva captura

Estas métricas son de **stock** (foto del portafolio hoy): no se pueden reconstruir hacia atrás desde transacciones. Hace falta una foto diaria.

**Ya existe** infraestructura de snapshots (`par_snapshots` + comando `credify:snapshot-par`, agendado a diario y con el scheduler corriendo en prod — ~3 meses de historia). **Pero** `par_snapshots` guarda `par1..par90` y `total_portfolio`, que usan **definiciones distintas** a las del dashboard (`delinquency_rate` = vencido/por_cobrar; `por_cobrar` ≠ `total_portfolio`). Reusar esa historia daría una flecha que **no reconcilia** con el número mostrado. Por eso se snapshotean las **métricas exactas** del dashboard, en su propia tabla.

## Decisiones (del brainstorming)

- **Alcance:** Mora + Cartera + Patrimonio (+ Caja, gratis). **Admin-only** — snapshots por empresa; los trends de equipo del supervisor quedan fuera (necesitarían snapshots por-cobrador, otro proyecto).
- **Ventana:** hoy vs **hace 7 días** (semana-sobre-semana), suaviza el ruido diario.
- **Consistencia > inmediatez:** se snapshotea la métrica exacta del dashboard. Las flechas **arrancan vacías** y aparecen tras ~7 días de rampa (no hay backfill de stock). Aceptado.
- **Almacenamiento:** **tabla nueva** `dashboard_snapshots` + comando nuevo (aislamiento de la tabla PAR).

## Arquitectura

### 1. Tabla `dashboard_snapshots` (migración nueva)
Una fila por (empresa, día). Columnas:
- `id`, `company_id` (FK cascadeOnDelete), `snapshot_date` (date)
- `business_total` (decimal 14,2) — patrimonio (por_cobrar + caja)
- `cash_base` (decimal 14,2) — caja
- `por_cobrar` (decimal 14,2) — saldo por cobrar
- `overdue` (decimal 14,2) — vencido (PAR)
- `delinquency_rate` (decimal 5,2) — % de mora
- `created_at` (timestamp useCurrent)
- `unique(company_id, snapshot_date)`, index en `snapshot_date`

Modelo `App\Models\DashboardSnapshot` (fillable + casts numéricos, `$timestamps = false` con `created_at` manual, igual patrón que `ParSnapshot`).

### 2. Comando `credify:snapshot-dashboard` (`app/Console/Commands/SnapshotDashboardMetrics.php`)
- Itera las empresas activas (mismo criterio que `SnapshotPar`).
- Por empresa: llama `AdminDashboardMetricsService::getMetrics($companyId)` y `updateOrCreate` en `dashboard_snapshots` por (company_id, hoy) con `business_total`, `cash_base`, `por_cobrar`, `overdue` (= `por_cobrar.overdue`), `delinquency_rate`.
- **Idempotente** (updateOrCreate) — re-correrlo el mismo día no duplica.
- Agendado en `bootstrap/app.php`: `$schedule->command('credify:snapshot-dashboard')->dailyAt('23:55')` (después del de PAR a las 23:50; el scheduler ya corre en prod vía cron `schedule:run`).

### 3. Lectura de trend (`app/Services/Dashboard/DashboardTrendService.php`)
- `trendFor(int $companyId, float $todayValue, string $metric): ?array` — busca el snapshot con `snapshot_date <= today()->subDays(7)` **más reciente** para la empresa; si no hay (rampa), devuelve `null`.
- Con el snapshot, computa `pct = round((today - past)/past * 100, 1)` (null-safe si `past` es 0) y `good` según **polaridad** del métrica:
  - `business_total`, `cash_base`, `por_cobrar` → ↑ = bueno.
  - `delinquency_rate` → ↓ = bueno.
- Devuelve `{ pct, good }` (misma forma que `interest_month.trend` de Fase 2, consumible por el `trendOf()` del frontend).

### 4. Wiring en `AdminDashboardMetricsService::getMetrics()`
Inyectar `DashboardTrendService`. Tras computar las cifras vivas, añadir `trend` a las claves del retorno:
- `business_total`, `cash_base`, `por_cobrar` pasan a incluir una subclave `trend` (p.ej. `business_total => ['amount' => ..., 'trend' => {pct, good}|null]`), preservando `amount`.
- Mora: se añade una **clave nueva** `delinquency_trend` (`{pct, good}|null`) a nivel de `stats`, **sin** tocar `delinquency_rate` (escalar que otros consumidores ya usan).
- `null` cuando no hay snapshot ≥7 días → el frontend no dibuja flecha.

### 5. Frontend (`AdminHome.vue`)
- KPIs **Patrimonio** y **Caja**: `trend: trendOf(stats.business_total, 'vs semana pasada')` / idem caja.
- KPI **Mora**: `trend` desde `delinquency_trend` (polaridad ya resuelta en backend vía `good`).
- Card **Saldo por Cobrar**: pequeña flecha desde `por_cobrar.trend`.
- Reutiliza el helper `trendOf()` y el prop `trend` de `KpiCard` (ambos de Fase 2). Sin componentes nuevos.

## Componentes y límites

| Unidad | Responsabilidad | Depende de |
|---|---|---|
| `dashboard_snapshots` (tabla+modelo) | Persistir la foto diaria financiera por empresa | — |
| `SnapshotDashboardMetrics` (comando) | Escribir la foto de hoy (idempotente) | `AdminDashboardMetricsService`, modelo |
| `DashboardTrendService` | Calcular delta 7d + polaridad desde el snapshot | modelo |
| `AdminDashboardMetricsService` | Exponer `trend` en el payload admin | `DashboardTrendService` |
| `AdminHome.vue` | Dibujar las flechas | payload |

Aislamiento: el comando de escritura y el servicio de lectura son independientes y testeables por separado; ninguno conoce al frontend.

## Errores / bordes
- **Rampa (sin snapshot ≥7d):** `trend = null` → sin flecha (no error, estado esperado los primeros ~7 días).
- **Día faltante** (si un día el scheduler no corrió): se toma el snapshot más reciente con `date <= hoy-7` (no exige exactamente -7). 
- **`past = 0`:** `pct = null` (evita división por cero).
- **Empresa sin datos:** `getMetrics` ya devuelve ceros; el snapshot guarda ceros; trend `null`/0 coherente.
- **Idempotencia:** `updateOrCreate` — seguro re-correr; útil para backfill manual de "hoy" si se despliega tarde.

## Pruebas
- **Comando:** correrlo crea/actualiza una fila por (empresa, hoy) con los valores de `getMetrics`; re-correrlo no duplica (idempotente).
- **DashboardTrendService:** con un snapshot sembrado a hoy-7, computa el `pct` correcto y `good` según polaridad (mora ↓ = good, patrimonio ↑ = good); devuelve `null` sin snapshot en ventana; `pct = null` si `past = 0`.
- **Payload admin:** `/pwa/dashboard` (admin) incluye `trend` en las claves; `DashboardDataScopeTest` sigue verde (supervisor/cobrador sin cambios — esto es admin-only).
- PHPStan nivel 5 `app/` + Pint limpios (correr **antes** de pushear).

## Fuera de alcance
- Trends de equipo del **supervisor** (necesitarían snapshots por-cobrador).
- Backfill histórico de patrimonio/caja (no reconstruible).
- Sparkline de saldo de cartera histórico (Fase 2 ya usa el sparkline de cobros, derivable).
- Retención/poda de snapshots (volumen trivial; YAGNI).
