---
id: MOD-CONEXOES-PARCEIROSINSTITUCIONAIS-DADOS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-PARCEIROSINSTITUCIONAIS]
fontes:
  - V2/Modules/Conexoes/ParceirosInstitucionais/
depende_de: [CORE-DATA, CORE-BOOT, SVC-GALLERY, SVC-SEARCH]
consumidores: [MOD-LAYOUTS-HOMEPAGE, UI-PORTAL, SVC-SEO, SVC-DASHBOARD, DATA-SEEDS-CONTENT, DATA-SEEDS-BASICPAGES]
criticidade: media
dono: ParceirosInstitucionais
---

# Conexões/ParceirosInstitucionais — dados

> Anexo do módulo [`spec.md`](spec.md). Responde: qual é o estado final da tabela, como
> sete migrations chegaram nele, o que o `rename`/`cleanup` realmente faz com os dados, e
> quem escreve cada coluna. Uma tabela de domínio, um pivô compartilhado, um índice de
> busca derivado — e uma coluna `logo` legada que sobreviveu à migração para `logo_id`.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Persistir parceiros institucionais com identidade UUID, slug único, taxonomia por tags e atributos sociais/endereço em JSONB; manter o índice público coerente com `is_active`. |
| Entrada | `store`/`update` do CMS, `toggle`, `reorder`, `destroy`, `DemoContentSeeder::seedParceiros`, `PopulateMissingImages`. |
| Saída | Linhas em `v2_parceiros_institucionais`, `v2_parceiro_tag_pivot`, `v2_public_search_index`; logs de migration. |
| Efeitos colaterais | `ON CONFLICT` no índice de busca, remoção no archive, `activity_log`, deleção física da tabela legada na cleanup. |
| Particularidade central | A criação inicial **já** usa o nome final da tabela; o par rename/cleanup existe para bancos legados e pode **dropar** a `v2_parceiros` antiga sem migrar linhas. |
| Como se integra | `Parceiro` Eloquent (PK string, `SoftDeletes`); `GalleryMedia` por `logo_id`; `ConexaoTag` pelo pivô. |
| Se removido, quebra | Homepage, portal, sitemap, busca, dashboard e os seeders que consultam `Parceiro`. |
| Superfície pública | `$table`, `$fillable`, `$casts`, hooks, `galleryLogo`, `tags`, scopes e métodos de sincronização. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

| Perfil | Ação | Resultado no banco |
|---|---|---|
| Operador | Criar parceiro | `INSERT` com UUID e slug; `metadata.social_media`; pivô de tags; `created_by`. |
| Operador | Editar | `UPDATE` de todos os campos do form; slug preservado quando já existe; pivô sincronizado. |
| Operador | Desativar | `UPDATE is_active=false, sort_order=-1` + `UPDATE ... decrement` nos demais. |
| Operador | Reordenar | N `UPDATE`s de `sort_order` (sem hooks). |
| Operador | Arquivar | `UPDATE deleted_at`; não toca no pivô. |
| Seeder | Demo | `INSERT` de 8 linhas com `sort_order` 1..8. |
| Comando | `gallery:populate-missing-images --only=parceiros` | `UPDATE logo_id` das linhas nulas. |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| `id` gerado com `v2_uuid()` quando vazio | hook `creating` | `Models/Parceiro.php:54-58` |
| `slug` único derivado de `name` | hook `saving` | `Models/Parceiro.php:60-77` |
| Upsert do índice público | hook `saved` | `Models/Parceiro.php:79-85` |
| Remoção do índice público | hooks `saved` (inativo) e `deleted` | `Models/Parceiro.php:87-93,143-145` |
| `metadata` serializado/deserializado como array | cast `array` | `Models/Parceiro.php:39-42` |
| `is_active` como booleano | cast `boolean` | `Models/Parceiro.php:39-42` |
| `v2_parceiros` legada dropada quando ambas existem | cleanup `up()` | `Migrations/2026_08_06_000000_cleanup_old_parceiros_table.php:42-44` |

### 1.3 Contrato observável (dado → efeito)

| Entrada | Efeito |
|---|---|
| `name` novo, sem `id` | `id = UUIDv4`, `slug = slugify(name)` (ou `-N`), `created_at/updated_at` preenchidos. |
| `name` de um parceiro **arquivado** | `INSERT` viola `unique(slug)` → exceção SQL não tratada pelo controller. |
| `metadata.social_media` com item `{"network":"","url":"x"}` | descartado antes do save. |
| `tags = ['Ouro','Ouro ']` | duas tentativas, mesmo slug → uma tag só; `sync` mantém uma linha com `is_primary=1`. |
| `is_active=false` via `save()` | linha removida de `v2_public_search_index`. |
| `delete()` | `deleted_at` + remoção do índice; pivô intacto. |
| `reorder` com id inexistente | 0 linhas afetadas, resposta `success:true`. |

