# Guia: Adicionar um novo tipo de integração

> Checklist de referência para o Claude (e para o time) seguir **sempre** que for criar um
> novo tipo de integração no dashboard (ex.: TikTok Ads, LinkedIn Ads, RD Station, etc.).
>
> A ordem das etapas importa: cada uma depende do que ficou definido na anterior.
> Antes de escrever código, **leia esta arquitetura inteira** e confirme os arquivos citados
> ainda existem (a base evolui).

---

## Modelo mental: as 4 camadas de uma integração

```
Credencial global (integration_credentials + credential_secrets)
        │  1 credencial -> N assets descobertos
        ▼
Asset descoberto (integration_assets)        [discoverAssets()]
        │  vinculado a um projeto
        ▼
Integração de projeto (project_integrations) [provider.sync()]
        │  alimenta as tabelas de fato
        ▼
Dados (fact_*_daily + fact_marketing_daily)  -> Dashboard (abas/blocos/templates)
```

Provider (conector) = a classe PHP que sabe **validar** a credencial, **descobrir** assets e
**sincronizar** dados. Tudo gira em torno do contrato
[`ProviderInterface`](../app/Services/Integrations/Contracts/ProviderInterface.php).

`integration_type` / `provider` é uma **string canônica** usada de ponta a ponta
(ex.: `ga4`, `google_ads`, `meta_ads`, `exent_hub`). Escolha a sua (ex.: `tiktok_ads`) no
início e use exatamente a mesma em TODAS as camadas.

---

## Etapa 1 - Configurações (Conector / Provider)

**Objetivo:** ter um conector que valida credencial, descobre assets e sincroniza dados.

1. **Verificar se um conector existente já serve.**
   - Revisar os providers atuais em [`app/Services/Integrations/Providers/`](../app/Services/Integrations/Providers/):
     `GoogleAdsProvider`, `Ga4Provider`, `MetaAdsProvider`, `ExentHubProvider`.
   - Se a nova fonte só muda parâmetros (mesma API/auth), estender/parametrizar o existente.
     Só crie um provider novo se a API, o método de auth ou o shape dos dados forem diferentes.

2. **Definir o método de autenticação** e os campos de segredo/metadata.
   - Métodos já suportados (`auth_method`): `oauth`, `service_account`, `token_based`.
   - Mapear: quais campos são **secrets** (criptografados em `credential_secrets.secret_data`)
     e quais são **metadata** não sensível (ex.: `base_url` do Exent Hub).

3. **Criar o provider** implementando
   [`ProviderInterface`](../app/Services/Integrations/Contracts/ProviderInterface.php):
   - `validate(IntegrationCredential): bool` - bate na API, confirma permissões mínimas.
     Retorna `false` para credencial inválida; lança exceção só em falha de infra.
   - `discoverAssets(IntegrationCredential): array` - retorna lista de mapas com as keys
     `asset_type`, `asset_id`, `asset_name`, `metadata`. (Providers que persistem via Edge
     Function retornam `[]` e deixam o caller contar.)
   - `sync(ProjectIntegration, SyncContext): int` - sincroniza o período e retorna nº de linhas
     processadas. Obrigatório: `loadMissing(['credential','asset'])`, log de start/finish com
     `project_id`, `credential_id`, `period`, `rows_received/processed`, e `RuntimeException`
     em falha. Usar `grain_id` no upsert para idempotência (ver Etapa 2).
   - Reaproveitar `IntegrationCredentialService::getDecryptedSecrets()` para ler credenciais.

