---
id: MOD-CONEXOES-ASSOCIADOS-DADOS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-ASSOCIADOS]
fontes:
  - V2/Modules/Conexoes/Associados/
depende_de: [CORE-DATA, CORE-BOOT, MOD-CONEXOES-UNIDADEREGIONAL, SVC-GALLERY, SVC-SEARCH]
consumidores: [MOD-CONEXOES-ASSOCIADOS, MOD-CONEXOES-UNIDADEREGIONAL, SVC-SEARCH, UI-CMS, UI-PORTAL]
criticidade: alta
dono: Associados
---

# Conexões/Associados — dados e modelo

> Cinco tabelas próprias (`v2_associado_instituicoes`, `v2_associado_instituicao_gallery`,
> `v2_associado_tags`, `v2_associado_tag_pivot` e a tabela legada `v2_chapters`
> arquivada), um pivô compartilhado (`v2_unidade_regional_empresas`) e uma projeção
> (`v2_public_search_index`). Nada de `type`, nada de `api_token`: as duas migrations
> que prometem esses campos são no-op.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Persistir instituições conveniadas, suas mídias, tags e vínculos regionais, de forma que o portal consiga listar somente ativas e o CMS consiga escopar por unidade regional. |
| Entrada | `save()`/`delete()`/`restore()` do model, `sync()` do pivô de tags, inserts do controller para galeria e vínculo regional, migrations e seeder. |
| Saída | Linhas UUID nas tabelas `v2_associado_*`, projeção em `v2_public_search_index`, JSON `metadata` com benefícios/sociais/SEO. |
| Efeitos colaterais | Slug derivado do nome apenas quando vazio (unicidade garantida no save); índice público atualizado/removido; `activity_log` via controller; backup por rename no `down()` das migrations. |
| Particularidade central | `gallery_media_id` é `varchar(36)` e guarda o id **inteiro** de `v2_gallery_media`, em paralelo ao path textual em `image`; a coluna não é FK. |
| Como se integra | Eloquent puro, sem repositório; `AssociadoInstituicao implements RegionalOwnable`; relações `gallery`, `tags`, `primaryTag`, `unidadesRegionais`. |
| Se removido, quebra | Portal (`/associados`, `/associado/{slug}`), API regional de associados, índice público do módulo `associados`, contadores do modal de tags do Core (não afetados, pois usam outro catálogo). |
| Superfície pública | Models e migrations listados na §1; helpers `v2_uuid()` e `str_slug()`. |

## 1. Efeito

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

- Cadastrar/editar uma instituição com um registro principal e N mídias de galeria.
- Classificar por tags próprias (N:N) e por uma única tag primária.
- Vincular a instituição a uma unidade regional (ou várias, via API).
- Arquivar (soft delete) e restaurar sem perder a linha.

### 1.2 O que muda no sistema

| Efeito | Tabela/arquivo | Fonte |
|---|---|---|
| Cria/atualiza o registro principal, com UUID e slug único | `v2_associado_instituicoes` | `V2/Modules/Conexoes/Associados/Models/AssociadoInstituicao.php:177-202` |
| Cria a tag ausente e substitui o conjunto do pivô | `v2_associado_tags`, `v2_associado_tag_pivot` | `AssociadoInstituicao.php:231-252` |
| Cria/atualiza o vínculo regional | `v2_unidade_regional_empresas` | `V2/Modules/Conexoes/Associados/Controllers/InstituicaoCmsController.php:374-396` |
| Insere linha de galeria com path e id de mídia | `v2_associado_instituicao_gallery` | `InstituicaoCmsController.php:333-339,465-471` |
| Remove vínculos de galeria fora da lista | `v2_associado_instituicao_gallery` | `InstituicaoCmsController.php:448-451` |
| Indexa/remove no portal | `v2_public_search_index` | `AssociadoInstituicao.php:204-218,254-300` |
| Arquiva a tabela legada de capítulos | `v2_chapters` → `backup_v2_chapters_<timestamp>` | `Migrations/2024_01_01_000013_archive_v2_chapters_table.php:17-31` |
| Semeia o catálogo inicial de tags | `v2_associado_tags` | `Migrations/2024_01_01_000019_reset_tags_to_default_categories.php:21-44` |

### 1.3 Contrato observável (entrada → saída)