### 1.4 O que este anexo NÃO cobre

- Estrutura de `v2_gallery_media`, `v2_public_search_index` e `v2_conexao_tags` em profundidade — pertencem a `SVC-GALLERY`, `SVC-SEARCH` e `CORE-AUTH`/`CORE-CMS`.
- Dados de páginas fixas (`v2_basic_pages`) semeados pelo `ParceirosInstitucionaisSeeder` — pertencem a `DATA-SEEDS-BASICPAGES`.

## 2. Abordagem

### 2.1 Estratégia em uma página

Sete migrations contam uma história em duas camadas: a camada `2024_01_01_0000xx` **cria e evolui** a tabela já com o nome final; a camada `2026_07/08` faz **reparo de bases legadas** que ainda tinham `v2_parceiros` (do módulo `Conexoes/Parceiros` removido), com guards `hasTable` para idempotência.

```mermaid
flowchart LR
  A[2024_000002 cria v2_parceiros_institucionais] --> B[000010 logo_id + 3 redes]
  B --> C[000020 address + maps]
  C --> D[000030 metadata jsonb]
  D --> E[000040 summary]
  E --> F[2026_07_31 rename v2_parceiros -> nova se só antiga]
  F --> G[2026_08_06 cleanup: antiga only=rename; ambas=DROP antiga]
  G --> H[2026_08_18 pivô de tags]
```

### 2.2 Mecânica passo a passo

1. `CreateV2ParceirosTable::up()` abre transação, aplica `lock_timeout = 3s` e cria a tabela final com `uuid` PK, `slug` unique, `logo` textual, índices de ativo e ordem (`Migrations/2024_01_01_000002_create_v2_parceiros_table.php:23-56`).
2. `down()` **não** dropa: remove índices e renomeia para `backup_v2_parceiros_institucionais_<Y_m_d_His>` (`:63-90`).
3. `AddSocialAndLogoFields`: adiciona `logo_id`, `linkedin_url`, `instagram_url`, `facebook_url` com posicionamento relativo (`Migrations/2024_01_01_000010_...php:16-21`); `down()` remove as quatro colunas.
4. `AddAddressFields`: `address` text e `google_maps_url` varchar(500) (`Migrations/2024_01_01_000020_...php:16-19`).
5. `AddMetadata`: `metadata` **jsonb** nullable (`2024_01_01_000030_...php:16-19`).
6. `AddSummary`: `summary` text nullable logo após `description` (`2024_01_01_000040_...php:16-19`).
7. `RenameV2ParceirosToV2ParceirosInstitucionais`: se `v2_parceiros` existe e a nova não, `Schema::rename` (`2026_07_31_000000_...php:18-23`); `down()` faz o inverso com os mesmos guards (`:30-35`).
8. `CleanupOldParceirosTable`: três ramos — só antiga → rename (dados herdados); ambas → `Schema::drop('v2_parceiros')` (dados da antiga perdidos); nenhuma ação caso só a nova exista (`2026_08_06_000000_...php:36-47`).
9. `down()` da cleanup renomeia a tabela ativa para `backup_v2_parceiros_institucionais_<timestamp>` **somente** se `v2_parceiros` não existir (`:62-82`).
10. O pivô `v2_parceiro_tag_pivot` é criado no core junto com `v2_conexao_tags` e a tag reservada `Geral` (`V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:18-56`).

### 2.3 Decisões de design e por quê

- **`uuid` PK gerado na aplicação** `[C]`: a migration não define `default`; `v2_uuid()` é helper PHP (`V2/Core/Helpers/helpers.php:61-69`). Inserções por SQL cru precisam do id.
- **`slug` unique desde a criação** `[C]`: identifica o parceiro em busca e na blacklist do widget, embora a URL pública use `id`.
- **`metadata` jsonb** `[C]`: extensibilidade sem migration para redes sociais e chaves futuras.
- **Rename com guard nos dois sentidos** `[C]`: torna a migration segura tanto em base nova (no-op) quanto em base legada (repara).
- **Cleanup destrutivo quando há duas tabelas** `[C]` + `[I]`: o comentário diz que a antiga é "esqueleto antigo com menos dados"; a decisão de dropar assume que `v2_parceiros_institucionais` é a fonte correta. Não há contagem/comparação antes do `DROP`.
- **Sem foreign keys** `[C]`: nem `logo_id`, nem o pivô declaram FK; integridade é de aplicação.

