# Relatório de Viabilidade — Relatório Semanal Automático Linkia (PX3 Lab)

Autor: Paulo (dev sênior, Climb Digital)
Data: 2026-06-22
Status: Análise técnica, sem implementação

## TL;DR

Viável, sem bloqueio técnico. A GHL API expõe tudo que precisamos: listar pipelines, buscar opportunities por pipeline com filtro de stage, e ler notes por contato. Não existe filtro nativo de notes por data, então o filtro semanal vai ser feito no código. Já temos infra (Python 3, requests, cron, bot Telegram, Drive, padrão `_ghl_headers`) e o token PX3 já está em uso em `prospeccao_ativa/app.py`. Estimativa: 4 a 6 horas pra v1 entregue (script + cron + envio Telegram + arquivamento Drive).

Recomendação: script standalone novo em `/opt/mia/workspace/clientes/px3lab/relatorio_semanal/`, rodando via cron toda sexta 18h, que envia resumo no Telegram e sobe o relatório completo em HTML + JSON pro Drive.

---

## 1. Fluxo de dados (endpoints e ordem)

Base: `https://services.leadconnectorhq.com`
Headers: `Authorization: Bearer <PX3_TOKEN>`, `Version: 2021-07-28`, `Content-Type: application/json`

### Passo 1 — Resolver pipeline alvo
`GET /opportunities/pipelines?locationId={LOCATION_ID}`
- Já é usado em `app.py:1207`. Devolve pipelines + stages com IDs e nomes.
- Usar `SDR_PIPELINE_ID` (já mapeado em `config_ghl.py`: `iV13JEVbMgfcad9Xw6gz` — "SDR MARIA — PROSPECÇÃO ATIVA").
- O dump da resposta vai virar o "dicionário stage_id -> stage_name" usado no relatório.

### Passo 2 — Buscar opportunities da pipeline (todas, paginado)
`GET /opportunities/search`
Parâmetros chave:
- `location_id={LOCATION_ID}`
- `pipeline_id={SDR_PIPELINE_ID}`
- `status=open` (e fazer chamada separada para `won` e `lost` se quisermos contar fechamentos / perdas da semana — ver Passo 4)
- `limit=100`
- `startAfter` / `startAfterId` para paginação (padrão GHL: cursor da última opp da página anterior)
- Opcional: `date` filters disponíveis (`updatedAt` start/end). Útil pra pré-filtrar somente opps que tiveram update na semana, mas NÃO substitui o filtro de notes (uma opp pode ter sido atualizada sem nota nova).

Resposta interessa: `id`, `name`, `contactId`, `pipelineStageId`, `status`, `monetaryValue`, `updatedAt`, `assignedTo`.

Estratégia: paginar até esvaziar; armazenar lista em memória. Pipeline SDR provavelmente cabe em 1-3 páginas (até 300 opps), tempo total < 5s.

### Passo 3 — Para cada opportunity, buscar notes do contato
`GET /contacts/{contactId}/notes`
- Endpoint padrão devolve array `notes` com `id`, `body`, `userId`, `dateAdded` (ISO 8601 UTC), `dateUpdated`.
- Não tem filtro de data na query — filtro é no Python.
- Limite por chamada: cerca de 100 notes (suficiente; PX3 raramente passa disso por contato).

Filtro semanal (segunda 00:00 a sexta 23:59:59, horário Brasília `America/Sao_Paulo`):
```python
inicio = segunda_da_semana_atual_zoneinfo_sao_paulo_00h
fim    = sexta_da_semana_atual_zoneinfo_sao_paulo_23_59_59
notes_da_semana = [n for n in notes if inicio <= parse(n["dateAdded"]).astimezone(SP) <= fim]
```

Otimização anti-rate-limit: usar `ThreadPoolExecutor(max_workers=5)` pra paralelizar o fetch de notes. GHL aceita rajadas curtas, e isso baixa o tempo de uma pipeline de 200 opps de ~40s sequencial para ~8-10s.