4. **Registrar o provider no factory.**
   - Adicionar o `case` em
     [`IntegrationCredentialService::providerFor()`](../app/Services/Integrations/IntegrationCredentialService.php#L191)
     (o `match` de string -> classe). É a única peça de registro - não há binding automático.

5. **Adicionar o novo tipo aos enums do Postgres** (passo fácil de esquecer).
   - ⚠️ No banco real, `integration_credentials.provider` e `project_integrations.integration_type`
     são o **ENUM nativo** `public.integration_type` (NÃO varchar+CHECK, apesar do que os arquivos
     de migration antigos sugerem). Confirme sempre o tipo real antes
     (`information_schema.columns` → `data_type=USER-DEFINED`, `udt_name=integration_type`).
   - Para essas colunas: `ALTER TYPE public.integration_type ADD VALUE IF NOT EXISTS '<novo_tipo>'`,
     e a migration precisa de `public $withinTransaction = false;` (ADD VALUE não roda em transação).
   - `dashboard_integration_configs.integration_type` é `varchar` com CHECK `dic_integration_type_check`:
     recriar o CHECK incluindo o novo tipo. `sync_runs.integration_type` é varchar livre (sem ação).
   - Precedente correto: [`2026_06_16_000001_add_search_console_to_integration_enums.php`](../database/migrations/2026_06_16_000001_add_search_console_to_integration_enums.php).

6. **Criar os testes por provider.**
   - **Gap atual:** hoje só existem `tests/Unit/SyncPolicyTest.php` e `SyncDueCalculatorTest.php`;
     **não há testes de provider**. Ao adicionar uma integração, criar a pasta/convenção:
     `tests/Feature/Integrations/{Nome}ProviderTest.php` (ou `tests/Unit/Providers/`).
   - Cobrir no mínimo: `validate()` com credencial ok/inválida (HTTP fake), `discoverAssets()`
     retornando o shape correto, e `sync()` fazendo upsert idempotente (rodar 2x não duplica).
   - Usar `Http::fake()` para não bater na API real.

7. **Tela de editar configuração com descoberta de assets.**
   - O CRUD de credencial global já é genérico:
     [`Pages/Integracoes/Form.jsx`](../resources/js/Pages/Integracoes/Form.jsx) +
     [`IntegrationCredentialController`](../app/Http/Controllers/IntegrationCredentialController.php).
   - Adicionar o novo provider em `PROVIDER_CONFIG` (campos de secret/metadata) e em
     `PROVIDER_OPTIONS` do `Form.jsx`. O painel "Ativos descobertos" + botão "Atualizar ativos"
     (rota `integracoes.refresh-assets`) já funciona para qualquer provider que implemente
     `discoverAssets()`.

---

## Etapa 2 - Banco de dados (estrutura dos dados)

**Objetivo:** decidir onde e como os dados sincronizados são salvos, no padrão atual.

1. **Avaliar se a estrutura atual comporta.** Hoje existem dois níveis:
   - **Tabela consolidada genérica** [`fact_marketing_daily`](../database/migrations/2026_03_26_000003_create_fact_marketing_daily_table.php)
     (`source` = provider; particionada por mês) - alimenta KPIs e gráficos agregados das abas fixas.
   - **Tabelas de fato específicas por provider** para drill-down detalhado
     (ex.: `fact_gads_campaign_daily`, `fact_meta_ad_daily`, `fact_ga4_*`).

2. **Decidir o padrão para a nova integração:**
   - Se a fonte se encaixa nas dimensões existentes (campanha/adset/ad/keyword/landing/device,
     métricas spend/clicks/impressions/conversions/revenue): **gravar em `fact_marketing_daily`**
     com `source = '{novo_tipo}'`. Sem migration nova de dados; só usar a coluna `source`.
   - Se há drill-down próprio com dimensões/métricas que não cabem no genérico: **criar
     `fact_{provider}_{nivel}_daily`** seguindo o padrão das existentes:
     - colunas `id (uuid)`, `project_id (FK)`, dimensões, métricas, `grain_id (text UNIQUE)`, timestamps;
     - índices `(project_id, metric_date DESC)` e `(project_id, {dimensão}, metric_date DESC)`;
     - considerar particionamento por `metric_date` se o volume for alto (ver `partition_*` migrations);
     - habilitar RLS no padrão dos outros facts (`is_exent_admin()` / `user_project_ids()`).
   - Criar o Model em `app/Models/Fact{...}Daily.php` espelhando os existentes (casts de data/inteiro,
     `belongsTo Project`).

3. **Filtros sempre suportados:** garantir que `project_id` + `metric_date` sejam a chave dos
   índices, e usar `grain_id` (concatenação de todas as dimensões) como UNIQUE para upsert
   idempotente no `sync()`.

4. **Persistência via seeder, não só no banco** (lição aprendida).
   Qualquer bloco/template/estrutura que a integração precise **tem que estar em migration ou
   seeder versionado**, nunca só no banco de staging - senão quebra em produção.
   Ver `AquisicaoDefaultTemplateSeeder` como precedente.

---

## Etapa 3 - Projetos (cards de integração por projeto)

**Objetivo:** permitir vincular e configurar a integração dentro de cada projeto.

1. Com base na Etapa 2, definir **o que carrega**: qual `asset_type` é vinculado, e o que vai
   em `project_integrations.settings` (sync_policy + flags específicas).

2. O card por projeto já é genérico - reaproveitar:
   - [`ProjectIntegrationsSection.jsx`](../resources/js/Components/ProjectIntegrationsSection.jsx)
     (tabela de integrações + ações: editar, sync, histórico, toggle, setup).
   - [`ProjectIntegrationFormSheet.jsx`](../resources/js/Components/ProjectIntegrationFormSheet.jsx)
     (cascata Provedor -> Credencial -> Asset + `SyncPolicySection`).

3. **Registrar o novo tipo no frontend** (respeitando a estrutura de config atual):
   - Ícone + label em [`integrationIcons.jsx`](../resources/js/Components/Dashboard/integrationIcons.jsx)
     (`PROVIDER_ICON`, `INTEGRATION_LABELS`).
   - Defaults e limites de sync em [`lib/syncPolicy.js`](../resources/js/lib/syncPolicy.js)
     (`defaultsFor`, `limitsFor`) - definir lookback/runs por dia adequados à API.
   - Validar `StoreProjectIntegrationRequest` / `UpdateProjectIntegrationRequest` aceitam o novo tipo.

4. Conferir que `bind` -> `tryAutoSetup` -> `bootstrap`/`sync` (em
   [`ProjectIntegrationService`](../app/Services/Integrations/ProjectIntegrationService.php))
   funciona para o novo provider (readiness checks, particionamento de bootstrap).

---

## Etapa 4 - Cards de template para a nova integração

**Objetivo:** disponibilizar os blocos da integração no **editor de templates** das abas
fixas (`/templates`), para que possam ser arrastados para qualquer template.

> ⚠️ Há **dois caminhos de renderização** que compartilham a mesma definição de blocos:
> 1. **Aba dedicada** (Etapa 5) - renderizada por `IntegrationTab.jsx` a partir de
>    `dashboard_integration_configs.blocks` (chaves "cruas", ex.: `block_kpis`).
> 2. **Editor de templates** (esta etapa) - catálogo gerado por `integrationItems()` com
>    code `{provider}_{block_key}` e renderizado por `blockRegistry.resolveBlock()`.
>
> `MetricDefinitions::BLOCKS` é a **fonte única** dos dois. `DashboardConfigController` deriva
> os blocos da aba dedicada de `MetricDefinitions::blocksForIntegration()`, então mudar ali
> afeta **os dois caminhos** - planeje as chaves de bloco pensando nisso.

1. **Definir os blocos e métricas no backend** em
   [`MetricDefinitions`](../app/Services/Dashboard/MetricDefinitions.php):
   - `METRICS[$provider]` - KPIs individuais (viram cards size-2 no catálogo).
   - `BLOCKS[$provider]` - blocos visuais (`block_kpis`, `block_timeline`, `block_drilldown*`),
     cada um com `key`, `label`, `type` (`kpis`/`chart`/`drilldown`), `default_visible`, `default_order`.
   - O catálogo expõe `{provider}_{key}` automaticamente via `TabBlockDefinitions::integrationItems()`.

2. **Registrar a label do provider** em **TODOS** os `INTEGRATION_LABELS` (senão a seção do
   editor e os nomes saem com a string crua, ex.: "search_console - KPI Cards"):
   - [`TabBlockDefinitions`](../app/Services/Dashboard/TabBlockDefinitions.php) (nomes dos itens),
   - [`TemplateController`](../app/Http/Controllers/TemplateController.php) (rótulo da seção no catálogo),
   - [`DashboardConfigController`](../app/Http/Controllers/DashboardConfigController.php) (tela de config),
   - frontend [`integrationIcons.jsx`](../resources/js/Components/Dashboard/integrationIcons.jsx)
     (`PROVIDER_ICON` + `INTEGRATION_LABELS`, usados em ícones/tooltips).

3. **Habilitar o rendering no frontend** em
   [`blockRegistry.jsx`](../resources/js/Components/Dashboard/blockRegistry.jsx):
   - Adicionar o provider em `PROVIDER_PREFIXES` (senão os codes não resolvem e renderizam `null`).
   - Adicionar entradas explícitas no `BLOCK_REGISTRY` para os blocos visuais:
     `{provider}_block_kpis` -> `<BlockIntegrationKpis provider=... />`,
     `{provider}_block_timeline` -> `<BlockIntegrationTimeline provider=... />`,
     e cada code de drill-down -> seu componente.
   - KPIs individuais (`{provider}_{metric}`) caem no fallback `IntegrationKpiBlock` - basta
     adicionar o provider em `IntegrationKpiBlock.PROVIDERS` e em `BlockIntegrationKpis.DEFAULT_VISIBLE`
     (ordem fixa dos KPIs).
   - Dados/format: `extract{Provider}Data()`, `METRIC_META` (label/format/`invertTrend`) e
     `TIMELINE_CHART_CONFIG` em
     [`tabs/integrationDataHelpers.js`](../resources/js/Components/Dashboard/tabs/integrationDataHelpers.js).

4. **Drill-down: um card único, não fatiado por dimensão.** O drill-down no template deve ser
   **um só bloco** que exibe o detalhamento completo do jeito da aba dedicada (dimensões **juntas,
   em abas**) - não criar um bloco por tabela. Precedente: Search Console usa um único
   `block_drilldown` (-> `BlockGscDrilldown`, que renderiza `SearchConsoleDrilldown` com todas as
   abas). Providers de ads (Google/Meta/GA4) mapeiam um code por nível porque seus drill-downs já
   têm níveis hierárquicos distintos (campanha -> anúncio -> keyword); use o mesmo critério.

5. **Sincronizar o catálogo no banco via seeder** (`DashboardBlockSeeder`) - obrigatório
   versionar (ver lição da Etapa 2.4). Rodar `php artisan db:seed --class=DashboardBlockSeeder` no deploy.
   - ⚠️ O seeder faz `updateOrCreate` por `code`: ao **renomear/remover** uma chave de bloco, o
     code antigo **fica órfão** em `dashboard_blocks` (não é removido). Limpe os órfãos e, se algum
     `template_block` já os referenciar, **migre** para o novo code antes de apagar (FK).
   - Para preservar config salva ao renomear chaves, usar `BLOCK_LEGACY_RENAMES` em `MetricDefinitions`.

6. **Limpar o opcache no deploy** (`php artisan optimize:clear`): labels/catálogo vêm de
   constantes PHP; com opcache ativo a alteração só aparece após limpar.

7. **Boas práticas de card** (já adotadas no projeto):
   - Seguir a ordem fixa de colunas de drill-down e a nomenclatura de métricas do `CLAUDE.md`
     ("Investimento" nunca "Custo"; "Conversões" nunca "Leads"; `invertTrend` para CPL/CPC/CPA).
   - Preencher `description`/`type`/`sources` para o `BlockInfoTooltip` e o `MetricCatalog`
     exibirem o bloco corretamente no editor.
   - Status badges no padrão shadcn do `CLAUDE.md`.

---

## Etapa 5 - Aba dedicada da integração

**Objetivo:** uma aba própria (estilo GA4 / Google Ads / Meta Ads) para a nova integração.

1. A aba dedicada usa o componente genérico
   [`tabs/IntegrationTab.jsx`](../resources/js/Components/Dashboard/tabs/IntegrationTab.jsx)
   (KPIs + timeline + drill-down). Avaliar se ele já atende; criar `tabs/{Provider}Tab.jsx`
   só se o layout precisar fugir do padrão.

2. **Ligar a aba no orquestrador** [`Pages/Dashboard.jsx`](../resources/js/Pages/Dashboard.jsx):
   - Adicionar o tipo em `INTEGRATION_FEATURES` (linha ~18) e garantir que entra em
     `integrationSections` (depende de `section_visible` + `has_data`).
   - Visibilidade/ordem da seção são controladas por `dashboard_integration_configs`
     (ver [`DashboardConfigController`](../app/Http/Controllers/DashboardConfigController.php)
     e a tela `/dashboard-config`).

3. **Servir os dados no backend:**
   [`DashboardDataController`](../app/Http/Controllers/DashboardDataController.php) precisa
   retornar os dados/KPIs do novo provider (agregados + período anterior para trends).

---

## Etapa 6 - Abas fixas: APENAS sugestão

**Objetivo:** não alterar as abas fixas automaticamente.

1. Abas fixas atuais (em `TabBlockDefinitions::TABS`): `resumo`, `aquisicao`, `trafego`, `leads`.

2. Analisar se algum bloco novo (Etapa 4) faria sentido nas abas fixas (ex.: incluir o novo
   provider num KPI combinado de "Investimento Total" ou num gráfico consolidado).

3. **NÃO aplicar.** Apenas **sugerir** ao usuário, listando: qual aba, qual bloco, e o porquê.
   A decisão de incluir nas abas fixas é do usuário, e qualquer mudança em template fixo deve
   ir por seeder/migration versionado.

---

## Checklist rápido (resumo)

- [ ] 1. String canônica do tipo definida e usada em todas as camadas
- [ ] 1. Provider implementa `validate` / `discoverAssets` / `sync` e está no `providerFor()`
- [ ] 1. Enums do Postgres + CHECK constraints atualizados (migration)
- [ ] 1. Testes do provider criados (`Http::fake`, idempotência)
- [ ] 1. `PROVIDER_CONFIG`/`PROVIDER_OPTIONS` no `Form.jsx` (tela + refresh assets)
- [ ] 2. Estrutura de dados decidida (`fact_marketing_daily` vs. fact específica) + Model + índices + RLS
- [ ] 2. Upsert idempotente via `grain_id`
- [ ] 3. Ícone/label (`integrationIcons`) + sync policy defaults/limits + Requests aceitam o tipo
- [ ] 4. Blocos no backend (`MetricDefinitions`/`TabBlockDefinitions`) + componentes + `blockRegistry` + helpers
- [ ] 4. Label do provider em TODOS os `INTEGRATION_LABELS` (Tab/Template/Config controllers + `integrationIcons`)
- [ ] 4. Provider em `PROVIDER_PREFIXES` + `IntegrationKpiBlock.PROVIDERS` + `BlockIntegrationKpis.DEFAULT_VISIBLE`
- [ ] 4. Drill-down como card único (juntos em abas), não um bloco por dimensão
- [ ] 4. Catálogo sincronizado via seeder versionado (limpar codes órfãos/migrar `template_blocks`; `optimize:clear`)
- [ ] 5. Aba dedicada ligada em `Dashboard.jsx` + dados em `DashboardDataController`
- [ ] 6. Abas fixas: apenas sugestão, sem aplicar
- [ ] Rodar `php artisan migrate`, seeders e `npm run build` no deploy