### 2.4 Trechos de código pivô

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Migrations/2024_01_01_000002_create_v2_parceiros_table.php:30-45
// criação: nome final, slug unique, logo textual legada e índices de leitura
Schema::create('v2_parceiros_institucionais', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name', 255);
    $table->string('slug', 255)->unique();
    $table->text('description')->nullable();
    $table->string('logo', 255)->nullable()->comment('Caminho do arquivo de logo');
    $table->string('url', 255)->nullable()->comment('Site do parceiro');
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->string('created_by', 36)->nullable()->comment('UUID do usuário que criou');
    $table->timestamps();
    $table->softDeletes();

    $table->index('is_active', 'idx_v2_parceiros_institucionais_active');
    $table->index('sort_order', 'idx_v2_parceiros_institucionais_sort');
});
```

O nome do arquivo (`..._create_v2_parceiros_table`) não corresponde à tabela criada: a base nova nunca verá `v2_parceiros`. `logo` nasce aqui e nunca é removida nas migrations seguintes — é a coluna que o `_form` não escreve e o `preview` ainda tenta ler.

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Migrations/2026_08_06_000000_cleanup_old_parceiros_table.php:36-47
// três ramos: herdar, descartar ou não fazer nada
$hasOldTable = Schema::hasTable('v2_parceiros');
$hasNewTable = Schema::hasTable('v2_parceiros_institucionais');

if ($hasOldTable && !$hasNewTable) {
    Schema::rename('v2_parceiros', 'v2_parceiros_institucionais');
    Log::info("[Migration] Tabela 'v2_parceiros' renomeada para 'v2_parceiros_institucionais'.");
} elseif ($hasOldTable && $hasNewTable) {
    Schema::drop('v2_parceiros');
    Log::info("[Migration] Tabela legada 'v2_parceiros' removida — 'v2_parceiros_institucionais' é a tabela ativa.");
} else {
    Log::info("[Migration] Nenhuma ação necessária — 'v2_parceiros_institucionais' já é a única tabela de parceiros.");
}
```

**Consequência para dados**: em um banco que tenha as duas tabelas (por exemplo, instalação antiga + migrations da camada 2024 já aplicadas), o `DROP` elimina qualquer linha exclusiva da `v2_parceiros`. Não há `INSERT ... SELECT`, contagem ou backup. O `down()` não recria a antiga.

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Models/Parceiro.php:101-124 — taxonomia
// compartilhada por nome, com flag de principal no primeiro item
public function syncTagsFromArray(array $tagNames)
{
    $tagNames = array_values(array_filter($tagNames, function ($n) { return !empty(trim($n)); }));
    $tagIds = [];

    foreach ($tagNames as $idx => $name) {
        $name = trim($name);
        $slug = \Illuminate\Support\Str::slug($name);
        $tag = \V2\Core\Models\ConexaoTag::firstOrCreate(
            ['slug' => $slug],
            ['name' => $name]
        );
        $tagIds[$tag->id] = ['is_primary' => $idx === 0];
    }

    $this->tags()->sync($tagIds);
}
```

A relação é `belongsToMany(ConexaoTag::class, 'v2_parceiro_tag_pivot', 'parceiro_id', 'tag_id')->withPivot('is_primary')->withTimestamps()` (`Models/Parceiro.php:101-106`). `sync()` remove vínculos não listados e atualiza `is_primary`; tags nunca são apagadas ao desvincular. `firstOrCreate` pode correr em paralelo e violar `unique(slug)` — não há `try/catch` nem `lockForUpdate`.

```php
// V2/Modules/Conexoes/ParceirosInstitucionais/Models/Parceiro.php:39-48 — contrato de tipos
protected $casts = [
    'is_active' => 'boolean',
    'metadata' => 'array',
];

