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

# Conexões/Associados — rotas e contratos HTTP

> Dezesseis rotas declaradas (treze CMS, três públicas), sem FormRequest e sem
> route-model binding. A autenticação é a sessão Sentinel via `CmsAuthenticate`, que
> encadeia o RBAC do módulo `Associados`; três rotas CMS são ainda bloqueadas para
> operador regional. As rotas públicas herdam `page.cache` (Redis, 5 min) e
> `public.cache` (`Cache-Control: public, max-age=60`).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Expor o CRUD CMS de instituições (com galeria e fundo) e as três URLs públicas que listam, fragmentam e renderizam a instituição. |
| Entrada | Verbo + URI + parâmetros de §5; formulários `application/x-www-form-urlencoded`/`multipart` (galeria) e query string no portal. |
| Saída | HTML, fragmento HTML, JSON `{success,count}`, `302` com flash, `403`, `404`, `419`, `422`. |
| Efeitos colaterais | Escrita nas tabelas do módulo, `activity_log`, índice público, cache Redis e `v2_portal_configs`. |
| Particularidade central | Não há `where()` nos parâmetros: `{id}` e `{galleryId}` aceitam qualquer string e a validação é `findOrFail` no controller; o grupo CMS não declara `permission:` — o RBAC vem do encadeamento `CmsAuthenticate → V2PermissionMiddleware`. |
| Como se integra | Rotas nomeadas `v2.associados.*`; controllers em `V2\Modules\Conexoes\Associados\Controllers`; views `v2-associados::`. |
| Se removido, quebra | Menu do CMS, links de "Ver no portal", chamadas do modal público e a API regional (que referencia os models, não as rotas). |
| Superfície pública | 16 endpoints de §5; nomes de rota listados. |

## 1. Efeito

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

- CMS: listar/arquivar/restaurar/editar/preview e gerenciar galeria e fundo.
- Portal: listar com filtros, abrir fragmento de detalhe e acessar a página por slug.

### 1.2 O que muda no sistema

| Efeito | Onde | Fonte |
|---|---|---|
| `module.active:Associados` é aplicado a **todas** as rotas CMS pelo provider | `mapModuleRoutes` | `V2/Core/Providers/V2ServiceProvider.php:303-306` |
| `CmsAuthenticate` exige sessão + ≥1 papel e chama o RBAC | middleware | `V2/Core/Middleware/CmsAuthenticate.php:18-45` |
| Rotas públicas entram em `page.cache` e `public.cache` | `mapModuleRoutes` web | `V2/Core/Providers/V2ServiceProvider.php:297-301` |
| Escrita invalida todo o cache `page:*` | controller | `V2/Modules/Conexoes/Associados/Controllers/InstituicaoCmsController.php:572-583` |
| CSRF obrigatório em POST/PUT/DELETE | grupo `web` | `app/Http/Kernel.php` (VerifyCsrfToken no grupo `web`) |

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

| Entrada | Saída |
|---|---|
| `GET /cms/associados/instituicoes?search=x&category=<uuid>` | `200` HTML; 15 por página; `search` casa `name`/`description` com `ilike` |
| `GET /cms/associados/instituicoes/arquivadas` | `200` HTML só soft-deleted; operador regional `302` dashboard + erro |
| `GET /cms/associados/instituicoes/nova` | `200` HTML com tags e regionais permitidas |
| `POST /cms/associados/instituicoes/nova` | `302` index; erro de validação volta com `errors` |
| `GET /cms/associados/instituicoes/editar/{id}` | `200` HTML com galeria e tags; `404` se não existir; `403` se fora da regional |
| `PUT/POST /cms/associados/instituicoes/editar/{id}` | `302` para a edição + flash |
| `GET /cms/associados/instituicoes/preview/{id}` | `200` HTML com layout do portal |
| `DELETE /cms/associados/instituicoes/{id}` | `302` index + flash |
| `POST /cms/associados/instituicoes/{id}/gallery` | `302` back; `422` se imagem inválida |
| `POST /cms/associados/instituicoes/{id}/gallery/sync` | `200 {"success":true,"count":N}` |
| `DELETE /cms/associados/gallery/{galleryId}` | `302` back |
| `POST /cms/associados/salvar-imagem-fundo` | `302` index ou back com erro |
| `GET /associados` | `200` HTML (cache HIT/MISS no header `X-Cache-Status`) |
| `GET /associados/detalhe/{id}` | `200` fragmento HTML; `404` se inativo/inexistente |
| `GET /associado/{slug}` | `200` página completa; `404` se inativo/inexistente |