### Passo 4 — Agregar métricas
Com a lista de opps + notes da semana em memória:
- **Total leads na semana** = opps que têm pelo menos 1 nota nova na janela seg-sex.
- **Qualificados** = opps cujo `pipelineStageId` está em `[SDR_STAGE_QUALIFICANDO, SDR_STAGE_QUALIFICADO, SDR_STAGE_EM_NEGOCIACAO, SDR_STAGE_DEMO_AGENDADA]` E que tiveram nota na semana. (Mapa a confirmar com a Amanda — ver seção 7.)
- **Fechamentos** = opps com `status == "won"` E `updatedAt` na semana (chamada extra com `status=won` no Passo 2).
- **No-shows** = só conseguimos identificar se existir um custom field tipo `SDR_FIELD_STATUS_ATEND` com valor "no-show" OU se houver stage / nota com a palavra-chave. Hoje não vejo stage explícito de no-show no `config_ghl.py`. Duas opções:
  1. Heurística por keyword na nota (`re.search(r"no.?show|faltou|não compareceu", body, re.I)`).
  2. Criar custom field dedicado no GHL ou tag específica. Recomendo opção 2 pra ter dado limpo — Amanda pode criar.

### Passo 5 — Render do relatório
Gera 3 saídas em paralelo:
1. Resumo curto pro Telegram (texto formatado, ~1500 chars).
2. HTML completo (template Jinja2) salvo em `/opt/mia/workspace/clientes/px3lab/relatorio_semanal/saidas/AAAA-WW.html`.
3. JSON bruto pra reaproveitar / auditoria.

### Passo 6 — Distribuição
- Sobe HTML + JSON pro Drive na pasta `PASTA DA MIA > PX3 Lab > Relatórios Semanais SDR` (usar `google_drive` já configurado).
- Manda mensagem Telegram via outbox da Mia, com resumo + link público do Drive.

---

## 2. Estrutura do script proposta

Diretório novo: `/opt/mia/workspace/clientes/px3lab/relatorio_semanal/`

```
relatorio_semanal/
├── gerar_relatorio.py           # ponto de entrada (chamado pelo cron)
├── ghl_client.py                # wrapper sobre endpoints GHL (reusa _ghl_headers)
├── agregador.py                 # lógica de filtro semanal + métricas
├── render.py                    # gera HTML (Jinja2) + texto Telegram
├── distribuicao.py              # upload Drive + envio outbox Mia
├── templates/
│   └── relatorio.html.j2
├── saidas/                      # histórico de relatórios gerados
│   └── 2026-W26.html
└── logs/
    └── exec_2026-06-26.log
```

### Pseudocódigo do `gerar_relatorio.py`

```python
def main(data_ref: date | None = None):
    """data_ref = sexta-feira da semana a relatar (default: hoje)."""
    cfg = carregar_config()                       # token, location, pipeline_id, stage_map
    inicio, fim = janela_semanal(data_ref)        # seg 00:00 -> sex 23:59:59 SP

    pipelines = ghl_client.get_pipelines(cfg)
    stage_map = build_stage_map(pipelines, cfg.pipeline_id)

    opps_abertas  = ghl_client.list_opportunities(cfg, status="open")
    opps_won      = ghl_client.list_opportunities(cfg, status="won",  updated_since=inicio)
    opps_lost     = ghl_client.list_opportunities(cfg, status="lost", updated_since=inicio)
    todas         = opps_abertas + opps_won + opps_lost

    notes_por_opp = paralelo(ghl_client.list_notes, [o.contact_id for o in todas])
    leads_semana  = agregador.filtrar_por_notas_na_semana(todas, notes_por_opp, inicio, fim)
    metricas      = agregador.calcular_metricas(leads_semana, opps_won, opps_lost, stage_map)

    html  = render.html(leads_semana, metricas, stage_map, inicio, fim)
    texto = render.telegram(metricas, leads_semana, stage_map)
    json_bruto = render.json_dump(leads_semana, metricas)

    paths = distribuicao.salvar_local(html, json_bruto, semana_iso=iso_week(data_ref))
    drive_link = distribuicao.upload_drive(paths)
    distribuicao.enviar_telegram_mia(texto, link=drive_link)

    log(f"OK - {len(leads_semana)} leads, drive={drive_link}")
```

### Ponto importante de robustez

Todas as chamadas GHL precisam tratar:
- HTTP 429 (rate limit): backoff exponencial 1s, 2s, 4s, max 3 tentativas.
- HTTP 5xx: idem.
- `dateAdded` ausente ou string mal formatada: pular nota com log de warn, não derrubar o relatório.
- `contactId` ausente em opp órfã: skip silencioso.