protected $dates = [
    'created_at',
    'updated_at',
    'deleted_at',
];
```

Não há cast para `id`/`logo_id` (strings), nem `$dates` para `google_maps_url`; `metadata` volta do jsonb como array PHP em qualquer leitura. Em PostgreSQL o jsonb preserva a ordem das chaves do objeto, mas a lista `social_media` é uma array JSON — a ordem é a do builder JS (`order` recalculado 1..N a cada render).

### 2.5 Algoritmos, consultas e custos

| Operação | SQL gerado | Custo |
|---|---|---|
| Listagem CMS | `SELECT ... WHERE (name ILIKE ? OR description ILIKE ?) ORDER BY sort_order, created_at DESC LIMIT 15 OFFSET ?` | full scan por `ILIKE %x%`. |
| Listagem portal | `... WHERE is_active = true ORDER BY sort_order, name LIMIT 12` | usa `idx_v2_parceiros_institucionais_active` e depois ordena. |
| Slug | `SELECT EXISTS(... WHERE slug = ? AND id != ?)` por tentativa | 1 query por colisão. |
| Busca | `INSERT ... ON CONFLICT (entity_type, entity_id) DO UPDATE` | precisa do unique composto em `v2_public_search_index`. |
| Toggle off | 1 `UPDATE` do alvo + 1 `UPDATE ... sort_order = sort_order - 1` | atômico por statement, **não** em transação única. |
| Reorder | N `UPDATE ... WHERE id = ?` | sem validação de pertencimento/limite. |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Histórico de duas camadas de migrations** com o mesmo nome de tabela: a criação já é "institucional", o rename é retrocompatibilidade.
- **Coluna `logo` órfã**: sobrevive a todas as migrations e é lida por `preview()` e pelo integrador da Homepage; nenhuma escrita do CRUD a alimenta.
- **`sort_order` com `-1` como sentinela de inativo**, sem coluna dedicada de posição "arquivada".
- **Pivô nomeado `v2_parceiro_tag_pivot`** (singular) e compartilhado com `v2_unidade_regional_tag_pivot` pela mesma tabela de tags.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-PARCEIROSINSTITUCIONAIS-13 | `slug` é NOT NULL por construção (atribuído no `saving` quando vazio), mas ausente do `$fillable`. | `create(['slug'=>...])` é ignorado; em updates o slug carregado é mantido, mesmo alterando `name`. | `Models/Parceiro.php:22-37,60-77` |
| RN-PARCEIROSINSTITUCIONAIS-14 | `id` não tem default no banco. | Insert cru sem id falha; model sempre gera. | migration `:31`; `Models/Parceiro.php:54-58` |
| RN-PARCEIROSINSTITUCIONAIS-15 | `logo_id` é `varchar(36)` sem FK para `v2_gallery_media`. | Apagar mídia deixa referência pendurada (view mostra placeholder se a relação for nula). | migration `000010:17`; `Models/Parceiro.php:96-99` |
| RN-PARCEIROSINSTITUCIONAIS-16 | Pivô sem FK/cascade e com PK composta `(parceiro_id, tag_id)`. | Soft delete não limpa vínculos; `sync` é a única via de manutenção. | core migration `:26-35` |
| RN-PARCEIROSINSTITUCIONAIS-17 | `metadata` é jsonb nullable; o cast devolve `null` quando a coluna é nula, e o controller faz `?? []`. | Primeiro save sem metadata grava `{"social_media":[]}`. | `Models/Parceiro.php:41`; controller `:86,159` |
| RN-PARCEIROSINSTITUCIONAIS-18 | `created_by` é gravado apenas no store. | Editar/reativar não altera o autor original. | controller `:91,127-182` |
| RN-PARCEIROSINSTITUCIONAIS-19 | `deleted_at` participa da PK lógica da busca apenas via `entity_id`. | Reindexar após arquivar e recriar com o mesmo id recria a entrada. | `Models/Parceiro.php:87-93,140-159` |
| RN-PARCEIROSINSTITUCIONAIS-20 | Migrations usam `lock_timeout = 3s` e transação explícita. | Em tabela grande sob carga, a migration pode falhar por timeout e ser reexecutada. | migrations `000002:25-55`, `cleanup:31-54` |

### 3.3 Casos-limite

- **Base sem nenhuma das tabelas** e apenas a camada 2026 aplicada: rename e cleanup não fazem nada; a tabela não existe e o módulo quebra em runtime (as migrations 2024 são a única criação).
- **Rollback completo**: `migrate:rollback` na ordem inversa executa `down()` da pivô (drop), da cleanup (rename para `backup_*`), do rename (só se `v2_parceiros` ausente — após a cleanup não há, então renomeia `v2_parceiros_institucionais` → `v2_parceiros`), e das colunas; a `down()` da criação tenta `dropIndex` em `v2_parceiros_institucionais`, que já não existe → `Schema::hasTable` protege e nada acontece. Resultado: os dados ficam em `v2_parceiros` com as colunas removidas.
- **Duas execuções da mesma migration** (timestamp duplicado em dois módulos): o migrator registra por nome de arquivo; a segunda falha em `Schema::create` se a tabela existir. Não há guard na criação.
- **`metadata` inválido gravado por SQL** (texto não-JSON): o cast `array` retorna `null` silenciosamente em `json_decode` falho.
- **Concorrência de slug**: duas requisições simultâneas com o mesmo nome podem calcular o mesmo slug; a segunda falha no unique (não há `lockForUpdate`).

### 3.4 Armadilhas de reimplementação

1. **Não recriar a tabela como `v2_parceiros`**: todo o código (model, SEO, dashboard) aponta para `v2_parceiros_institucionais`; o rename só serve a bases antigas.
2. **Não dar `DROP` sem inventário**: a cleanup presume que a nova tabela é a correta; em reimplementação, migrar dados (`INSERT ... ON CONFLICT`) antes de remover.
3. **`metadata` não é só redes sociais**: o seeder grava `metadata.seo`; qualquer normalização que substitua o objeto inteiro apaga chaves desconhecidas. O controller atual lê `?? []` e reescreve apenas `social_media`, preservando o resto.
4. **`SoftDeletes` + unique**: o índice unique não tem `WHERE deleted_at IS NULL`; reimplementar com índice parcial muda o comportamento de colisão.
5. **Timestamps no pivô**: `withTimestamps()` exige `created_at/updated_at` na tabela; o `sync` grava ambos.

### 3.5 Divergências e dívidas

- **Comentário do cabeçalho da criação** promete "rollback seguro: renomeia para backup… (NUNCA perde dados)", mas a cleanup dropa a tabela antiga (`cleanup:20` vs `:43`).
- **`down()` da rename** contradiz o docblock "sem rollback para o nome antigo" (`rename:9` vs `:30-35`).
- **`logo` e `logo_id` coexistem** sem migração de dados nem deprecação formal.
- **`logo` está no `DemoContentSeeder` como chave de dados** (`'logo' => '01.png'`), mas o seeder só grava `logo_id` (`V2/Seeds/Content/DemoContentSeeder.php:1009-1018`).
- **`Parceiro::max('sort_order')` ignora soft-deleted**, podendo reutilizar ordens (`Controllers/...CmsController.php:82`).

## 4. Integrabilidade

### 4.1 Superfície de dados

| Símbolo | Tipo | Assinatura/estrutura | Precondição | Consumidores |
|---|---|---|---|---|
| `v2_parceiros_institucionais` | tabela | 17 colunas de domínio + 3 timestamps (20 no total) | migrations 2024 aplicadas | todo o módulo e leitores externos |
| `v2_parceiro_tag_pivot` | tabela | `(parceiro_id, tag_id)` PK, `is_primary`, timestamps | core 2026_08_18 | `Parceiro::tags`, `ConexaoTag::parceiros` |
| `Parceiro` | model | `$table`, `$fillable`, `$casts`, `$dates` | — | controllers, seeders |
| `Parceiro::syncTagsFromArray` | método | `(array): void` | nomes não vazios | CMS |
| `Parceiro::syncToSearch` | método | `(): void` | tabela de índice | hooks |

### 4.2 Contrato de UI/JS

Não se aplica diretamente: nenhum JS lê o schema. O contrato indireto é o hidden `logo_id` (string do id de mídia) e o `social_media_json` (array JSON com `network`, `icon`, `url`, `order`, `active`) que o controller persiste em `metadata.social_media`.

### 4.3 Ordem e ciclo de vida

1. Migrations do core primeiro no caminho de boot, mas o migrator ordena globalmente por nome: as `2024_01_01_*` rodam antes das `2026_06_*` do core.
2. `v2_uuid()` é helper PHP, não função SQL: nenhuma migration depende da função de banco.
3. O pivô (2026_08_18) é posterior ao cleanup (2026_08_06): em base legada, o rename acontece antes de o pivô ser criado.
4. `DemoContentSeeder` presume a tabela e a galeria já existentes (`galleryId`).

### 4.4 Receitas de uso

**Consultar sem carregar tags (evita N+1):**

```php
$parceiros = Parceiro::with(['galleryLogo'])
    ->active()->ordered()->get(['id', 'name', 'slug', 'summary', 'logo_id', 'metadata']);