### 1.4 O que estas rotas NÃO fazem

- Não há endpoint JSON de listagem/detalhe: o modal recebe HTML.
- Não há `api.php` no módulo; a API de associados é de UnidadeRegional.
- Não há throttle próprio; só o global do Laravel (nenhum `throttle:` declarado).
- Não há `show` por id nem `detalhe` por slug; são rotas distintas.

## 2. Abordagem

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

```mermaid
flowchart LR
  R[request] --> W[web: CSRF/sessão]
  W --> M[module.active:Associados]
  M --> A[CmsAuthenticate]
  A --> P[V2PermissionMiddleware]
  P --> C[Controller]
  C --> V[view/redirect/json]
```

### 2.2 Mecânica passo a passo

1. O provider monta o grupo CMS com `web` + `module.active:Associados` e namespace do controller; o arquivo de rotas acrescenta `CmsAuthenticate` e prefixo `cms/associados` (`Routes/cms.php:3-7`).
2. `module.active` consulta a flag; sem linha, ativo (`V2/Core/Services/ModuleFlagService.php:72-95`). Desativado: AJAX `404 {"error":"Modulo desativado: Associados","code":404}` e navegação CMS `404 Modulo desativado.` (HTML). O redirect ao dashboard só ocorreria se o prefixo fosse exatamente `cms`, o que não acontece com `cms/associados`. O portal **não** usa este middleware (`V2/Core/Middleware/ModuleAccessMiddleware.php:40-63`; `V2/Core/Providers/V2ServiceProvider.php:297-306`).
3. `CmsAuthenticate` valida Sentinel e papéis; sem papel, logout + redirect login com `Acesso negado. Seu usuário não possui permissões administrativas.` (`:18-45`).
4. `V2PermissionMiddleware` casa `cms/associados*` com a chave `Associados`; `none` → redirect dashboard; `read` → só GET/HEAD; `write` → tudo (`V2/Core/Middleware/V2PermissionMiddleware.php:79-128`).
5. Nas rotas marcadas, `RegionalRestrictMiddleware` bloqueia operador regional (`V2/Core/Middleware/RegionalRestrictMiddleware.php:29-50`).
6. O controller faz `findOrFail`, autoriza escopo quando aplicável e responde.

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

- **Sem FormRequest** `[C]`: as validações vivem inline em `$this->validate()`; mensagens são as padrões do Laravel. Isso impede reuso por API e mantém regras duplicadas entre `store` e `update`.
- **Rotas separadas para arquivadas e restauração** `[C]`: permitem aplicar `RegionalRestrictMiddleware` sem afetar o CRUD normal.
- **Prefixo `cms/associados`** `[C]`: o mesmo string é o `active_pattern` do provider, garantindo o match do RBAC.
- **`/associado/{slug}` e `/associados/detalhe/{id}`** `[C]`: um para SEO/links, outro para o modal; ambos leem o mesmo model com `active()`.

### 2.4 Trechos de código pivô

```php
// V2/Modules/Conexoes/Associados/Routes/cms.php:3-23 — grupo e rotas nacional-only
Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/associados',
], function () {
    Route::get('/instituicoes', ['as' => 'v2.associados.cms.instituicoes.index', 'uses' => 'InstituicaoCmsController@index']);

    Route::get('/instituicoes/arquivadas', [
        'as' => 'v2.associados.cms.instituicoes.archived',
        'uses' => 'InstituicaoCmsController@archived',
        'middleware' => [\V2\Core\Middleware\RegionalRestrictMiddleware::class],
    ]);

    Route::post('/instituicoes/{id}/restaurar', [
        'as' => 'v2.associados.cms.instituicoes.restore',
        'uses' => 'InstituicaoCmsController@restore',
        'middleware' => [\V2\Core\Middleware\RegionalRestrictMiddleware::class],
    ]);
```

