iachat/docs/captain-retention-indicators.md
Rodribm10 6fa2f621fa feat(retention): UI layer — badge, filters, cohort matrix, KPI dashboard
- RetentionSummaryBadge in the "Previous conversations" sidebar:
  tiered status (First contact / Active / Recurring / Sleeping /
  At risk / Inactive) + counts of interactions, one-shots, Pix.

- Retention tab in Captain Reports: KpiCards, FlowCard, CohortMatrix
  (12x13 heatmap with CSV export).

- Five new filters on the contacts list: recurring, last interaction,
  days since, interactions count, reservations paid.

- Full pt_BR + en i18n under CAPTAIN_REPORTS.RETENTION.*

- Spec for InteractionCalculatorService covering gap behavior,
  one-shot classification, internal-label exclusion, multi-conversation
  grouping across the 30h window.

- Docs: docs/captain-retention-indicators.md with business rules,
  column reference, endpoint shape, and backup SQL queries.
2026-04-22 10:30:19 -03:00

139 lines
5.4 KiB
Markdown

# Captain — Indicadores de Retenção e Recorrência
Design e implementação do sistema de métricas de retenção para motel (negociado
com Rodrigo em 2026-04-22).
## Regras de negócio
### Definição de "interação"
- Todas as mensagens públicas (não-privadas, status != failed) do contato e do
atendimento (Captain::Assistant ou User) são consideradas.
- **Gap de 30h** entre a última msg de uma interação e a próxima msg abre uma
nova interação. Abaixo disso, mesma "sessão".
- Motivo: motel tem frequência alta (múltiplas visitas na mesma semana); 30h
evita que manhã+tarde virem 2 interações mas captura corretamente
"voltou-no-dia-seguinte".
### Classificação de cada grupo
| Cliente | Atendimento | Tipo |
|---------|-------------|------|
| ≥ 2 msgs textuais | ≥ 2 msgs textuais | **qualificada** (conta em `interactions_count`) |
| ≥ 1 msg textual | ≥ 1 msg textual | **one-shot** (conta em `one_shot_consultations_count`) |
| 0 de um dos lados | — | ignorado |
**Textual** = pelo menos 2 letras (`\p{L}`). Exclui emoji-only, sticker,
"Message without content".
### Exclusão
Contatos com o label `equipe_interna` (manual) são ignorados em todas as
métricas. Aplica-se a gerentes, testes, números internos.
### Recorrência
`is_recurring = interactions_count >= 2`.
### Reservas vs Pix
- `pix_generated_count`: toda `Captain::PixCharge` criada (sinal de intenção).
- `reservations_paid_count`: `Captain::PixCharge.where(status: 'paid')` (reserva real).
- Conversão = paid / generated.
## Colunas em `contacts`
| Coluna | Tipo | Atualizada por |
|--------|------|----------------|
| `interactions_count` | integer | `Captain::Retention::RecalculateContactStatsJob` |
| `one_shot_consultations_count` | integer | idem |
| `first_interaction_at` | datetime | idem |
| `last_interaction_at` | datetime | idem |
| `pix_generated_count` | integer | idem (conta PixCharges) |
| `reservations_paid_count` | integer | idem |
| `is_recurring` | boolean | idem |
| `days_since_last_interaction` | integer | idem (recalculado no job) |
## Fluxo de atualização
- **Job diário** (`captain_retention_recalculate_all_contact_stats_job`, 6h UTC
/ 3h BRT): itera todos os contatos com conversa nos últimos 120 dias e
enfileira `RecalculateContactStatsJob` por contact.
- **Evento conversa resolvida**: `CaptainListener#conversation_resolved`
enfileira recalc incremental do contato.
- **Evento PixCharge**: `after_create_commit` e `after_update_commit`
enfileiram recalc (cobre geração e mudança pra paid/expired/failed).
## Endpoints
### `GET /api/v1/accounts/:id/captain/reports/retention`
Params: `start_date`, `end_date` (ISO, opcional, default = mês corrente).
Resposta:
```json
{
"period": { "start": "2026-04-01", "end": "2026-04-22" },
"status": {
"active": 340, "recurring": 85, "sleeping": 62,
"at_risk": 30, "churned": 15, "never_interacted": 0
},
"flow": { "new": 120, "returned": 40, "total_touches": 160 },
"pix": { "generated": 180, "paid": 45, "conversion_rate": 0.25 },
"retention": { "last_30d": 0.28, "last_90d": 0.12 }
}
```
Cortes de status (relativos a `Time.current`):
- **active**: `last_interaction_at >= now - 30d`
- **recurring**: `is_recurring=true AND last_interaction_at >= now - 90d`
- **sleeping**: `last_interaction_at` entre 30 e 90 dias atrás
- **at_risk**: entre 90 e 180 dias atrás
- **churned**: mais de 180 dias atrás
- **never_interacted**: `last_interaction_at IS NULL`
### `GET /api/v1/accounts/:id/captain/reports/retention/cohort`
Params: `history_months` (default 12, max 24).
Resposta: matriz com `cohorts` (linha por mês), cada linha tem
`size` + `retention[]` (array de 13 objetos: offset 0..12 com count e rate).
Cohort = mês da `first_interaction_at`. Célula [cohort, M+N] = % de contatos
da cohort com qualquer msg de cliente no mês M+N.
## UI
- **Aba Retenção em Relatórios do Captain** (`retention/RetentionTab.vue`):
- KpiCards: 4 cards (ativos, recorrentes, retorno 30d, conversão Pix)
- FlowCard: novos vs retornaram no período + situação absoluta (sleeping/
at_risk/churned)
- CohortMatrix: tabela cohort 12x13 com heatmap verde + export CSV
- **Badge no card de contato** (`RetentionSummaryBadge.vue`): mostra tier
(Primeiro contato/Ativo/Recorrente/Adormecido/Em risco/Inativo) + contadores
+ "última há X dias".
- **Filtros na lista de contatos** (`contactProvider.js`): 5 filtros novos —
cliente recorrente, última interação, dias sem interagir, nº de interações,
reservas pagas.
## Queries SQL de backup
Contagem rápida de ativos no mês:
```sql
SELECT COUNT(*) FROM contacts
WHERE account_id = $1 AND last_interaction_at >= NOW() - INTERVAL '30 days';
```
Taxa de retorno 30d da cohort de 30-37 dias atrás:
```sql
WITH cohort AS (
SELECT id FROM contacts
WHERE account_id = $1
AND first_interaction_at BETWEEN NOW() - INTERVAL '37 days' AND NOW() - INTERVAL '30 days'
)
SELECT (COUNT(*) FILTER (WHERE c.last_interaction_at >= NOW() - INTERVAL '7 days'))::float
/ NULLIF(COUNT(*), 0) AS rate
FROM contacts c WHERE c.id IN (SELECT id FROM cohort);
```
## Próximos passos possíveis
- Recalcular `first_interaction_at` / `last_interaction_at` via SQL direto
no job (atualmente é via service Ruby — OK até ~10k contatos ativos).
- Widget "Top recorrentes" (ranking dos contatos com mais reservas pagas).
- Alerta quando taxa de retorno cai > 10% mês a mês.
- Histórico da métrica para rodar tendências (tabela `retention_snapshots`).