---

## 3. Formato de saída recomendado

Híbrido — é o que dá mais valor sem inflar o Telegram:

### 3a. Telegram (resumo)
Curto, escaneável em 5 segundos no celular:
```
Relatório Semanal Linkia — semana 22/06 a 26/06

Visão geral
- Total de leads tocados: 18
- Qualificados: 7
- Fechamentos: 2
- No-shows: 1

Top movimentações
- Empresa A (Demo Agendada): "Confirmou demo terça 16h"
- Empresa B (Qualificado): "Procurando solução pra 5 escritórios"
- ...

Relatório completo: https://drive.google.com/...
```

### 3b. HTML no Drive (detalhado)
Template Jinja2 com:
- Cabeçalho com logo PX3 + intervalo da semana
- Cards de métricas (4 KPIs)
- Tabela: Nome | Stage | Última nota da semana | Data da nota | Link p/ opp no Linkia
- Agrupamento opcional por stage (sanfona)
- Footer com timestamp da geração

Salvo no Drive em `PASTA DA MIA > PX3 Lab > Relatórios Semanais SDR > 2026-W26.html` (link viewer pra qualquer um da PX3 abrir).

### 3c. JSON bruto
Para auditoria, debug, e reaproveitar em dashboard futuro. Fica versionado em `saidas/`.

PDF: dispensável agora. Se o Renato pedir depois, Playwright já está na VPS — converter HTML->PDF é 1 função.

---

## 4. Agendamento

**Recomendação**: cron na VPS, toda sexta 18h00 (Brasília).

Justificativa do horário:
- Tarde de sexta a equipe da PX3 já fez maior parte das tratativas do dia.
- 18h sobra margem pra Renato/SDR reagir antes do fim de semana.

```cron
# /etc/cron.d/relatorio_semanal_px3
0 18 * * 5 mia /usr/bin/python3 /opt/mia/workspace/clientes/px3lab/relatorio_semanal/gerar_relatorio.py >> /opt/mia/workspace/clientes/px3lab/relatorio_semanal/logs/cron.log 2>&1
```

Adicionalmente expor um trigger manual (uso do Renato ou da Mia):
- CLI: `python3 gerar_relatorio.py --data 2026-06-26` (gera relatório de qualquer sexta passada — útil pra teste).
- Endpoint Flask opcional em `app.py` (`POST /api/px3/relatorio-semanal`), atrás de `@login_required`, dispara em thread igual aos jobs existentes. Bom pra testar via UI sem cron.

---

## 5. Onde encaixar — standalone vs `app.py`

**Recomendação: script standalone**, em diretório dedicado.

Motivos:
1. `app.py` já tem 1900+ linhas, é o servidor Flask de prospecção e webhook Dinastia. Acoplar geração de relatório agendado vira monolito difícil de manter.
2. Rodando standalone, o cron não depende do Flask estar de pé, e crash do relatório não derruba prospecção.
3. Reaproveitamento limpo: `ghl_client.py` pode importar `_ghl_headers` e `GHL_BASE` direto do `app.py` (ou duplicar 5 linhas — trivial) sem virar dependência cruzada.

Integração leve com `app.py`: só o endpoint de trigger manual `POST /api/px3/relatorio-semanal`, que faz `subprocess.Popen([sys.executable, ".../gerar_relatorio.py"])` ou importa `main()` em thread. 15 linhas a mais no `app.py`, opcional.

---

## 6. Dependências

Tudo que precisa já está na VPS:
- `requests` (já usado).
- `jinja2` (provavelmente já — Flask depende). Confirmar com `pip show jinja2`.
- `zoneinfo` (Python 3.9+, padrão).
- `google-api-python-client` + OAuth tokens (já configurado, ver `/opt/mia/knowledge/.../google_drive.md`).
- Outbox da Mia (já estabelecido em `/opt/mia-bot/outbox/`).

Nada novo a instalar. Sem custo de licença/API extra (GHL e Drive já cobertos).

---

## 7. Limitações e dependências externas

### a. Mapa de stages "qualificado/fechamento/no-show"
A Amanda está investigando em paralelo. Sugestão de mapa preliminar (baseado em `config_ghl.py`):

