# Mapeamento de Assemblers - Preview/Snapshot de Relatórios

> Objetivo: um **assembler único** (`ReportDataAssembler`) que monta o dado completo de um relatório
> (payload do `dashboard.data` + breakdowns por bloco), consumido **pelo preview e pela geração do
> snapshot** - garantindo que os dois sejam sempre idênticos.
>
> Status: **levas 1-4 implementadas** (assembler + endpoint de preview + 9 breakdowns).
> Pendentes: ⏸ dispositivos/cidades (aguardam sync GA4 - Fase 2) e a decisão de
> persistência de imagem Meta no snapshot (antes do envio por e-mail).

## Arquitetura

```
GET relatorios/{project}/preview-data?start_date&end_date   (preview, ao vivo)
        └─> ReportDataAssembler::assemble(Project, start, end)
                ├─ base:       DashboardDataController::getData()        (já usado hoje)
                └─ breakdowns: um assembler por bloco (tabela abaixo)
ReportGenerationService::generate()                          (snapshot)
        └─> MESMO ReportDataAssembler::assemble()  → salvo em report_snapshots.payload
```

- O assembler recebe `(Project, startDate, endDate)` e devolve `['base' => <dashboard.data>, 'breakdowns' => ['<block_key>' => <dado>]]`.
- Breakdown **só é montado se o bloco estiver ligado** na config do projeto (`report_config.sections/blocks`) - evita custo desnecessário.
- Cada breakdown reusa a MESMA query dos controllers de drill-down (extraída para método compartilhável ou chamada interna), com `per_page` fixo (top N) e sem paginação interativa.
- Padrão do relatório: **top 10 por bloco**, ordenado pela métrica principal.

## Mapa bloco → fonte de dados

Legenda de status: ✅ pronto (payload atual) · 🔌 reuso de endpoint existente · 🆕 agregação nova · ⏸ aguarda sync (Fase 2).

### GA4

| Bloco | Status | Fonte | Detalhe |
|---|---|---|---|
| `ga4_visao_geral` | ✅ | `base.trafegoKpis` + `base.trends.trafegoKpis` | - |
| `ga4_evolucao_sessoes` | ✅ | `base.ga4` (diário) | - |
| `ga4_source_medium` | ✅ | `base.ga4BySourceMedium` | top 10 já aplicado no controller |
| `ga4_conversoes_evento` | 🔌 | `Ga4DrilldownController@events` (`fact_ga4_event_daily`) | params: `start_date`, `end_date`, `conversions_only=1`, `sort=conversions`, `direction=desc`, `per_page=10`. Shape: `{event_name, event_count, conversions, users_count}` |
| `ga4_paginas` | 🔌 | `Ga4DrilldownController@pages` (`fact_ga4_page_daily`) | `sort=pageviews desc, per_page=10`. Shape: `{page_path, page_title, pageviews, sessions, users_count, conversions, bounce_rate, avg_session_duration}` |
| `ga4_dispositivos` | ⏸ | - | `fact_marketing_daily.device_type` 100% NULL; aguarda sync novo (dimensão `deviceCategory`) |
| `ga4_cidades` | ⏸ | - | idem (dimensão `city`) |

### Google Ads

| Bloco | Status | Fonte | Detalhe |
|---|---|---|---|
| `gads_visao_geral` | ✅ | `base.aquisicaoByProvider.google_ads` | - |
| `gads_campanhas` | ✅ | `base.aquisicaoCampaigns.google_ads.current` (+`previous`) | CTR/CPL derivados no front |
| `gads_anuncios` | 🆕 | `fact_gads_ad_daily` | **Agregação nova por conta** (endpoints atuais são por campanha: `campaigns/{id}/ads`). Query: SUM(impressions, clicks, cost_micros, conversions) GROUP BY ad_id, top 10 por spend. Colunas: nome (fallback `{ad_type} #{ad_id}` p/ RSA), campanha, impressões, cliques, CTR, conversões, custo/conv., invest. |
| `gads_keywords` | 🆕 | `fact_gads_keyword_daily` | **Agregação nova por conta** (atual é por campanha). GROUP BY keyword_id/keyword_text/match_type, top 10 por spend |

### Meta Ads