```

**Trocar apenas a ordem em lote (sem disparar hooks):**

```php
foreach ($ids as $i => $id) {
    Parceiro::where('id', $id)->update(['sort_order' => $i + 1]);
}
```

**Recuperar a taxonomia de um parceiro:**

```php
$tags = $parceiro->tags()->orderByPivot('is_primary', 'desc')->get();
// ConexaoTag { id, name, slug, is_reserved }
```

### 4.5 Anti-uso

1. **`Parceiro::create($request->all())`**: `slug`/`created_by` não são fillable e o UUID só é gerado no hook — funciona, mas `logo`/`created_by` do payload são descartados silenciosamente.
2. **Apagar tag com `delete()` direto**: deixa linhas no pivô (sem FK). Use `ConexaoTagCmsController::destroy`, que reatribui para `Geral` (`V2/Core/Controllers/ConexaoTagCmsController.php:70-109`).
3. **Contar parceiros com `DB::table('v2_parceiros_institucionais')->count()`**: inclui arquivados; o dashboard usa `Parceiro::active()->count()`.

### 4.6 Custos e limites

- `jsonb` sem índice GIN: não há consulta por chave de metadata.
- `ILIKE` sem trigram na busca do CMS.
- Sem paginação em exports/seeders; `PopulateMissingImages` carrega todos os sem logo.
- `lock_timeout` de 3 s nas migrations.

## 5. Rotas e contratos HTTP

Não se aplica a este anexo: as rotas que escrevem os dados estão em [`rotas.md`](rotas.md). O contrato relevante aqui é o efeito de cada action sobre as tabelas, resumido em §1.2.

## 6. Dados e modelo de domínio

### 6.1 `v2_parceiros_institucionais`

| Coluna | Tipo | Null | Default | Origem | Quem escreve |
|---|---|---|---|---|---|
| `id` | uuid | não | — | `000002:31` | `creating` (app) |
| `name` | varchar(255) | não | — | `000002:32` | CMS/seeder |
| `slug` | varchar(255) | não | — | `000002:33` (unique) | hook `saving` (só quando vazio; updates preservam) |
| `description` | text | sim | — | `000002:34` | CMS (JSON TipTap)/seeder |
| `summary` | text | sim | — | `000040:17` | CMS (≤250)/seeder |
| `logo` | varchar(255) | sim | — | `000002:35` | **ninguém** (legado) |
| `logo_id` | varchar(36) | sim | — | `000010:17` | CMS/`PopulateMissingImages`/seeder |
| `url` | varchar(255) | sim | — | `000002:36` | CMS/seeder |
| `linkedin_url` | varchar(255) | sim | — | `000010:18` | hidden do form |
| `instagram_url` | varchar(255) | sim | — | `000010:19` | hidden do form |
| `facebook_url` | varchar(255) | sim | — | `000010:20` | hidden do form |
| `address` | text | sim | — | `000020:17` | CMS |
| `google_maps_url` | varchar(500) | sim | — | `000020:18` | CMS |
| `metadata` | jsonb | sim | — | `000030:17` | CMS (`social_media`)/seeder (`seo`) |
| `sort_order` | integer | não | 0 | `000002:37` | CMS (store/toggle/reorder)/seeder |
| `is_active` | boolean | não | true | `000002:38` | CMS/seeder |
| `created_by` | varchar(36) | sim | — | `000002:39` | store |
| `created_at`/`updated_at` | timestamp | sim | — | `000002:40` | Eloquent |
| `deleted_at` | timestamp | sim | — | `000002:41` | `delete()` |

**Índices**: PK `id`; `unique(v2_parceiros_institucionais_slug_unique)` em `slug`; `idx_v2_parceiros_institucionais_active` em `is_active`; `idx_v2_parceiros_institucionais_sort` em `sort_order` (`000002:33,43-44`). **Sem** índice em `deleted_at`, `name` ou `logo_id`.

### 6.2 `v2_parceiro_tag_pivot`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `parceiro_id` | uuid | não | — |
| `tag_id` | uuid | não | — |
| `is_primary` | boolean | não | false |
| `created_at`/`updated_at` | timestamp | sim | — |

PK composta `pk_v2_par_tag_pivot`; índices `idx_v2_par_pivot_parceiro` e `idx_v2_par_pivot_tag`; sem FKs (`V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:26-35`). `v2_conexao_tags` tem `id`, `name(100)`, `slug(100) unique`, `is_reserved` e a tag `geral` semeada (`:18-24,48-56`).

### 6.3 `v2_public_search_index` (derivado)

| Campo indexado | Valor para parceiro |
|---|---|
| `title` | `name` |
| `slug` | `slug` |
| `module` | `parceiros-institucionais` |
| `entity_type` | `V2\Modules\Conexoes\ParceirosInstitucionais\Models\Parceiro` |
| `entity_id` | `id` |
| `summary_fragment` | `description` cru (ou `Parceiro do portal`) |
| `tags` | `["parceiro","publicacao"]` fixos |
| `search_deep_content` | HTML da descrição sem tags |

Fonte: `Models/Parceiro.php:148-159`; upsert em `V2/Services/Search/Services/PublicIndexerService.php:21-61`.

### 6.4 Diagrama

```mermaid
erDiagram
  v2_parceiros_institucionais {
    uuid id PK
    varchar name
    varchar slug UK
    text description
    text summary
    varchar logo
    varchar logo_id
    jsonb metadata
    int sort_order
    boolean is_active
    varchar created_by
    timestamp deleted_at
  }
  v2_parceiro_tag_pivot {
    uuid parceiro_id PK
    uuid tag_id PK
    boolean is_primary
  }
  v2_conexao_tags {
    uuid id PK
    varchar name
    varchar slug UK
    boolean is_reserved
  }
  v2_parceiros_institucionais ||--o{ v2_parceiro_tag_pivot : "id"
  v2_conexao_tags ||--o{ v2_parceiro_tag_pivot : "id"
```

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Ativo
  Ativo --> Inativo: is_active=false / sort_order=-1 / remove índice
  Inativo --> Ativo: is_active=true / sort_order=max+1 / upsert índice
  Ativo --> Arquivado: deleted_at / remove índice
  Inativo --> Arquivado: deleted_at / remove índice
```

`FL-PARCEIROSINSTITUCIONAIS-04` (seed de demo): `count()>0` → aborta; senão 8 `save()` sequenciais, cada um indexando a busca (8 upserts) e criando slug.

`FL-PARCEIROSINSTITUCIONAIS-05` (populate de logos): seleciona `whereNull('logo_id')`, baixa imagem, `GalleryService::seedFromPath`, grava `logo_id` e `save()` → reindexa cada linha.

## 8. UI CMS

Não se aplica como tela; a UI que produz esses dados está em [`cms.md`](cms.md). O único contrato de dados vindo da UI é o formato de `metadata.social_media` e o array `tags[]`.

## 9. UI Portal

Não se aplica como tela; a leitura pública e os campos exibidos estão em [`portal.md`](portal.md).

## 10. Configuração, permissões e flags

| Chave | Default | Efeito sobre dados | Fonte |
|---|---|---|---|
| `cache.prefix` | `laravel` | define o padrão de chaves apagadas após escrita | `Controllers/ParceirosInstitucionaisCmsController.php:278` |
| `filesystems.media_disk` | default do FS | caminho/URL do logo via `StorageService` | `V2/Services/Storage/StorageService.php:33-43` |
| flag `ParceirosInstitucionais` | ativo quando ausente | desativar bloqueia escrita CMS (portal continua) | `V2/Core/Services/ModuleFlagService.php:93-94` |
| permissão `ParceirosInstitucionais` | `none` | controla leitura/escrita das rotas CMS | `V2/Core/Middleware/V2PermissionMiddleware.php:80-127` |

## 11. Observabilidade, erros e limites

- `Log::info` nas migrations de criação/adição/rename/cleanup e `Log::alert` ao renomear para backup (`000002:49,80`; `cleanup:41,44,46,73`).
- `Log::error` quando uma migration falha e faz rollback (`000002:53`; `000010:26`; `cleanup:52`).
- `Log::error` de indexação (`[Search] Public Indexing failed: …`) e de render (`[Parceiro] …`).
- Erros de banco não tratados: unique violation de slug, insert sem id, jsonb inválido.
- Limite de linha: `name` 255, `summary` 250 (validação) / text (coluna), `google_maps_url` 500, `logo_id` 36, tag `name` 100.

## 12. Seeds e dados iniciais

| Seeder | Tabelas | Registros | Idempotência | Ordem | Fonte |
|---|---|---|---|---|---|
| `DemoContentSeeder::seedParceiros` | `v2_parceiros_institucionais` (+ galeria) | 8 (6 com `logo_id`) | pula se `Parceiro::count() > 0` | após galeria/mídia | `V2/Seeds/Content/DemoContentSeeder.php:923-1030` |
| `ParceirosInstitucionaisSeeder` (`ParceirosSeeder`) | `v2_basic_pages` | 1 página HTML estática com 6 logos | upsert por `slug` | BasicPages | `V2/Seeds/BasicPages/ParceirosInstitucionaisSeeder.php:63-99` |
| `SucesuHomepageSeeder` | `v2_homepage_sections` | 1 seção `sucesu-associados` | conforme implementação do seeder | Homepage | `V2/Seeds/BasicPages/SucesuHomepageSeeder.php:310-335` |
| core `2026_08_18_000001` | `v2_conexao_tags` | tag `Geral` (`is_reserved=true`) | fixa na migration | antes do uso de tags | `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:48-56` |

## 13. Lacunas

- `LG-PARCEIROSINSTITUCIONAIS-06` — Não há registro de migração de dados de `logo` → `logo_id` para bases antigas; onde procurar: migrations de `SVC-GALLERY`/`SVC-MEDIA`, scripts de one-off. Impacto: médio (logos ausentes).
- `LG-PARCEIROSINSTITUCIONAIS-07` — Não existe histórico versionado das migrations que criavam `v2_parceiros` (módulo removido); a estrutura da tabela antiga só é inferida pelo nome. Onde procurar: dump de produção. Impacto: alto para decidir se o `DROP` da cleanup perde dados.
- `LG-PARCEIROSINSTITUCIONAIS-08` — O índice `unique` do slug é global e sem filtro parcial; confirmar no banco de produção se há índice adicional não versionado. Impacto: baixo.
- `LG-PARCEIROSINSTITUCIONAIS-09` — `v2_public_search_index` não tem DDL neste módulo; verificar o unique `(entity_type, entity_id)` no banco. Impacto: médio para o upsert.

## 14. Checklist de paridade

- [ ] `PC-PARCEIROSINSTITUCIONAIS-17` — Banco novo: após `migrate`, existe `v2_parceiros_institucionais` com 20 colunas e 3 índices, e **não** existe `v2_parceiros`. Fonte: `000002:30-45`; `cleanup:36-47`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-18` — Banco legado só com `v2_parceiros`: as linhas são herdadas pela tabela nova (rename), inclusive `id`s. Fonte: `cleanup:39-41`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-19` — Banco com as duas: `v2_parceiros` é dropada e o log `Tabela legada 'v2_parceiros' removida…` é emitido. Fonte: `cleanup:42-44`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-20` — `down()` da cleanup: a tabela ativa vira `backup_v2_parceiros_institucionais_<Y_m_d_His>` e nenhuma linha é apagada. Fonte: `cleanup:62-82`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-21` — Inserir via SQL sem `id` falha (not-null/PK); via model funciona com UUID v4. Fonte: `000002:31`; `Models/Parceiro.php:54-58`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-22` — `syncTagsFromArray(['Ouro','Prata'])` cria 2 tags e 2 pivôs com `is_primary` só em Ouro; segunda chamada com `['Prata']` remove o pivô de Ouro e deixa Prata com `is_primary=1`. Fonte: `Models/Parceiro.php:108-124`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-23` — `metadata` com `seo` + `social_media`: salvar pelo CMS preserva `seo` e substitui apenas `social_media`. Fonte: controller `:86-89,158-162`.
- [ ] `PC-PARCEIROSINSTITUCIONAIS-24` — Arquivar não apaga `v2_parceiro_tag_pivot`. Fonte: `Controllers/...CmsController.php:185-200`; core migration `:26-35`.

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `V2/Modules/Conexoes/ParceirosInstitucionais/Migrations/2024_01_01_000002_create_v2_parceiros_table.php` | 1-91 |
| `.../2024_01_01_000010_add_social_and_logo_fields_to_v2_parceiros_table.php` | 1-37 |
| `.../2024_01_01_000020_add_address_fields_to_v2_parceiros_table.php` | 1-35 |
| `.../2024_01_01_000030_add_metadata_to_v2_parceiros_table.php` | 1-35 |
| `.../2024_01_01_000040_add_summary_to_v2_parceiros_table.php` | 1-35 |
| `.../2026_07_31_000000_rename_v2_parceiros_to_v2_parceiros_institucionais.php` | 1-36 |
| `.../2026_08_06_000000_cleanup_old_parceiros_table.php` | 1-83 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Models/Parceiro.php` | 1-176 |
| `V2/Modules/Conexoes/ParceirosInstitucionais/Controllers/ParceirosInstitucionaisCmsController.php` | 54-250 |
| `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php` | 1-85 |
| `V2/Core/Models/ConexaoTag.php` | 1-56 |
| `V2/Core/Controllers/ConexaoTagCmsController.php` | 1-111 |
| `V2/Services/Search/Services/PublicIndexerService.php` | 1-78 |
| `V2/Seeds/Content/DemoContentSeeder.php` | 923-1030 |
| `V2/Seeds/BasicPages/ParceirosInstitucionaisSeeder.php` | 1-122 |
| `V2/Services/Gallery/Console/Commands/PopulateMissingImages.php` | 100-118 |
| `V2/Core/Helpers/helpers.php` | 55-70 |