| Operação | Resultado |
|---|---|
| `new AssociadoInstituicao(['name' => 'X'])->save()` | `id` UUID v4, `slug = 'x'` (ou `x-1`), `is_active = true` por default de banco, timestamps; `saved` indexa |
| Renomear para `Y` e salvar | `slug` permanece o da criação (`x`); a URL antiga continua válida |
| `syncTagsFromArray(['A','B'])` | tag A com `is_primary=1`, tag B com `0`; vínculos anteriores removidos |
| `delete()` | `deleted_at` preenchido; `deleted` remove do índice; linha permanece |
| `restore()` | `deleted_at = null`; **não** dispara `saved`; índice continua sem o registro |
| Insert de galeria sem `gallery_media_id` | linha válida (coluna nullable), invisível ao sync por IDs |

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

- Estrutura interna de `v2_gallery_media` (ver SVC-GALLERY) e de `v2_unidade_regionais` (ver MOD-CONEXOES-UNIDADEREGIONAL); aqui só o que este módulo escreve/lê.
- Trigger de `search_vector` do índice público (ver SVC-SEARCH).

## 2. Abordagem

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

```mermaid
erDiagram
  v2_associado_instituicoes {
    uuid id PK
    varchar name
    varchar slug UK
    text description
    text convenio_description
    varchar logo
    varchar cover_image
    varchar url
    varchar linkedin_url
    varchar instagram_url
    varchar facebook_url
    varchar discount_badge
    text benefits_list
    int sort_order
    boolean is_active
    jsonb metadata
    varchar created_by
    timestamp created_at
    timestamp updated_at
    timestamp deleted_at
  }
  v2_associado_instituicao_gallery {
    uuid id PK
    uuid instituicao_id FK
    varchar image
    varchar gallery_media_id
    int sort_order
  }
  v2_associado_tags {
    uuid id PK
    varchar name
    varchar slug UK
  }
  v2_associado_tag_pivot {
    uuid associado_id PK
    uuid tag_id PK
    boolean is_primary
  }
  v2_associado_instituicoes ||--o{ v2_associado_instituicao_gallery : "1:N"
  v2_associado_instituicoes ||--o{ v2_associado_tag_pivot : "1:N"
  v2_associado_tags ||--o{ v2_associado_tag_pivot : "1:N"
```

A modelagem separa três concerns: **conteúdo** (instituição), **mídia** (galeria por referência) e **classificação** (tags + regional). Benefícios, redes sociais e SEO ficam em `metadata` (jsonb), justamente para não exigir migrations a cada novo campo de apresentação.

### 2.2 Mecânica passo a passo

1. `AssociadoInstituicao::boot()` registra `creating` (id e slug ausente), `saving` (derivação do slug vazio e unicidade), `saved` (busca) e `deleted` (remoção da busca) (`Models/AssociadoInstituicao.php:175-218`).
2. O controller atribui os escalares e os acessores de `metadata` (`benefits`, `social_media`), resolve logo/capa e salva (`InstituicaoCmsController.php:133-158`).
3. Tags: `syncTagsFromArray` normaliza, faz `firstOrCreate` por slug e `sync` no pivô (`Model:231-252`).
4. Regional: `syncUnidadeRegional` insere/reaponta a linha do pivô.
5. Galeria: linhas independentes, com `sort_order` incremental.
6. Portal consulta com `active()->ordered()` ou filtros; nunca lê `metadata` por SQL, apenas o cast `array`.

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

- **UUID em PHP, sem default** `[C]`: todas as migrations usam `$table->uuid('id')->primary()` sem `default`; o id é gerado por `v2_uuid()` no model/controller. Impõe que todo insert fora do Eloquent gere id manualmente.
- **Slug estável após a criação** `[C]`: o `saving` só deriva o slug quando ele está vazio; renomear preserva a URL. Como a coluna é única, colisões recebem `-N`.
- **Soft delete + flag** `[C]`: dois estados independentes (`deleted_at` e `is_active`); o portal lê só a flag, o CMS de arquivadas lê só o soft delete.
- **Metadados sem schema** `[I]`: `metadata` jsonb concentra `benefits`, `social_media`, `logo_id`, `cover_image_id`, `seo` e as chaves `source_*` da API; não há validação de forma.
- **Galeria desnormalizada** `[I]`: guardar `image` além de `gallery_media_id` permite renderizar mesmo se a mídia da Galeria for removida; o custo é ficar com paths órfãos.

### 2.4 Trechos de código pivô

