# F6 — Click Tracking (produtos mais clicados)

## Status

- Situação: **Planejada (não implementada)**
- Objetivo: medir **quais ofertas geram mais cliques** trocando o link direto de afiliado por um redirect próprio em `ogerenteendoidou.com.br/r/{hash}`, que conta o clique e encaminha para o link real preservando a tag.
- Pré-requisito: Etapa 5 (F1) concluída — só vale medir cliques de ofertas que estão efetivamente sendo publicadas.

---

## Por que importa

Hoje a gente **não sabe** se o que está sendo publicado está dando clique. Sem isso:
- Não dá para saber qual plataforma performa melhor (Amazon vs ML)
- Não dá para saber quais categorias bombam
- Não dá para decidir o que ajustar no texto/imagem
- Não dá para fechar o loop com vendas (frente F7)

Com tracking, a gente passa a ter base real para decisões.

---

## Arquitetura

### Fluxo do clique

```
Usuário no Telegram clica em https://ogerenteendoidou.com.br/r/abc123
   ↓
App Laravel (rota /r/{hash}) recebe
   ↓
Registra clique em link_clicks (async/fila, sem bloquear)
   ↓
Responde HTTP 302 com Location: <link_real_de_afiliado>
   ↓
Usuário chega na Amazon/ML e a comissão é preservada pela tag do link real
```

**Crítico:** o redirect tem que ser **rápido** (alvo < 100ms), senão o usuário abandona. Por isso o registro do clique vai para fila (queue) e a resposta 302 sai **antes** do insert no banco (ou em paralelo).

---

## Modelagem de dados

### Nova tabela `tracked_links`

Cada oferta publicada tem **um** tracked_link ativo. O `hash` é o que aparece na URL.

```sql
CREATE TABLE tracked_links (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    hash VARCHAR(16) NOT NULL UNIQUE,                -- ex: "abc123Xy"
    processed_offer_id BIGINT UNSIGNED NULL,         -- vem do pipeline (F1)
    generated_link_id BIGINT UNSIGNED NULL,          -- vem do gerador manual
    platform VARCHAR(32) NOT NULL,
    destination_url TEXT NOT NULL,                   -- link real de afiliado (com tag)
    product_title VARCHAR(500) NULL,                 -- snapshot para relatórios
    product_image_url VARCHAR(500) NULL,             -- snapshot para relatórios
    click_count INT UNSIGNED NOT NULL DEFAULT 0,     -- contador denormalizado (para ordenar rápido)
    is_active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP NULL,
    updated_at TIMESTAMP NULL,
    INDEX idx_hash (hash),
    INDEX idx_offer (processed_offer_id),
    INDEX idx_generated (generated_link_id),
    INDEX idx_platform_clicks (platform, click_count),
    INDEX idx_created (created_at)
);
```

Regras:
- `hash` é curto (8–10 caracteres base62) e **único**
- Exatamente **um** dos dois FKs é preenchido (`processed_offer_id` para ofertas auto, `generated_link_id` para o gerador manual)
- `destination_url` guarda o link real com tag; se o link real mudar (ex: recálculo de afiliado), gera um novo tracked_link em vez de atualizar

### Nova tabela `link_clicks`

Um registro por clique recebido.

```sql
CREATE TABLE link_clicks (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    tracked_link_id BIGINT UNSIGNED NOT NULL,
    clicked_at TIMESTAMP NOT NULL,
    ip_hash VARCHAR(64) NULL,                        -- SHA256(ip+salt) para dedupe sem armazenar IP bruto
    user_agent VARCHAR(500) NULL,
    referer VARCHAR(500) NULL,
    country_code CHAR(2) NULL,                       -- opcional, via GeoIP
    is_bot BOOLEAN NOT NULL DEFAULT FALSE,           -- detectado por UA (não conta no ranking)
    INDEX idx_tracked (tracked_link_id, clicked_at),
    INDEX idx_date (clicked_at),
    FOREIGN KEY (tracked_link_id) REFERENCES tracked_links(id) ON DELETE CASCADE
);
```

Notas:
- `ip_hash` é hash com salt (não IP puro) — LGPD friendly, serve só para dedupe
- `is_bot` separa crawlers do Telegram (o Telegram às vezes faz preview do link e isso conta como clique se não filtrar)
- Tabela pode crescer muito; prever prune automático após 90 dias

---

## Geração do link curto

### Domínio

- Produção: `https://ogerenteendoidou.com.br`
- Formato: `https://ogerenteendoidou.com.br/r/{hash}`
- `{hash}` = 8 caracteres base62 gerados aleatoriamente com colisão tratada

### Quando gerar

Dois pontos do código criam tracked_links:

1. **Pipeline automático (F1)** — ao final do processamento, quando `formatted_text` é montado, **antes** de publicar:
   - O `Compre aqui:` do `formatted_text` passa a ser `https://ogerenteendoidou.com.br/r/{hash}` em vez do `meli.la/xxx` ou `amzn.to/xxx` direto
   - O link real (com tag) continua em `tracked_link.destination_url`

2. **Gerador manual (`/admin/amazon-affiliate`)** — ao salvar `GeneratedLink`, gera tracked_link e oferece os dois links (com e sem tracking) para copiar

### Prioridade do link exibido

```
[usa tracked_link.url curto]  ← padrão, para mensuração
       ↓ se F6 desabilitada
[usa link real com tag]        ← fallback
```

Flag de config para ligar/desligar: `TRACKING_ENABLED=true`

---

## Rota de redirect

### `routes/web.php`

```php
// FORA do grupo admin e FORA do middleware auth
Route::get('/r/{hash}', [TrackedLinkController::class, 'redirect'])
    ->where('hash', '[A-Za-z0-9]+')
    ->name('tracked.redirect');
```

### `TrackedLinkController::redirect()`

Pseudocódigo (a implementar):

```php
public function redirect(Request $request, string $hash)
{
    $link = TrackedLink::where('hash', $hash)->where('is_active', true)->first();

    if (!$link) {
        return redirect('https://ogerenteendoidou.com.br/', 302);  // ou página 404 amigável
    }

    // Enfileira o registro do clique (não bloqueia o redirect)
    dispatch(new RegisterClickJob(
        trackedLinkId: $link->id,
        ip: $request->ip(),
        userAgent: $request->userAgent(),
        referer: $request->header('referer'),
    ))->afterResponse();

    // Incrementa o contador denormalizado (UPDATE rápido, 1 query)
    $link->increment('click_count');

    return redirect($link->destination_url, 302);
}
```

### `RegisterClickJob`

Job que roda **depois** da resposta, sem atrasar o redirect:
- Detecta se é bot (lista de UAs conhecidos, especialmente `TelegramBot (like TwitterBot)`)
- Se bot, grava com `is_bot = true` (não entra em ranking)
- Grava `clicked_at`, `ip_hash`, `user_agent`, `referer`

---

## Admin web (relatórios)

### Nova tela: `/admin/tracking/top` — Produtos mais clicados

Cards no topo:
- Total de cliques (24h / 7d / 30d, seletor)
- Cliques de humanos vs bots
- Cliques por plataforma

Tabela principal (ordenada por clicks DESC):

| Coluna | Detalhe |
|---|---|
| # | Ranking |
| Produto | miniatura + título |
| Plataforma | amazon / mercadolivre |
| Publicado em | data da oferta |
| Cliques | contador no período filtrado |
| CTR estimado | cliques / visualizações do canal (se disponível via API Telegram; senão oculto) |
| Link | abrir a URL real |

Filtros: período (hoje / 7d / 30d / custom), plataforma, busca por título.

### Nova tela: `/admin/tracking/clicks/{tracked_link}` — Detalhe

- Timeline de cliques (gráfico por dia ou por hora)
- Split humano vs bot
- Top referrers
- Top user-agents
- Top países (se GeoIP estiver ligado)

### Integração com tela de ofertas (F1)

Na tabela `/admin/monitoring/offers`, adicionar coluna "Cliques" que consulta `tracked_links.click_count`. Já começa útil no dia 1.

---

## Permissões

- `tracking.view` — ver relatórios
- `tracking.manage` — desativar tracked_link manualmente, forçar regeneração de hash

---

## Comandos Artisan

### `php artisan tracking:prune --days=90`

Remove `link_clicks` com mais de 90 dias. Mantém `tracked_links` (que têm o contador agregado).

### `php artisan tracking:backfill-dashboard`

Recalcula `click_count` denormalizado a partir da tabela `link_clicks` (para auditoria, não deve precisar em operação normal).

---

## Schedule

```php
$schedule->command('tracking:prune --days=90')
    ->dailyAt('04:00')
    ->withoutOverlapping();
```

---

## Configuração

### `config/tracking.php` (novo)

```php
return [
    'enabled'      => (bool) env('TRACKING_ENABLED', true),
    'base_url'     => env('TRACKING_BASE_URL', 'https://ogerenteendoidou.com.br'),
    'hash_length'  => (int) env('TRACKING_HASH_LENGTH', 8),
    'retention_days' => (int) env('TRACKING_RETENTION_DAYS', 90),
    'geoip_enabled'  => (bool) env('TRACKING_GEOIP_ENABLED', false),
];
```

### `.env.example`