| Bloco | Status | Fonte | Detalhe |
|---|---|---|---|
| `meta_visao_geral` | ✅ | `base.aquisicaoByProvider.meta_ads` | - |
| `meta_campanhas` | ✅ | `base.aquisicaoCampaigns.meta_ads.current` | - |
| `meta_conjuntos` | 🆕 | `fact_meta_adset_daily` | **Agregação por conta** (atual: `campaigns/{id}/adsets`). GROUP BY adset_id, top 10 por spend. Métricas: impressions, clicks, spend, reach, leads (primaryLeads = leads_platform>0 ? leads_platform : conversions), CPL |
| `meta_anuncios` | 🆕 | `fact_meta_ad_daily` + thumbnails | **Agregação por conta** (atual: `adsets/{id}/ads`). GROUP BY ad_id + `MAX(creative_id)`, top 10 por spend. Thumbnails: reusar `resolveCreativeThumbnails()` (Graph API ao vivo, cache 1h). **No snapshot**: baixar a imagem no momento do run (decisão pendente: base64 no payload vs. arquivo em storage) - até lá, snapshot guarda a URL viva com validade limitada |

### Exent Hub

| Bloco | Status | Fonte |
|---|---|---|
| `hub_resumo` | ✅ | `base.leadsKpis` + trends |
| `hub_origem` | ✅ | `base.leadsSourceRanking` |
| `hub_campanha` | ✅ | `base.leadsCampaignRanking` |

### CVCRM

| Bloco | Status | Fonte |
|---|---|---|
| `cv_vendas` | ✅ | `base.cvcrm.kpis` |
| `cv_funil` | ✅ | `base.cvcrm.funnel` |
| `cv_roi` | ✅ | `base.cvcrm.roi.channels` |
| `cv_estoque` | ✅ | `base.cvcrm.estoque` |

### Search Console

| Bloco | Status | Fonte | Detalhe |
|---|---|---|---|
| `gsc_queries` | 🔌 | `SearchConsoleDrilldownController@queries` (`fact_gsc_query_daily`) | `sort=clicks desc, per_page=10`. Shape: `{query, clicks, impressions, ctr, position}` |
| `gsc_paginas` | 🔌 | `SearchConsoleDrilldownController@pages` (`fact_gsc_page_daily`) | idem, por `page` |

### Instagram Social

| Bloco | Status | Fonte | Detalhe |
|---|---|---|---|
| `ig_visao_geral` | ✅ | `base.instagramSocial` (agregado no front) | seguidores = último snapshot não-nulo; nunca somar follower_count |
| `ig_posts` | 🔌 | `SocialMediaDrilldownController@posts` (`fact_social_media_daily`) | `provider=instagram_social, sort=engagement desc, per_page=10`. Shape: `{caption, media_type, published_at, permalink, thumbnail_url, reach, impressions, likes, comments, shares, saves, engagement, engagement_rate}`. Métricas de post são cumulativas (último snapshot por media_id - o controller já resolve via DISTINCT ON) |

## Contagem

- ✅ prontos: **15** blocos (payload atual)
- 🔌 reuso de endpoint: **5** (ga4 eventos, ga4 páginas, gsc queries, gsc páginas, ig posts)
- 🆕 agregação nova: **4** (gads anúncios, gads keywords, meta conjuntos, meta anúncios)
- ⏸ aguarda sync: **2** (ga4 dispositivos, ga4 cidades - Fase 2)

## Ordem incremental de implementação

1. **Assembler base + endpoint de preview** - `ReportDataAssembler` devolvendo `base` + estrutura de `breakdowns` vazia; preview e `ReportGenerationService` passam a usar o assembler (snapshot ganha o mesmo shape). Sem bloco novo ainda, mas fecha a arquitetura.
2. **Leva 🔌 (reuso)** - ga4_conversoes_evento, ga4_paginas, gsc_queries, gsc_paginas, ig_posts. Extrair a query de cada controller para método reutilizável (controller e assembler chamam o mesmo código) OU chamar o controller internamente com Request sintético (mesmo padrão já usado na geração). Renderizadores de tabela no front.
3. **Leva 🆕 Google** - agregações por conta de anúncios e keywords (queries novas no assembler, sem endpoint público novo).
4. **Leva 🆕 Meta** - conjuntos e anúncios por conta + thumbnails ao vivo no preview; decisão de persistência de imagem no snapshot fica para a etapa do agendador/e-mail.
5. **⏸ Fase 2** - dispositivos/cidades entram automaticamente quando o sync GA4 for estendido (blocos já cadastrados com `requiresSync`).

## Decisões de projeto (registradas)

- **Top N fixo (10)** por bloco no relatório; sem paginação (relatório é estático).
- **Período**: sempre o do run (d-7/d-30, D-1 SP); breakdowns não têm comparativo com período anterior no MVP (só os KPI-rows têm trend, vindos de `base.trends`).
- **Custo**: breakdowns só rodam para blocos ligados; o assembler recebe a config resolvida.
- **Consistência preview × snapshot**: ambos consomem exclusivamente o assembler. Nenhum dado de relatório pode vir de outro caminho.
- **Meta thumbnails no snapshot**: pendência explícita (base64 vs storage) - decidir antes do envio por e-mail.