```php
// V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000004_create_v2_associado_instituicoes_table.php:18-36 — tabela base
Schema::create('v2_associado_instituicoes', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name', 255);
    $table->string('slug', 255)->unique();
    $table->text('description')->nullable();
    $table->text('convenio_description')->nullable();
    $table->string('logo', 255)->nullable();
    $table->string('url', 255)->nullable();
    $table->string('discount_badge', 255)->nullable();
    $table->text('benefits_list')->nullable();
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->string('created_by', 36)->nullable();
    $table->timestamps();
    $table->softDeletes();

    $table->index('is_active', 'idx_v2_asc_inst_active');
    $table->index('sort_order', 'idx_v2_asc_inst_sort');
});
```

Observe que `logo` e `url` já nascem na tabela; `cover_image`, os três campos sociais e `metadata` chegam em migrations posteriores.

```php
// V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000016_add_category_and_tags_to_associado_instituicoes.php:17-33 — catálogo próprio
Schema::create('v2_associado_tags', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('name', 100);
    $table->string('slug', 100)->unique();
    $table->timestamps();
});

Schema::create('v2_associado_tag_pivot', function (Blueprint $table) {
    $table->uuid('associado_id');
    $table->uuid('tag_id');
    $table->boolean('is_primary')->default(false);
    $table->timestamps();

    $table->primary(['associado_id', 'tag_id'], 'pk_v2_asc_tag_pivot');
    $table->index('associado_id', 'idx_v2_asc_pivot_associado');
    $table->index('tag_id', 'idx_v2_asc_pivot_tag');
});
```

Apesar do nome da migration, a coluna `category` **não** é criada aqui; a migration `000020` só a remove se existir. O pivô não declara FKs — a integridade depende do Eloquent.

```php
// V2/Modules/Conexoes/Associados/Models/AssociadoInstituicao.php:54-82 — relações
public function gallery()
{
    return $this->hasMany(AssociadoInstituicaoGallery::class, 'instituicao_id', 'id')->orderBy('sort_order');
}

public function tags()
{
    return $this->belongsToMany(AssociadoTag::class, 'v2_associado_tag_pivot', 'associado_id', 'tag_id')
        ->withPivot('is_primary')
        ->withTimestamps()
        ->orderBy('v2_associado_tag_pivot.is_primary', 'desc');
}

public function primaryTag()
{
    return $this->belongsToMany(AssociadoTag::class, 'v2_associado_tag_pivot', 'associado_id', 'tag_id')
        ->withPivot('is_primary')
        ->wherePivot('is_primary', true);
}

public function unidadesRegionais()
{
    return $this->belongsToMany(UnidadeRegional::class, 'v2_unidade_regional_empresas', 'empresa_id', 'unidade_regional_id')
        ->withPivot('status')->withTimestamps();
}
```

`primaryTag` é uma relação separada porque o portal precisa agrupar por ela com eager loading (`with(['primaryTag'])`). `unidadesRegionais` não filtra `status`, logo um vínculo `inactive` continua contando para escopo e exibição.

```php
// V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000011_add_gallery_media_id_to_associado_instituicao_gallery.php:15-20 — referência frouxa
Schema::table('v2_associado_instituicao_gallery', function (Blueprint $table) {
    $table->string('gallery_media_id', 36)->nullable()->after('image');
});
```

O id da Galeria é `increments` (inteiro) em `v2_gallery_media`, mas aqui é armazenado como string de até 36 chars, sem FK. O sync converte para `int` apenas para validar e comparar.

### 2.5 Algoritmos, consultas e custos

- `active()->ordered()`: `WHERE is_active = true ORDER BY sort_order ASC, name ASC`; usa `idx_v2_asc_inst_active` e `idx_v2_asc_inst_sort`.
- `whereHas('tags')`: `EXISTS` sobre o pivô; índices `idx_v2_asc_pivot_associado` e `idx_v2_asc_pivot_tag`.
- `withCount('tags as matched_tags_count')`: subquery correlacionada; sem índice específico para a contagem, mas o pivô é pequeno por instituição.
- Galeria: `count(*)` por item no insert (N+1 no upload/sync); leitura sempre `ORDER BY sort_order`.
- `syncToSearch` faz uma query de regionais por save; o índice público faz UPSERT por `(entity_type, entity_id)`.
- `activity_log` recebe um insert por operação (via pacote de activity), com `properties` json.

## 3. Particularidades

### 3.1 O que só existe aqui