```
# ─── F6 Click Tracking ───────────────────────────────────────────────────────
TRACKING_ENABLED=true
TRACKING_BASE_URL=https://ogerenteendoidou.com.br
TRACKING_HASH_LENGTH=8
TRACKING_RETENTION_DAYS=90
TRACKING_GEOIP_ENABLED=false
```

---

## Detecção de bots

Lista mínima de user-agents a flaggear como `is_bot = true` (não contam no ranking):

- `TelegramBot`
- `facebookexternalhit`
- `WhatsApp`
- `Twitterbot`
- `Slackbot`
- `Discordbot`
- `LinkedInBot`
- `Googlebot`, `bingbot`

Motivo: sempre que o link é postado num canal, o próprio Telegram faz um `HEAD`/`GET` para gerar o preview, e isso conta como "clique" se não filtrar. Precisa estar filtrado desde o dia 1.

---

## Critérios de aceite

- Publicação da F1 gera `tracked_link` automaticamente e o `Compre aqui:` usa a URL `ogerenteendoidou.com.br/r/{hash}`
- Acessar o link no browser redireciona em < 200ms para o link real com tag de afiliado
- Clique humano é contabilizado; clique do bot do Telegram é registrado mas marcado `is_bot = true`
- Tela `/admin/tracking/top` lista ranking por período, atualizada em tempo real
- Se `TRACKING_ENABLED=false`, o pipeline volta a publicar o link real direto (sem tracking), sem erro

---

## Checklist técnico

### Infra
- [ ] Configurar DNS de `ogerenteendoidou.com.br` apontando para o servidor
- [ ] Certificado SSL (Let's Encrypt) para HTTPS
- [ ] Virtual host / nginx config para o domínio servir o Laravel

### Banco e modelos
- [ ] Migration `create_tracked_links_table`
- [ ] Migration `create_link_clicks_table`
- [ ] Model `TrackedLink` (com scope `active`, `forPeriod`, `topClicks`)
- [ ] Model `LinkClick`

### Pipeline
- [ ] Service `TrackedLinkService::createForOffer(ProcessedOffer)` e `createForGeneratedLink(GeneratedLink)`
- [ ] Integrar criação no fluxo da F1 (antes de `posted_at`)
- [ ] Integrar criação no `AmazonAffiliateController` (gerador manual)
- [ ] Substituir `Compre aqui:` para usar a URL do tracked_link quando `TRACKING_ENABLED`

### Redirect
- [ ] Rota `/r/{hash}` em `routes/web.php` (fora do admin)
- [ ] `TrackedLinkController::redirect()`
- [ ] `RegisterClickJob` com `afterResponse()`
- [ ] Detecção de bots por UA
- [ ] Hash base62 único com colisão tratada

### Admin
- [ ] Permissões `tracking.view`, `tracking.manage`
- [ ] Controller `Admin\TrackingController`
- [ ] View `/admin/tracking/top` (ranking)
- [ ] View `/admin/tracking/clicks/{link}` (detalhe)
- [ ] Menu lateral: seção "Tracking"
- [ ] Coluna "Cliques" na tela de ofertas (F1)

### Comandos
- [ ] `tracking:prune` com retenção configurável
- [ ] `tracking:backfill-dashboard` (utilitário)
- [ ] Schedule em `Kernel.php`

### Validação
- [ ] Criar oferta manualmente, clicar no link do Telegram real, ver contador subir
- [ ] Clicar com curl imitando `TelegramBot` UA → registra mas não entra no ranking
- [ ] Validar redirect < 200ms com `ab -n 100` ou similar

---

## Decisões em aberto

1. **GeoIP habilitado no dia 1?** — MaxMind GeoLite2 é grátis mas adiciona peso. Sugestão: começar desligado, habilitar depois se fizer sentido
2. **Página 404 customizada?** — Ou redirecionar para a home `ogerenteendoidou.com.br/`? Redirecionar é menos atrito
3. **Hash reutilizável?** — Se o mesmo produto for publicado de novo (em outro período), usa o mesmo tracked_link ou gera novo? Sugestão: **gera novo** — cada publicação é um evento
4. **Compartilhar tracked_link entre destinos?** — Se publicar no Telegram e no WhatsApp (F3 futuro), usa o mesmo hash ou um por destino? Um por destino permite separar "cliques vindos do Telegram" vs "cliques vindos do WhatsApp"

---

## Relação com outras frentes

- **Depende de F1** — só faz sentido quando há publicação automática acontecendo
- **Alimenta F7** — as vendas serão cruzadas com os cliques via `ascsubtag={hash}` nos links Amazon, fechando o loop clique → venda
- **Integra com F3 (WhatsApp)** — quando WhatsApp entrar, os mesmos tracked_links servem; ou um hash por canal, decidir na época