O middleware adicional é declarado **na rota**, não no grupo; por isso só essas três rotas (arquivadas, restaurar, salvar fundo) são nacional-only.

```php
// V2/Modules/Conexoes/Associados/Routes/cms.php:40-53 — update aceita PUT e POST; destroy por DELETE
Route::match(['PUT', 'POST'], '/instituicoes/editar/{id}', [
    'as'   => 'v2.associados.cms.instituicoes.update',
    'uses' => 'InstituicaoCmsController@update',
]);

Route::get('/instituicoes/preview/{id}', [
    'as'   => 'v2.associados.cms.instituicoes.preview',
    'uses' => 'InstituicaoCmsController@preview',
]);

Route::delete('/instituicoes/{id}', [
    'as'   => 'v2.associados.cms.instituicoes.destroy',
    'uses' => 'InstituicaoCmsController@destroy',
]);
```

O `match` existe porque o form usa `_method=PUT`, mas integrações podem postar direto.

```php
// V2/Modules/Conexoes/Associados/Routes/web.php:10-27 — três rotas públicas
Route::group(['middleware' => ['web']], function () {
    Route::get('/associados', [
        'as' => 'v2.associados.portal.index',
        'uses' => 'AssociadosPortalController@index',
    ]);

    Route::get('/associados/detalhe/{id}', [
        'as' => 'v2.associados.portal.detalhe',
        'uses' => 'AssociadosPortalController@detalhe',
    ]);

    Route::get('/associado/{slug}', [
        'as' => 'v2.associados.portal.show',
        'uses' => 'AssociadosPortalController@show',
    ]);
});
```

O `web` interno é redundante com o grupo do provider; os middlewares de manutenção/cache são adicionados **fora** do arquivo (`V2/Core/Providers/V2ServiceProvider.php:297-301`).

### 2.5 Algoritmos, consultas e custos

- Todas as rotas CMS fazem lookup por PK UUID sem regex: valor arbitrário gera `404` do Postgres/Eloquent (cast para uuid pode gerar erro 500 em Postgres se a string não for UUID — ver §3.3).
- `index`: 1 query paginada + eager loads; `archived`: idem.
- `preview`: 1 query com gallery/tags + render Tiptap.
- `uploadGallery`: N uploads sequenciais, cada um com processamento de imagem e insert; sem transação.
- `syncGallery`: 1 delete + 1 select + K inserts + 1 log.
- Portal: cache Redis 5 min por URL completa (inclui query string), com `X-Cache-Status: HIT|MISS`; AJAX "puro" (com `X-Requested-With`) não é cacheado, mas o modal usa XHR sem esse header e é cacheado.
- `public.cache` adiciona `Cache-Control: public, max-age=60, must-revalidate` e `Vary: Accept-Encoding` (`V2/Core/Middleware/PublicCacheMiddleware.php:85-96`).

## 3. Particularidades

### 3.1 O que só existe aqui