- **Pivô de tags próprio** com coluna `is_primary`, paralelo a `v2_parceiro_tag_pivot` e `v2_unidade_regional_tag_pivot` (que apontam para `v2_conexao_tags`).
- **Duas fontes para a mesma imagem**: `logo`/`cover_image` (path) + `metadata.*_id` (id da Galeria).
- **`benefits` híbrido**: `benefits_list` (texto multilinha, legado/API) e `metadata.benefits` (array nome+link, CMS).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-ASSOCIADOS-D01` | `slug` é único e calculado apenas quando vazio | colisão resolvida com `-N`; rename **não** muda URL | `Models/AssociadoInstituicao.php:186-202` |
| `RN-ASSOCIADOS-D02` | `is_active` default `true` no banco; `destroy` grava `false` | restauração não reativa | `Migrations/...000004:29`; `Controllers/InstituicaoCmsController.php:275-277` |
| `RN-ASSOCIADOS-D03` | Uma única tag primária por instituição (a primeira do array) | agrupamento do portal depende da ordem | `Models/AssociadoInstituicao.php:234-250` |
| `RN-ASSOCIADOS-D04` | O pivô de tags não tem FK e `sync` pode deixar tags sem uso | catálogo acumula; exclusão de tag não é cascateada | `Migrations/...000016:24-33` |
| `RN-ASSOCIADOS-D05` | `v2_associado_instituicao_gallery.gallery_media_id` é string sem FK | mídia pode ser apagada e deixar referência morta | `Migrations/...000011:17` |
| `RN-ASSOCIADOS-D06` | `metadata` é jsonb com cast `array`; acessores leem/gravam chaves | sobrescrever `metadata` inteiro apaga benefícios/sociais | `Models/AssociadoInstituicao.php:43-46,131-173` |
| `RN-ASSOCIADOS-D07` | `v2_unidade_regional_empresas` tem unique `(unidade_regional_id, empresa_id)` e soft delete | reinserção após soft delete viola a unique | `V2/Modules/Conexoes/UnidadeRegional/Migrations/2026_07_23_000002:25-27`; `2026_07_31_000001:17-19` |
| `RN-ASSOCIADOS-D08` | Nenhuma coluna `type` ou `api_token` em instituições | escopo/token vêm da regional | `Migrations/2024_01_01_000012:6-23`; `000014:6-23` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **`str_slug('')`**: nome composto só por símbolos/não latinos pode gerar slug vazio; a unique aceita um único `''`, o segundo insert falha.
- **Nome de tag > 100**: slug pode passar de 100 e o Postgres rejeita (`varchar(100)`).
- **`benefits_list` com `\n`**: o portal quebra em linhas e remove vazias; `\r\n` deixa `\r` no texto.
- **`metadata` nulo**: os acessores usam `?? []`; gravar `benefits` cria o array, mas não preserva `seo` se o caller setar `metadata` inteiro.
- **Galeria legada**: linhas criadas antes de `gallery_media_id` têm `NULL` e nunca são removidas pelo sync; aparecem no portal via `image`.
- **Ordem de migrations**: `000016` (tags) precisa vir antes de `000019` (seed de tags); `000020` (drop category) é defensiva e não falha se a coluna não existe.
- **`down()` preserva dados**: a maioria renomeia para `backup_<tabela>_<data>` em vez de dropar; exceções: tags/pivô (`000016` usa `dropIfExists`) e `v2_chapters` (down vazio).
- **Soft delete não cascateia**: a FK `ON DELETE CASCADE` da galeria só age em hard delete; arquivar a instituição mantém as linhas de `v2_associado_instituicao_gallery` (`Migrations/2024_01_01_000005:25-27`).

### 3.4 Armadilhas de reimplementação

- **Criar `category` de novo**: o código lê `category` em um ponto da API regional, mas a coluna não existe; replicar a coluna cria divergência com a migration que a remove.
- **Assumir FK em `associado_id`/`tag_id`**: não há; deletar tag diretamente deixa pivô órfão.
- **Assumir que `restore()` reindexa**: o hook é `saved`, não `restored`.
- **Ignorar `sort_order`**: a listagem pública e o CMS ordenam por ele antes do nome; sem valor, todos ficam `0`.
- **Tratar `gallery_media_id` como inteiro no schema**: o tipo real é varchar; comparações com `whereIn` de ints funcionam por cast implícito, mas o valor gravado é string.

### 3.5 Divergências documentação × código e dívidas

- **Migration `000012` (type/regionais)** e **`000014` (api_token)** são no-op e contradizem o nome do arquivo; o comentário interno explica que os campos foram para `v2_unidade_regionais` (`Migrations/000012:6-23`; `000014:6-23`).
- **Migration `000016`** chama-se "add_category_and_tags" mas não adiciona `category`; a `000020` remove uma coluna que pode nunca ter existido.
- **API regional** serializa `$empresa->category` (sempre `null`) e `benefits_list` (legado) — resíduos do modelo antigo.
- **Sem FKs** no pivô de tags e em `gallery_media_id`: dívida de integridade referencial.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Retorno | Precondição |
|---|---|---|---|
| `AssociadoInstituicao::scopeActive($q)` | builder | builder | — |
| `::scopeOrdered($q)` | builder | builder | — |
| `::scopeByUnidadeRegional($q, $unidadeId)` | builder | builder | pivô populado |
| `::belongsToUnidadeRegional($unidadeId)` | `string` | `bool` | model existe |
| `::getUnidadeRegionalId()` | — | `?string` | model existe |
| `::syncTagsFromArray(array $names)` | — | `$this` | nome string |
| `::syncTagsFromString(string $csv)` | — | `$this` | vírgulas |
| `::syncToSearch()` | — | `void` | tabela de índice |
| `AssociadoInstituicaoGallery::galleryMedia()` | — | `BelongsTo` | id válido |
| `AssociadoTag::associados()` | — | `BelongsToMany` | — |

### 4.2 Contrato de UI/JS

Não há JS de dados; o formulário envia `tags[]`, `benefits[i][name|link]`, `social_media_json` (string JSON), `logo_id`, `cover_image_id` e `convenios_gallery_ids[]`. O componente `media-group-input` produz o nome `<name>_ids[]` a partir de `name="convenios_gallery"` (`V2/Services/Media/Views/components/media-group-input.blade.php:20,40-44`).

### 4.3 Ordem e ciclo de vida

1. Migrations do módulo (carregadas por `loadMigrationsFrom`) na ordem dos nomes: `000004 → 000005 → 000011 → 000012 → 000013 → 000014 → 000015 → 000016 → 000017 → 000018 → 000019 → 000020`.
2. `000016` cria o catálogo; `000019` trunca e semeia 3 tags. Qualquer seed anterior de tags é perdida.
3. Ao subir a aplicação, `ModuleProvider` é instanciado e o model não tem hook de boot global além dos eventos Eloquent.
4. Escrita: `saving` → `creating` (novo) → `saved` → efeitos do controller.

### 4.4 Receitas de uso

**(a) Relacionamento com tag primária**

```php
$instituicao = AssociadoInstituicao::with(['tags', 'primaryTag'])->findOrFail($id);
$primaria = $instituicao->primaryTag->first(); // pode ser null
```

**(b) Vínculo regional idempotente fora do controller**

```php
UnidadeRegionalEmpresa::firstOrCreate(
    ['unidade_regional_id' => $urId, 'empresa_id' => $inst->id],
    ['id' => v2_uuid(), 'status' => 'active']
);
```

Atenção: `firstOrCreate` não enxerga soft-deleted; para reativar, use `withTrashed()->first()` e `restore()`.

**(c) Ler benefícios de forma compatível com os dois formatos**

```php
$beneficios = $inst->benefits; // metadata.benefits: [['name'=>..., 'link'=>...]]
$linhas = array_filter(array_map('trim', explode("\n", $inst->benefits_list ?? '')));
```

### 4.5 Anti-uso

- **`$inst->metadata = ['logo_id' => $id]`**: apaga `benefits`, `social_media` e `seo`. Use o accessor ou faça merge.
- **`AssociadoTag::find($id)->delete()`**: deixa o pivô órfão (sem FK/cascade) e a instituição passa a ter uma tag inexistente na relação.
- **`DB::table('v2_associado_instituicoes')->insert([...])` sem id**: falha por PK nula; o default de UUID não existe no banco.

### 4.6 Custos e limites da integração

- `metadata` jsonb sem schema pode crescer sem limite; não há índice GIN nesta tabela (só em `v2_public_search_index`).
- `syncToSearch` executa a cada save, inclusive em updates que não mudam conteúdo (arquivar dispara duas vezes: `save` + `delete`).
- `v2_associado_tag_pivot` cresce com a combinação instituição×tag; `is_primary` não tem índice parcial.
- Nenhuma operação de escrita é transacional por padrão no controller.

## 5. Rotas e contratos HTTP

As rotas não mudam o modelo além do descrito; o mapeamento completo está em [rotas.md](rotas.md). Para dados, os pontos relevantes são:

| Rota | Efeito em dados |
|---|---|
| `POST /cms/associados/instituicoes/nova` | insert em `v2_associado_instituicoes` + tags + pivô regional |
| `PUT/POST /cms/associados/instituicoes/editar/{id}` | update + sync de tags/regional/galeria |
| `DELETE /cms/associados/instituicoes/{id}` | `is_active=false` + `deleted_at` |
| `POST .../{id}/restaurar` | `deleted_at=null` (sem reindexar) |
| `POST .../{id}/gallery` e `/sync` | insert/delete em `v2_associado_instituicao_gallery` |
| `POST /cms/associados/salvar-imagem-fundo` | upsert em `v2_portal_configs` (`theme_associados_bg`) |

## 6. Dados e modelo de domínio

### 6.1 `v2_associado_instituicoes`

| Coluna | Tipo | Null/default | Observação |
|---|---|---|---|
| `id` | uuid | PK | gerado em PHP |
| `name` | varchar(255) | não | — |
| `slug` | varchar(255) | não, unique | derivado do nome apenas quando vazio; rename preserva o valor |
| `description` | text | null | resumo do card; sem limite server-side |
| `convenio_description` | text | null | JSON Tiptap (ou Markdown em dados antigos) |
| `logo` | varchar(255) | null | path no disco de mídia |
| `cover_image` | varchar(255) | null | adicionada depois de `logo` |
| `url` | varchar(255) | null | site |
| `linkedin_url`, `instagram_url`, `facebook_url` | varchar(255) | null | legado; o CMS usa `metadata.social_media` |
| `discount_badge` | varchar(255) | null | texto livre |
| `benefits_list` | text | null | legado; quebrado por `\n` no portal |
| `sort_order` | integer | `0` | ordenação |
| `is_active` | boolean | `true` | visibilidade pública |
| `metadata` | jsonb | null | `benefits`, `social_media`, `logo_id`, `cover_image_id`, `seo`, `source*` |
| `created_by` | varchar(36) | null | id Sentinel |
| `created_at/updated_at` | timestamps | null | — |
| `deleted_at` | timestamp | null | soft delete |

Índices: `idx_v2_asc_inst_active (is_active)`, `idx_v2_asc_inst_sort (sort_order)`, unique implícito em `slug`. Sem FK. Escrita: controller CMS, seeder, API regional (`createEmpresa`, `updateAssociado`) e `destroy` da API.

### 6.2 `v2_associado_instituicao_gallery`

| Coluna | Tipo | Null/default | Observação |
|---|---|---|---|
| `id` | uuid | PK | — |
| `instituicao_id` | uuid | não | FK → `v2_associado_instituicoes.id` `ON DELETE CASCADE` |
| `image` | varchar(255) | não | path copiado da mídia |
| `gallery_media_id` | varchar(36) | null | id inteiro da mídia, como string |
| `sort_order` | integer | `0` | posição |
| `created_at/updated_at` | timestamps | null | — |

Índice `idx_v2_asc_gal_instituicao`. Sem unique: a mesma mídia pode ser vinculada duas vezes se o sync falhar. Escrita: `uploadGallery`/`syncGalleryMediaIds`; remoção: sync e `destroyGallery`.

### 6.3 `v2_associado_tags` e `v2_associado_tag_pivot`

`v2_associado_tags`: `id uuid PK`, `name varchar(100)`, `slug varchar(100) unique`, timestamps. Sem `is_reserved`, sem soft delete.
`v2_associado_tag_pivot`: `associado_id uuid`, `tag_id uuid`, `is_primary boolean default false`, timestamps; PK composta `(associado_id, tag_id)`; índices por `associado_id` e `tag_id`. Sem FKs.

Escrita: `firstOrCreate` + `sync` em `syncTagsFromArray`. Leitura: relação `tags` (ordenada por `is_primary DESC`, sem ordem secundária) e `primaryTag`.

### 6.4 `v2_unidade_regional_empresas` (compartilhada)

`id uuid PK`, `unidade_regional_id uuid`, `empresa_id uuid`, `status varchar(20) default 'active'`, `metadata jsonb null`, timestamps, `deleted_at` (soft delete). Unique `(unidade_regional_id, empresa_id)`; índices por cada FK. O módulo escreve `status='active'` e lê via `unidadesRegionais`.

### 6.5 `v2_chapters` (legada)

Arquivada por rename para `backup_v2_chapters_<YmdHis>` na migration `000013`; índices `idx_v2_chapters_{slug,active,state,order}` são dropados antes. `down()` é vazio. Nenhum model do módulo referencia a tabela.

### 6.6 `v2_conexao_tags` (comparação)

`id uuid PK`, `name varchar(100)`, `slug varchar(100) unique`, `is_reserved boolean default false`, timestamps. Pivôs `v2_parceiro_tag_pivot` e `v2_unidade_regional_tag_pivot`. Fonte real das tags de **Parceiros** e **Unidades Regionais**; **não** das instituições associadas (`V2/Core/Migrations/2026_08_18_000001:18-46`).

### 6.7 `v2_public_search_index` (projeção)

O módulo grava `module='associados'`, `entity_type=AssociadoInstituicao::class`, `entity_id`, `title=name`, `slug`, `summary=description ?: 'Instituição associada'`, `tags` (JSON com `associado`, `convenio`, `instituicao`, `regional`, sigla/slug/UF) e `search_deep_content` (descrição + texto do Tiptap + `benefits_list` + nomes das regionais) (`Models/AssociadoInstituicao.php:266-300`; `V2/Services/Search/Services/PublicIndexerService.php:21-56`).

## 7. Fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> Ativa
  Ativa --> Arquivada: delete()
  Arquivada --> Arquivada: restore()  %% is_active permanece false
  Arquivada --> Ativa: update(is_active=true)
  Ativa --> Ativa: update()
```