| Conceito         | Stage IDs candidatos                                     |
|------------------|----------------------------------------------------------|
| Qualificado      | `SDR_STAGE_QUALIFICADO`, `SDR_STAGE_EM_NEGOCIACAO`, `SDR_STAGE_DEMO_AGENDADA` |
| Fechamento (won) | `SDR_STAGE_GANHAMOS` + filtro `status=won` no GHL        |
| Perda            | `SDR_STAGE_PERDEMOS` + filtro `status=lost`              |
| No-show          | Não existe stage dedicado hoje — ver opções abaixo       |

Bloqueio leve: enquanto Amanda não confirma, deixo o mapa em `config.json` editável (sem hard-code) pra trocar em 30 segundos.

### b. No-show
Sem campo/stage dedicado, três caminhos (em ordem de qualidade):
1. **Criar custom field "Status atendimento" com opção "no-show"** (Amanda faz no GHL). Filtramos por custom field.
2. **Criar tag "no-show"** no contato — mais leve, sem migração de schema.
3. **Heurística por keyword na nota** (`no.?show|faltou|não compareceu|não veio`). Funciona já, mas tem falso positivo.

Sugiro v1 com heurística (3) + flagar pra Amanda implementar (1) na próxima sprint.

### c. Rate limit GHL
Limite oficial documentado: 100 req/10s por location. Em 200 opps fazendo 1 listagem + 1 notes cada = 200-400 calls. Com paralelismo de 5 workers, cabe na janela. Backoff resolve picos.

### d. Notes com paginação
Endpoint `/contacts/{contactId}/notes` em algumas versões devolve só primeiras 100 — se contato tiver histórico longo, precisa de `limit` + `offset` ou cursor. Pra v1, assumir 100 e logar warning se vier exatamente 100 (sinal de truncamento).

### e. Timezone
GHL devolve `dateAdded` em UTC. Janela seg-sex precisa ser computada em `America/Sao_Paulo` e convertida. Bug clássico se esquecer: relatório da sexta puxa notes de domingo de manhã UTC.

---

## 8. Estimativa de esforço

| Bloco                                              | Tempo  |
|----------------------------------------------------|--------|
| Setup do diretório + `ghl_client.py` (com retry/backoff) | 1h     |
| `agregador.py` (janela + métricas + mapa de stage) | 1h     |
| `render.py` + template HTML Jinja2                 | 1h30   |
| `distribuicao.py` (Drive + outbox Telegram)        | 1h     |
| Cron + trigger manual + testes ponta-a-ponta       | 1h     |
| Margem (ajustes pós-validação com Amanda)          | 30min  |
| **Total v1**                                       | **~6h**|

Realista entregar a v1 em 1 dia útil. Se a Amanda travar no mapa de stages, entrego com mapa em `config.json` editável e desbloqueamos depois — não é blocker.

---

## 9. Riscos e mitigação

| Risco                                                | Mitigação                                                   |
|------------------------------------------------------|-------------------------------------------------------------|
| GHL muda contrato da API                             | Logar resposta bruta sempre; alerta no Telegram se schema quebrar |
| Token PX3 expira / é revogado                        | Já validamos em `app.py` que funciona. Logar HTTP 401 explícito |
| Relatório gera com 0 leads (semana morta)            | Mensagem honesta no Telegram "semana sem registros novos"   |
| Cron não roda (VPS reiniciada / cron parado)         | Healthcheck simples: Mia checa toda sexta 18h30 se arquivo da semana existe em `saidas/`; se não, alerta Renato |
| Drive sem permissão / quota                          | Fallback: anexar HTML como documento no Telegram via `/opt/mia-bot/outbox/` |

---

## 10. Próximos passos sugeridos (na ordem)

1. Amanda confirma/devolve mapa final de stages "qualificado / fechamento / no-show".
2. Renato aprova formato de saída (sugiro híbrido Telegram + Drive descrito na seção 3).
3. Renato decide horário do cron (sugiro sexta 18h Brasília).
4. Implementação v1 (~6h).
5. Rodar trigger manual em 2-3 sextas históricas pra validar dados antes de ligar o cron.
6. Ligar cron.

Fim do relatório.