- **Rota de fragmento** (`/associados/detalhe/{id}`) que devolve HTML sem layout, consumida por XHR.
- **Duas rotas de leitura da mesma entidade** com chaves diferentes (id x slug).
- **Rota de upload em lote** (`images[]`) sem UI no módulo: nenhuma view cria `<input name="images[]">`; só a rota existe (possivelmente para integração/legado).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-ASSOCIADOS-R01` | Rotas CMS passam por `module.active:Associados` antes do RBAC; as públicas não. | Desligar o módulo bloqueia o CMS (inclusive super-admin) mas mantém `/associados` no ar. | `V2ServiceProvider.php:303-306`; `ModuleAccessMiddleware.php:40-63` |
| `RN-ASSOCIADOS-R02` | A permissão é por método HTTP: `read` só GET/HEAD. | Perfil de leitura recebe 302 back com erro em qualquer escrita. | `V2PermissionMiddleware.php:101-122` |
| `RN-ASSOCIADOS-R03` | `arquivadas`, `restaurar` e `salvar-imagem-fundo` são nacional-only. | Operador regional não restaura nem troca o fundo. | `Routes/cms.php:13-23,70-74` |
| `RN-ASSOCIADOS-R04` | O modal usa XHR sem `X-Requested-With`; o cache de página o trata como navegação. | Fragmento pode ficar 5 min desatualizado. | `instituicoes.blade.php:296-298`; `PageCacheMiddleware.php:68-77` |
| `RN-ASSOCIADOS-R05` | `sync` retorna `count($mediaIds)` de entrada, não o número sincronizado. | Cliente não detecta IDs inválidos/inexistentes. | `InstituicaoCmsController.php:361-370` |
| `RN-ASSOCIADOS-R06` | `id`/`galleryId` não têm `where` nem binding. | String inválida chega ao controller; em Postgres o cast pode estourar 500. | `Routes/cms.php:19,50,65` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **`GET /cms/associados/instituicoes/arquivadas`** usa o mesmo blade do index; a flag `$isArchived` vem do **nome da rota**, não do controller (`index.blade.php:5`).
- **`GET /cms/associados/instituicoes/nova`** é declarada antes de `/instituicoes/{id}`? Não há `GET /instituicoes/{id}`, então não há conflito.
- **`POST /instituicoes/nova`** e `POST /instituicoes/editar/{id}` coexistam com `POST /instituicoes/{id}/restaurar`; `/nova` não é capturado por `{id}` porque a rota de restore exige o sufixo.
- **ID não-UUID**: `findOrFail('abc')` em coluna uuid no Postgres pode lançar `invalid input syntax for type uuid` (500) em vez de 404.
- **`sync` sem `media_ids`**: `input(..., [])` → apaga todas as linhas com `gallery_media_id` não nulo e devolve `count=0`.
- **Preview de arquivada**: `findOrFail` sem `withTrashed` → 404.
- **CSRF**: POST/PUT/DELETE sem `_token` → `419` HTML (ou JSON se `Accept: application/json`).
- **Cache de escrita**: qualquer POST/PUT/DELETE no CMS não passa por `page.cache` (não é GET), mas o controller invalida o cache manualmente.

### 3.4 Armadilhas de reimplementação

- **Adicionar `permission:...` no grupo** muda o contrato: hoje o RBAC é encadeado por `CmsAuthenticate`.
- **Tornar `/associados/detalhe/{id}` JSON** quebra o JS que injeta `xhr.responseText` direto no modal.
- **Mover `clearPageCache` para um observer** muda a ordem (o cache seria limpo antes do commit do índice, por exemplo).
- **Trocar `match(['PUT','POST'])` por `put()`** quebra formulários HTML sem JS.
- **Aplicar `RegionalRestrictMiddleware` ao grupo inteiro** impede o operador regional de criar/editar, que é o caso de uso principal.

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

- **Rotas de galeria por upload em lote e sync não são usadas por nenhuma view**: a galeria da edição usa `media-group-input` (que já sobe pela Galeria e envia IDs no update). As rotas ficam como API interna não documentada.
- **`GET /cms/associados/instituicoes` não valida `category` como UUID**: valor arbitrário vai ao `where` e pode gerar 500 no Postgres.
- **A rota `preview` entrega o layout público completo** (com `v2-portal::app-layout`) dentro do CMS, sem `X-Frame-Options` específico; o iframe usa `sandbox="allow-scripts allow-same-origin"`.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Consumidores |
|---|---|---|
| `route('v2.associados.cms.instituicoes.index')` | — | menu, redirects, blades |
| `route('v2.associados.cms.instituicoes.edit', ['id' => $id])` | — | listagem, pós-update |
| `route('v2.associados.portal.show', ['slug' => $slug])` | — | listagem CMS ("Ver no portal"), links externos |
| `route('v2.associados.portal.detalhe', ['id' => $id])` | — | template JS do modal |

### 4.2 Contrato de UI/JS

- `_form.blade.php` envia `tags[]`, `benefits[i][name|link]`, `social_media_json` e os hidden `logo_id`, `cover_image_id`, `is_active`, `sort_order`.
- `media-group-input` envia `convenios_gallery_ids[]`.
- O formulário usa `novalidate` e não implementa validação client-side além de `required`, `type=url`, `maxlength` (`create.blade.php:16-31`).
- O modal público usa `detalheUrlBase` com placeholder `__ID__` e substitui no evento `show.bs.modal` (`instituicoes.blade.php:243-318`).

### 4.3 Ordem e ciclo de vida

1. Rotas CMS registradas após as views; `module.active` resolve a flag.
2. `CmsAuthenticate` → `V2PermissionMiddleware`.
3. `RegionalRestrictMiddleware` por rota.
4. Controller → model hooks → efeitos → resposta.

### 4.4 Receitas de uso

**(a) Chamar o sync de galeria por HTTP**

```http
POST /cms/associados/instituicoes/9f1c.../gallery/sync
X-CSRF-TOKEN: <token>
Content-Type: application/json