`FL-ASSOCIADOS-D01` — insert: `saving` deriva o slug e garante unicidade, `creating` completa o id; `saved` indexa; controller complementa tags/regional.
`FL-ASSOCIADOS-D02` — update: `saving` preserva o slug existente e revalida a unicidade; `saved` reindexa; sync de galeria pode apagar linhas.
`FL-ASSOCIADOS-D03` — archive: `save(is_active=false)` dispara `saved` (remove do índice) e `delete()` dispara `deleted` (remove de novo) — remoção idempotente.
`FL-ASSOCIADOS-D04` — restore: só `deleted_at` volta; nenhum evento de indexação.

## 8. UI CMS

O formulário não expõe as colunas legadas `linkedin_url`/`instagram_url`/`facebook_url`/`benefits_list`/`sort_order` como campos editáveis; usa `metadata` e hiddens. Detalhe de campos em [cms.md](cms.md).

## 9. UI Portal

As views leem `gallery` (ordenada), `galleryMedia.file_path`/`variants`, `logo`, `description`, `benefits_list`, `benefits`, `social_media`, `tags`. O fallback de imagem é `galleryMedia ? file_path : image` (`Views/portal/detalhe.blade.php:117-127`; `Views/portal/instituicao.blade.php:268-289`).

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

Não há configuração de banco específica do módulo. O comportamento de dados depende de:

| Chave | Efeito |
|---|---|
| `filesystems.media_disk` | disco usado por `GalleryService::upload` (default `public`) |
| `cache.prefix` | prefixo do padrão `page:*` |
| `v2.unidade_regional.api_token_ttl_days` | expiração do token da regional (default 365) |

Permissões: `Associados` `read|write` (ver [rotas.md](rotas.md)).

## 11. Observabilidade, erros e limites

- Falha de índice é logada (`[AssociadoInstituicao] Falha ao sincronizar busca`) e não interrompe o save.
- Falha de remoção é logada (`Falha ao remover do índice público`).
- Falha de cache é `Log::warning` e segue.
- Erros de banco prováveis: unique de `slug`, unique do pivô regional, varchar(100) de tag/slug, JSON inválido em `metadata` (cast falha silenciosamente? `array` cast de string inválida retorna `null`).
- Sem transações: create com tags pode criar a instituição e falhar nas tags, deixando registro sem classificação.

## 12. Seeds e dados iniciais

- `2024_01_01_000019`: `TRUNCATE` em `v2_associado_tag_pivot` e `v2_associado_tags`, seguido de 3 inserts (`Tecnologia`, `Dados`, `Inovação`) com UUIDs novos (`:21-44`). Rodar de novo apaga vínculos.
- `DemoContentSeeder::seedAssociados()`: idempotente por contagem (`if (AssociadoInstituicao::count() > 0) return;`), 10 registros, uma tag por registro, vínculo regional por UF (`V2/Seeds/Content/DemoContentSeeder.php:753-916`).

## 13. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| `LG-ASSOCIADOS-D01` | Não há migration que crie `category`; não se sabe se existiu em produção. | Reimplementação pode ou não precisar da coluna. | histórico do banco |
| `LG-ASSOCIADOS-D02` | Cardinalidade real de tags/instituições desconhecida. | Escolha de índices e cache. | produção |
| `LG-ASSOCIADOS-D03` | Não há constraint garantindo uma única `is_primary` por instituição. | Dados inconsistentes podem ter 0 ou N primárias. | banco/produção |
| `LG-ASSOCIADOS-D04` | `v2_chapters` não tem model nem documentação de uso. | Arquivamento pode esconder dependência morta. | busca global por `chapters` |

## 14. Checklist de paridade

- [ ] `PC-ASSOCIADOS-D01` — `slug` único com sufixo numérico em colisão; rename **não** altera o slug. Fonte: `AssociadoInstituicao.php:186-202`.
- [ ] `PC-ASSOCIADOS-D02` — arquivar grava `is_active=false` e `deleted_at`. Fonte: `InstituicaoCmsController.php:275-277`.
- [ ] `PC-ASSOCIADOS-D03` — restaurar não altera `is_active`. Fonte: `:89-104`.
- [ ] `PC-ASSOCIADOS-D04` — `syncTagsFromArray` marca só a primeira como primária e remove as demais do pivô. Fonte: `Model:231-252`.
- [ ] `PC-ASSOCIADOS-D05` — `metadata.benefits` e `metadata.social_media` sobrevivem a updates de logo/capa. Fonte: `InstituicaoCmsController.php:398-438`.
- [ ] `PC-ASSOCIADOS-D06` — `gallery_media_id` é varchar e aceita nulo; sync ignora nulos. Fonte: `Migrations/...000011:17`; `:448-451`.
- [ ] `PC-ASSOCIADOS-D07` — vínculo regional único por par e soft-deletável. Fonte: `2026_07_23_000002:25-27`; `2026_07_31_000001:17-19`.
- [ ] `PC-ASSOCIADOS-D08` — migrations `000012` e `000014` não alteram o schema. Fonte: `:6-23` de ambas.