{"media_ids": [12, 15, 19]}
```

Resposta: `{"success":true,"count":3}`. IDs inexistentes são ignorados e o `count` continua 3.

**(b) Consumir o fragmento do modal**

```js
const url = `/associados/detalhe/${id}`;
const html = await fetch(url).then(r => r.text());
document.getElementById('associadoModalBody').innerHTML = html;
```

**(c) Redirecionar após criar (padrão do módulo)**

```php
session()->flash('created_item', [
    'type' => 'Instituição', 'title' => $instituicao->name,
    'route' => route('v2.associados.cms.instituicoes.index'),
]);
return redirect()->route('v2.associados.cms.instituicoes.index');
```

O layout do CMS consome `created_item` para abrir o modal de sucesso.

### 4.5 Anti-uso

- **Chamar `POST /cms/associados/instituicoes/nova` esperando JSON**: o controller sempre redireciona; não há negociação de conteúdo.
- **Passar `category` com nome em vez de UUID** na listagem: o `where` compara com a PK e não retorna nada (ou 500).
- **Reutilizar `/associados/detalhe/{id}` como API**: é HTML com classes do portal, sem contrato estável.

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

- Cache público de 60 s no navegador/CDN + 5 min no Redis.
- Sem throttle por rota; o limite é o global do Laravel.
- Upload de até 4 MB por imagem; número de imagens limitado pelo `post_max_size`.
- `count` do sync não reflete o estado final.

## 5. Rotas e contratos HTTP

| # | Tipo | Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|---|---|
| 1 | grupo | — | `cms/associados` | — | `web`, `module.active:Associados`, `CmsAuthenticate` | — | `Associados` (read/write) | agrupa o CRUD | `Routes/cms.php:3-7`; `V2ServiceProvider.php:302-306` |
| 2 | endpoint | GET | `/cms/associados/instituicoes` | `v2.associados.cms.instituicoes.index` | grupo | `InstituicaoCmsController@index` | read | lista 15/página com escopo regional | `Routes/cms.php:8-11` |
| 3 | endpoint | GET | `/cms/associados/instituicoes/arquivadas` | `...archived` | grupo + `RegionalRestrictMiddleware` | `@archived` | read (não regional) | lista soft-deleted | `:13-17` |
| 4 | endpoint | POST | `/cms/associados/instituicoes/{id}/restaurar` | `...restore` | grupo + regional-restrict | `@restore` | write (não regional) | `deleted_at=null` | `:19-23` |
| 5 | endpoint | GET | `/cms/associados/instituicoes/nova` | `...create` | grupo | `@create` | read | formulário | `:25-28` |
| 6 | endpoint | POST | `/cms/associados/instituicoes/nova` | `...store` | grupo | `@store` | write | cria instituição | `:30-33` |
| 7 | endpoint | GET | `/cms/associados/instituicoes/editar/{id}` | `...edit` | grupo | `@edit` | read | formulário + galeria | `:35-38` |
| 8 | endpoint | PUT/POST | `/cms/associados/instituicoes/editar/{id}` | `...update` | grupo | `@update` | write | atualiza e sincroniza | `:40-43` |
| 9 | endpoint | GET | `/cms/associados/instituicoes/preview/{id}` | `...preview` | grupo | `@preview` | read | HTML no layout do portal | `:45-48` |
| 10 | endpoint | DELETE | `/cms/associados/instituicoes/{id}` | `...destroy` | grupo | `@destroy` | write | arquiva | `:50-53` |
| 11 | endpoint | POST | `/cms/associados/instituicoes/{id}/gallery` | `...gallery.upload` | grupo | `@uploadGallery` | write | sobe N imagens | `:55-58` |
| 12 | endpoint | POST | `/cms/associados/instituicoes/{id}/gallery/sync` | `...gallery.sync` | grupo | `@syncGallery` | write | sincroniza por IDs | `:60-63` |
| 13 | endpoint | DELETE | `/cms/associados/gallery/{galleryId}` | `...gallery.destroy` | grupo | `@destroyGallery` | write | remove vínculo | `:65-68` |
| 14 | endpoint | POST | `/cms/associados/salvar-imagem-fundo` | `...save-bg` | grupo + regional-restrict | `@saveBackgroundImage` | write (não regional) | grava `theme_associados_bg` | `:70-74` |
| 15 | grupo | — | `/` | — | `web`, `MaintenanceMiddleware`, `page.cache`, `public.cache` | — | pública | agrupa as rotas do portal | `Routes/web.php:10-12`; `V2ServiceProvider.php:297-301` |
| 16 | endpoint | GET | `/associados` | `v2.associados.portal.index` | grupo web | `AssociadosPortalController@index` | pública | lista com filtros | `Routes/web.php:14-17` |
| 17 | endpoint | GET | `/associados/detalhe/{id}` | `v2.associados.portal.detalhe` | grupo web | `@detalhe` | pública | fragmento do modal | `:19-22` |
| 18 | endpoint | GET | `/associado/{slug}` | `v2.associados.portal.show` | grupo web | `@show` | pública | página SEO | `:24-27` |

### 5.1 Parâmetros e validação por rota

**`store`** (`InstituicaoCmsController.php:116-131`): `name` required string max255; `tags` required array min1; `tags.*` string max100; `url` nullable url max255; `discount_badge` nullable string max255; `benefits_list` nullable string; `logo_id`/`cover_image_id` nullable string max36; `unidade_regional_id` required string max36 exists em `v2_unidade_regionais,id`; `description`/`convenio_description` nullable string; `sort_order` nullable integer min0; `is_active` nullable boolean; `social_media_json` nullable string. **Sem** regras para `benefits`/`benefits.*`/`convenios_gallery_ids`.

**`update`** (`:201-219`): as mesmas + `benefits` nullable array, `benefits.*.name` nullable string max255, `benefits.*.link` nullable url max500, `convenios_gallery_ids` nullable array.

**`uploadGallery`** (`:323-326`): `images` required array; `images.*` required image mimes jpeg,png,jpg,webp max4096 (KB).

**`saveBackgroundImage`** (`:508-510`): `image_id` required string max36.

**Portal `index`**: `search` string livre (trim); `tags[]` só UUID válido; `regional` UUID livre (não validado por regex, usado em `where`).

### 5.2 Respostas de erro

| Situação | Código | Corpo |
|---|---|---|
| Não autenticado (CMS) | `302` login (ou `401 Unauthorized.` em AJAX) | — |
| Sem papel | `302` login + `Acesso negado. Seu usuário não possui permissões administrativas.` | — |
| RBAC `none` | `302` dashboard + `Acesso negado. Seu perfil não possui permissão para acessar o módulo: Associados Regionais` | — |
| RBAC `read` em escrita | `302` back + `Operação negada. Seu perfil possui apenas permissão de leitura para o módulo: Associados Regionais` (AJAX `403 Forbidden. Write actions are not allowed for your profile.`) | — |
| Regional em rota nacional | `302` dashboard + `Acesso negado. Esta ação é restrita à administração nacional.` (AJAX `403 Forbidden. Ação restrita à administração nacional.`) | — |
| Registro de outra regional | `403` | `Acesso restrito aos dados da sua Unidade Regional.` |
| Registro inexistente | `404` | HTML padrão |
| Validação | `302` back | `errors` em sessão |
| Módulo desativado (CMS) | `404` | `Modulo desativado.` |
| Módulo desativado (AJAX) | `404` | `{"error":"Modulo desativado: Associados","code":404}` |

### 5.3 Auth, CSRF, throttle e cache

- **Guard**: Sentinel, sessão `web`; sem token Bearer nestas rotas.
- **CSRF**: `VerifyCsrfToken` no grupo `web`, ativo para todos os verbos não-GET.
- **Throttle**: nenhum declarado.
- **Cache**: só portal; chave `page:sha1(fullUrl)` no store `redis`, TTL 5 min; `X-Cache-Status`; exclusão para `cms*`, `api*`, métodos não-GET e requisições com `X-Requested-With`.
- **Idempotência**: `DELETE`/`restore` não são idempotentes do ponto de vista de resposta (404 na segunda chamada); `sync` é idempotente no conjunto, mas `count` engana.

## 6. Dados e modelo de domínio

As rotas escrevem nas tabelas descritas em [dados.md](dados.md). Em especial: `store`/`update` gravam instituição, tags/pivô, pivô regional; `gallery.*` grava `v2_associado_instituicao_gallery`; `save-bg` grava `v2_portal_configs`.

## 7. Fluxos e estados

`FL-ASSOCIADOS-R01` — requisição CMS: provider → module.active → CmsAuthenticate → RBAC → (regional-restrict) → controller.
`FL-ASSOCIADOS-R02` — escrita: validação → domínio → hooks → efeitos → cache → auditoria → redirect.
`FL-ASSOCIADOS-R03` — portal: `page.cache` → controller → view → `public.cache`; HIT devolve conteúdo e headers cacheados (exceto `Set-Cookie`).

## 8. UI CMS

Ver [cms.md](cms.md) para a tela de cada rota.

## 9. UI Portal

Ver [portal.md](portal.md) para a estrutura das três rotas públicas.

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

| Item | Valor | Fonte |
|---|---|---|
| Flag | `Associados`/`associados` em `v2_module_flags`; ausente = ativo | `ModuleFlagService.php:72-95` |
| Permissão | chave `Associados`, ações `read`/`write` | `ModuleProvider.php:21`; `V2PermissionMiddleware.php:40-128` |
| Papel regional | `regional` com `{"Noticias":"write","Associados":"write"}` | `V2/Core/Migrations/2026_09_14_000002_seed_v2_regional_role.php:21-26` |
| `active_pattern` | `['cms/associados*']` (array; `Request::is()` aceita array) | `ModuleProvider.php:21` |
| Cache | store `redis`, TTL 5 min, chave `page:sha1(fullUrl)` | `PageCacheMiddleware.php:11-14,88-90` |
| Headers públicos | `public, max-age=60, must-revalidate`, `Vary: Accept-Encoding` | `PublicCacheMiddleware.php:85-96` |

## 11. Observabilidade, erros e limites

- `Log::info` por escrita CMS (contexto de auditoria); `Log::warning` de cache; `Log::error` de renderer.
- `activity_log` por escrita (ver [cms.md](cms.md)).
- `ModuleAccessMiddleware` registra apenas via resposta; `RegionalRestrictMiddleware` registra `Log::warning('[V2 > MIDDLEWARE > RegionalRestrict] Acesso nacional-only negado.', ...)`.
- `RegionalScopeService` registra `Log::debug` em escopo e `activity_log` em acesso negado (`V2/Core/Services/RegionalScopeService.php:92-100,190-208`).
- Limites de payload: 4 MB por imagem; sem limite de itens no array de galeria.

## 12. Seeds e dados iniciais

Nenhuma rota de seed. Os dados iniciais vêm de migration (`000019`) e de `DemoContentSeeder` (ver [dados.md](dados.md)).

## 13. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| `LG-ASSOCIADOS-R01` | Não há UI que use `/gallery` (upload) nem `/gallery/sync`; não se sabe se são consumidas por integração externa. | Rotas candidatas a remoção na reimplementação, mas removê-las pode quebrar clientes. | logs de acesso/produção |
| `LG-ASSOCIADOS-R02` | Não há contrato JSON documentado para o modal; ele é HTML acoplado ao CSS do portal. | Reimplementação de front precisa replicar classes. | `public/assets/css/modules/associados/portal-associados.css` |
| `LG-ASSOCIADOS-R03` | Comportamento de `{id}` não-UUID em Postgres (404 vs 500) não foi exercitado. | Pode exigir cast/validação explícita. | teste em ambiente com Postgres |

## 14. Checklist de paridade

- [ ] `PC-ASSOCIADOS-R01` — todas as rotas CMS exigem sessão, papel e RBAC da chave `Associados`. Fonte: `CmsAuthenticate.php:18-45`; `V2PermissionMiddleware.php:79-128`.
- [ ] `PC-ASSOCIADOS-R02` — flag desligada bloqueia o CMS (redirect/404) e **não** bloqueia o portal. Fonte: `ModuleAccessMiddleware.php:40-63`; `V2ServiceProvider.php:297-306`.
- [ ] `PC-ASSOCIADOS-R03` — operador regional é barrado em arquivadas/restaurar/fundo com a mensagem exata. Fonte: `RegionalRestrictMiddleware.php:36-50`.
- [ ] `PC-ASSOCIADOS-R04` — `update` responde a PUT e POST. Fonte: `Routes/cms.php:40`.
- [ ] `PC-ASSOCIADOS-R05` — `sync` devolve `{"success":true,"count":N}` e não falha com IDs inválidos. Fonte: `InstituicaoCmsController.php:355-371`.
- [ ] `PC-ASSOCIADOS-R06` — portal emite `X-Cache-Status` e `Cache-Control: public, max-age=60, must-revalidate`. Fonte: `PageCacheMiddleware.php:35-39`; `PublicCacheMiddleware.php:85-96`.
- [ ] `PC-ASSOCIADOS-R07` — `/associados/detalhe/{id}` devolve fragmento e `/associado/{slug}` página completa. Fonte: `Routes/web.php:19-27`.
- [ ] `PC-ASSOCIADOS-R08` — `store` não valida `benefits`/`convenios_gallery_ids`, mas `update` sim. Fonte: `:116-131` vs `:201-219`.

## 15. Evidências consultadas

- `V2/Modules/Conexoes/Associados/Routes/cms.php:1-77`; `Routes/web.php:1-28`
- `V2/Modules/Conexoes/Associados/Controllers/InstituicaoCmsController.php:47-113,114-181,195-291,317-371,440-526`
- `V2/Modules/Conexoes/Associados/Controllers/AssociadosPortalController.php:28-198`
- `V2/Core/Providers/V2ServiceProvider.php:80-320`
- `V2/Core/Middleware/{CmsAuthenticate,V2PermissionMiddleware,ModuleAccessMiddleware,RegionalRestrictMiddleware,PageCacheMiddleware,PublicCacheMiddleware}.php`
- `V2/Core/Services/ModuleFlagService.php:60-127`; `V2/Core/Services/RegionalScopeService.php:78-142`
- `V2/Core/Migrations/2026_09_14_000002_seed_v2_regional_role.php:15-55`
- `V2/Modules/Conexoes/Associados/Views/cms/instituicoes/index.blade.php:1-232`; `create.blade.php:16-45`; `edit.blade.php:90-196`
- `V2/Modules/Conexoes/Associados/Views/portal/instituicoes.blade.php:237-322`