## 15. Evidências consultadas

- `V2/Modules/Conexoes/Associados/Migrations/2024_01_01_000004_create_v2_associado_instituicoes_table.php:1-77`
- `.../2024_01_01_000005_create_v2_associado_instituicao_gallery_table.php:1-71`
- `.../2024_01_01_000011_add_gallery_media_id_to_associado_instituicao_gallery.php:1-34`
- `.../2024_01_01_000012_add_type_and_regionais_fields_to_associado_instituicoes.php:1-24`
- `.../2024_01_01_000013_archive_v2_chapters_table.php:1-48`
- `.../2024_01_01_000014_add_api_token_to_associado_instituicoes.php:1-24`
- `.../2024_01_01_000015_add_metadata_to_associado_instituicoes.php:1-54`
- `.../2024_01_01_000016_add_category_and_tags_to_associado_instituicoes.php:1-61`
- `.../2024_01_01_000017_add_social_fields_to_associado_instituicoes.php:1-50`
- `.../2024_01_01_000018_add_cover_image_to_associado_instituicoes.php:1-48`
- `.../2024_01_01_000019_reset_tags_to_default_categories.php:1-57`
- `.../2024_01_01_000020_drop_category_from_associado_instituicoes.php:1-63`
- `V2/Modules/Conexoes/Associados/Models/{AssociadoInstituicao,AssociadoInstituicaoGallery,AssociadoTag}.php`
- `V2/Modules/Conexoes/UnidadeRegional/Migrations/2026_07_23_000001_create_v2_unidade_regionais_table.php:17-47`
- `.../2026_07_23_000002_create_v2_unidade_regional_empresas_table.php:17-28`
- `.../2026_07_31_000001_add_soft_deletes_to_v2_unidade_regional_empresas_table.php:17-19`
- `V2/Core/Migrations/2026_08_18_000001_create_v2_conexao_tags_table.php:18-46`
- `V2/Seeds/Content/DemoContentSeeder.php:753-916`
