---
id: MOD-CONEXOES-UNIDADEREGIONAL-ROTAS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-CONEXOES-UNIDADEREGIONAL]
fontes:
  - V2/Modules/Conexoes/UnidadeRegional/
depende_de: [MOD-CONEXOES-UNIDADEREGIONAL, CORE-BOOT, CORE-AUTH, CORE-RUNTIME]
consumidores: [MOD-CONEXOES-UNIDADEREGIONAL]
criticidade: alta
dono: Regional
---

# Conexoes/UnidadeRegional — rotas e contratos HTTP

> Inventário completo das 30 declarações de rota do módulo (3 arquivos), com middleware,
> permissão, entrada, resposta e efeito. Payloads e erros da API em [`api.md`](api.md);
> fluxo de telas em [`cms.md`](cms.md) e [`portal.md`](portal.md).

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Expõe o CRUD de unidades regionais no CMS, as páginas públicas de regionais e a API Bearer por unidade, cada conjunto com um stack de middleware distinto. |
| Entrada | 3 arquivos de rota: `Routes/cms.php` (16 endpoints + grupo), `Routes/web.php` (2 endpoints + grupo), `Routes/api.php` (9 endpoints + grupo). |
| Saída | HTML do CMS, HTML do portal, JSON da API e JSON do OpenAPI. |
| Efeitos colaterais | Cache de página público; CSRF nas rotas CMS de escrita; throttle por token/IP nos POST da API. |
| Particularidade central | O prefixo e a maioria dos middlewares **não** estão nos arquivos de rota: vêm de `V2ServiceProvider::mapModuleRoutes()`, incluindo `module.active:UnidadeRegional` no CMS e `/api/v2/unidaderegional` na API. |
| Como se integra | Rotas nomeadas `v2.unidade-regional.*`; controllers resolvidos pelo namespace injetado `V2\Modules\Conexoes\UnidadeRegional\Controllers`. |
| Se removido, quebra | Links do menu lateral (`route` do `ModuleProvider`), portal de regionais, sitemap e API das regionais. |
| Superfície pública | Nomes de rota (contrato para views/testes), URIs públicas e assinaturas de resposta. |

## 1. Efeito

### 1.1 O que cada perfil consegue acessar

| Perfil | Rotas | Resultado |
|---|---|---|
| Anônimo | `GET /regionais`, `GET /regional/{slug}` | HTML público (page cache 5 min) |
| Anônimo | `GET /api/v2/unidaderegional/openapi.json` | JSON da spec (cache HTTP 1 h) |
| Anônimo | qualquer `/api/v2/unidaderegional/*` sem token | `401 invalid_token` |
| Usuário CMS sem permissão `UnidadeRegional` | `cms/unidades-regionais*` | `403` (JSON) ou redirect ao dashboard com flash de erro |
| Usuário CMS com `read` | GETs do CMS | listas/formulários; POST/PUT/DELETE bloqueados |
| Usuário CMS com `write` | todos os endpoints CMS | escrita completa |
| Portador de token `asc_...` | 8 endpoints de notícias/associados | dados da própria unidade |
| Qualquer usuário | `GET /cms/unidades-regionais/documentacao` | documentação da API (exige sessão CMS) |

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| `Cache-Control: public, max-age=60, must-revalidate` + `Vary: Accept-Encoding` nas páginas públicas | `PublicCacheMiddleware` | `V2/Core/Middleware/PublicCacheMiddleware.php:83-91` |
| Resposta HTML de `/regionais` e `/regional/{slug}` gravada em Redis por 5 min (`page:<sha1(fullUrl)>`, `X-Cache-Status: HIT|MISS`) | `PageCacheMiddleware` | `V2/Core/Middleware/PageCacheMiddleware.php:14-16,45-70,88-91` |
| Bloqueio de módulo desativado | `module.active:UnidadeRegional` | `V2/Core/Middleware/ModuleAccessMiddleware.php:39-62` |
| CSRF obrigatório nas escritas CMS | grupo `web` (`VerifyCsrfToken`) | `app/Http/Kernel.php:38-44` |
| Throttle 30/min por prefixo de Bearer + 100/min por IP nos POST da API | `throttle.api:30,1` + `throttle:100,1` | `Routes/api.php:7-9,11-13,18-20,22-24`; `V2/Core/Middleware/ThrottleApiToken.php:22-47` |
| Limpeza do page cache em toda escrita | controllers | `Controllers/UnidadeRegionalCmsController.php:524-536`; `Controllers/UnidadeRegionalNewsApiController.php:596-608` |

### 1.3 Contrato observável (rota → resposta)

| Rota | Sucesso | Falha |
|---|---|---|
| `GET /cms/unidades-regionais` | `200` HTML `v2-unidaderegional::cms.index` | `302` login se não autenticado; `302` dashboard se sem permissão |
| `POST /cms/unidades-regionais/nova` | `302` índice + flash `Unidade Regional "<nome>" criada com sucesso!` | `302` volta com `$errors` |
| `PUT/POST /cms/unidades-regionais/editar/{id}` | `302` para o próprio edit + flash `Unidade Regional atualizada com sucesso!` | `302` volta com `$errors`; `404` se id inexistente (`findOrFail`) |
| `DELETE /cms/unidades-regionais/{id}` | `302` índice + flash `Unidade Regional "<nome>" arquivada com sucesso!` | `404` |
| `GET /cms/unidades-regionais/arquivadas` | `200` | — |
| `POST /cms/unidades-regionais/{id}/restaurar` | `302` arquivadas + flash `Unidade Regional "<nome>" restaurada com sucesso!` | `404` |
| `POST /cms/unidades-regionais/{id}/api-token/gerar` | `200` JSON `ApiResponse` com `raw_token` | `404` |
| `POST /cms/unidades-regionais/{id}/api-token/revogar` | `200` JSON | `404` |
| `POST /cms/unidades-regionais/{id}/gallery` | `302` back + flash `Imagens enviadas com sucesso!` | `302` back com erro de validação |
| `POST /cms/unidades-regionais/{id}/gallery/sync` | `200 {"ok":true,"data":{"count":N},"message":"Galeria sincronizada com sucesso."}` | — |
| `DELETE /cms/unidades-regionais/gallery/{galleryId}` | `302` back + flash `Imagem removida da galeria com sucesso!` | `404` |
| `POST /cms/unidades-regionais/salvar-imagem-fundo` | `302` índice + flash `Imagem de fundo da página de regionais atualizada com sucesso!` | `302` back + flash `Imagem não encontrada na galeria.` |
| `GET /regionais` | `200` HTML | `500` se `v2_portal_configs` não existir? Não: bloco é `try/catch` silencioso |
| `GET /regional/{slug}` | `200` HTML | `404` se inativo/inexistente |
| `GET /api/v2/unidaderegional/noticias` | `200` JSON paginado | `401` |
| `POST /api/v2/unidaderegional/noticias` | `201` | `401`, `422`, `429` |
| `POST /api/v2/unidaderegional/noticias/{id}` | `200` | `401`, `404`, `409`, `422`, `429` |
| `DELETE /api/v2/unidaderegional/noticias/{id}` | `200` | `401`, `404`, `409` |
| `GET /api/v2/unidaderegional/associados` | `200` | `401` |
| `POST /api/v2/unidaderegional/associados` | `201` | `401`, `422`, `429` |
| `POST /api/v2/unidaderegional/associados/{id}` | `200` | `401`, `403`, `404`, `409`, `422`, `429` |
| `DELETE /api/v2/unidaderegional/associados/{id}` | `200` | `401`, `404` |
| `GET /api/v2/unidaderegional/openapi.json` | `200` `application/json` | — |

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

- Não há rota de **publicação/aprovação** de notícia: a API só cria (`pending_approval`) e arquiva.
- Não há rota de **listagem pública de notícias da regional**: o único filtro é
  `?regional=` na rota de Notícias, que o controller de Notícias ignora (ver §3.3).
- Não há rota de **API para listar regionais**: só o portal HTML.
- Não há rota de **exportação**, **importação** ou **sincronização em lote** de unidades.
- Não há rota de API para **token** (geração/revogação são exclusivas do CMS com sessão).
- Não há rota de API para **atualizar a unidade** (nome, sigla, UF).

## 2. Abordagem

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

```mermaid
flowchart LR
  subgraph Provider[V2ServiceProvider]
    P1["mapModuleRoutes('UnidadeRegional')"]
  end
  P1 -->|web + maintenance + page.cache + public.cache| W[Routes/web.php]
  P1 -->|web + module.active:UnidadeRegional| C[Routes/cms.php]
  P1 -->|prefix api/v2/unidaderegional + api| A[Routes/api.php]
  W --> PW[UnidadeRegionalPortalController]
  C --> CW[UnidadeRegionalCmsController]
  A --> AW[UnidadeRegionalNewsApiController]
  A --> AE[UnidadeRegionalEmpresaApiController]
  A --> AO[UnidadeRegionalApiController]
```

Cada arquivo declara apenas o que é específico (prefixo `cms/unidades-regionais`,
`CmsAuthenticate`, throttles); o resto é injetado no boot.

### 2.2 Mecânica passo a passo

1. `V2ServiceProvider::boot()` percorre os grupos e módulos; para este módulo, instancia
   `ModuleProvider` e guarda a config (`V2/Core/Providers/V2ServiceProvider.php:95-112`).
2. `loadViewsFrom(Views, 'v2-unidaderegional')` (`:120-123`) e `loadMigrationsFrom` (`:114-118`).
3. `mapModuleRoutes('UnidadeRegional', $path, 'Conexoes')`:
   - `web.php` → middleware `['web', MaintenanceMiddleware, 'page.cache', 'public.cache']` (`:297-301`);
   - `cms.php` → `['web', 'module.active:UnidadeRegional']` (`:303-307`);
   - `api.php` → prefixo `api/v2/unidaderegional` + middleware `api` (`:309-313`).
4. `cms.php` aplica `CmsAuthenticate` no grupo com prefixo `cms/unidades-regionais`
   (`Routes/cms.php:3-6`). `CmsAuthenticate` checa Sentinel, exige ≥ 1 papel e delega a
   `V2PermissionMiddleware` (`V2/Core/Middleware/CmsAuthenticate.php:16-44`).
5. O RBAC casa `active_pattern` (`ModuleProvider.php:21`) com a URI e usa a chave
   `UnidadeRegional` (`V2/Core/Middleware/V2PermissionMiddleware.php:35-61`).
6. `api.php` declara um `Route::group(['middleware' => ['bindings']])` para os 8 endpoints
   autenticados e deixa `openapi.json` fora do grupo (`Routes/api.php:3-38`) — logo a spec é
   pública, sem throttle e sem Bearer.
7. Nas rotas de escrita da API, a ordem efetiva é `api` → `bindings` → `throttle.api` →
   `throttle` → controller (middleware de rota executa depois do grupo).

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

- **Prefixos centralizados no provider** — evita divergência entre módulos e permite mudar o
  slug da API em um só lugar (`strtolower($moduleName)` → `unidaderegional`). `[C]`
- **`CmsAuthenticate` + RBAC no grupo, não por rota** — todas as 16 rotas herdaram o mesmo
  gate; as views reforçam com `@v2can`. `[C]`
- **`openapi.json` fora do grupo autenticado** — decisão explícita documentada no próprio
  arquivo (`Routes/api.php:33-37`). `[C]`
- **`Route::match(['PUT','POST'])` no update do CMS** — aceita formulário com `_method=PUT`
  mas também POST direto. `[C]`
- **`POST` para update/arquivamento na API** — em vez de `PUT/PATCH`, para simplificar clientes
  (§2.2 do `MODULE.md` não explica; inferido como compatibilidade com clientes simples). `[I]`

### 2.4 Trechos de código pivô

```php
// V2/Core/Providers/V2ServiceProvider.php:297-313 — middleware injetado por tipo de arquivo
if (File::exists($webRoutes)) {
    Route::middleware(['web', \V2\Core\Middleware\MaintenanceMiddleware::class, 'page.cache', 'public.cache'])
        ->namespace($controllerNamespace)
        ->group($webRoutes);
}

if (File::exists($cmsRoutes)) {
    Route::middleware(['web', 'module.active:' . $moduleName])
        ->namespace($controllerNamespace)
        ->group($cmsRoutes);
}

if (File::exists($apiRoutes)) {
    Route::prefix('api/v2/' . strtolower($moduleName))
        ->middleware('api')
        ->namespace($controllerNamespace)
        ->group($apiRoutes);
}
```

O `module.active` recebe `UnidadeRegional` (nome da pasta) e o prefixo de API é o nome em
minúsculas — reimplementações que mudem o nome da pasta mudam a URI pública.

```php
// V2/Modules/Conexoes/UnidadeRegional/Routes/api.php:3-16 — escopo e throttle
Route::group(['middleware' => ['bindings']], function () {
    Route::get('/noticias', 'UnidadeRegionalNewsApiController@index')
        ->name('v2.unidade-regional.api.noticias.index');

    Route::post('/noticias', 'UnidadeRegionalNewsApiController@store')
        ->middleware(['throttle.api:30,1', 'throttle:100,1'])
        ->name('v2.unidade-regional.api.noticias.store');

    Route::post('/noticias/{id}', 'UnidadeRegionalNewsApiController@update')
        ->middleware(['throttle.api:30,1', 'throttle:100,1'])
        ->name('v2.unidade-regional.api.noticias.update');

    Route::delete('/noticias/{id}', 'UnidadeRegionalNewsApiController@destroy')
        ->name('v2.unidade-regional.api.noticias.destroy');
```

`GET` e `DELETE` ficam **sem** `throttle.api` (achado SEC-G1-14). O `bindings` aqui é
`SubstituteBindings` (nenhum parâmetro tipado é vinculado por model binding).

```php
// V2/Modules/Conexoes/UnidadeRegional/Routes/cms.php:3-11 — gate e prefixo do CMS
Route::group([
    'middleware' => [\V2\Core\Middleware\CmsAuthenticate::class],
    'prefix'     => 'cms/unidades-regionais',
], function () {

    Route::get('/', [
        'as'   => 'v2.unidade-regional.cms.index',
        'uses' => 'UnidadeRegionalCmsController@index',
    ]);
```

O namespace do controller **não** é declarado aqui: vem de `->namespace($controllerNamespace)`
no provider.

```php
// V2/Core/Middleware/ThrottleApiToken.php:30-47 — chave do rate limit
$token = $request->bearerToken();
if ($token) {
    $key = 'api_token:' . substr($token, 0, 12);
} else {
    $key = 'api_token:ip:' . $request->ip();
}

if ($limiter->tooManyAttempts($key, $maxAttempts)) {
    $retryAfter = $limiter->availableIn($key);
    return response()->json([
        'error'       => 'too_many_requests',
        'message'     => 'Limite de requisições excedido. Tente novamente em ' . $retryAfter . ' segundos.',
        'retry_after' => $retryAfter,
    ], 429, [
        'X-RateLimit-Limit'     => $maxAttempts,
        'X-RateLimit-Remaining' => 0,
        'Retry-After'           => $retryAfter,
    ]);
}
```

A chave usa **só os 12 primeiros caracteres** (o prefixo `asc_` + 8), não o token inteiro.

### 2.5 Custos e consultas por rota

- `GET /cms/unidades-regionais`: 1 `COUNT` + 1 página de 15 + 1 query por linha
  `getTokenStatusBadge()` (nenhuma; é cálculo em memória). Busca usa `ILIKE` sem índice
  dedicado.
- `GET /cms/.../editar/{id}`: 1 unidade + `media` + `tags` + `withCount('media')` + 5 notícias
  (`byRegional`) + 5 vínculos com `empresa`.
- `GET /cms/.../{id}/noticias`: 1 unidade + 1 página de 15 notícias.
- `GET /regionais`: 1 `COUNT` + 12 unidades; `theme_regionais_bg` consultado com `pluck` em
  `v2_portal_configs`.
- `GET /regional/{slug}`: unidades + `media` + `empresas.empresa.tags` + 1 query de notícias
  (12) + 1 query de `GalleryMedia` para variantes da capa.
- API `GET /noticias`: 1 `COUNT` + 1 página; filtro JSONB sem índice.
- API `GET /associados`: 1 query com `with('empresa.tags')` (2 queries).

## 3. Particularidades

### 3.1 O que só existe aqui

- Duas famílias de escrita no mesmo recurso “unidade”: CMS (sessão, CSRF, RBAC) e API (Bearer,
  throttle) — mas a API **não** edita a unidade, só notícias/associados.
- `openapi.json` é a única rota pública de API do módulo e a única que responde com cache HTTP
  explícito de 1 hora.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-UNIDADEREGIONAL-R01 | Toda rota CMS herda `web` + `module.active:UnidadeRegional` + `CmsAuthenticate` | módulo desativado → `404 Modulo desativado.` antes do controller (sem redirect) | `V2ServiceProvider.php:303-307`; `ModuleAccessMiddleware.php:44-62` |
| RN-UNIDADEREGIONAL-R02 | Toda rota pública herda `page.cache` (5 min) e `public.cache` (`max-age=60`) | resposta pode vir do Redis com `X-Cache-Status: HIT` | `V2ServiceProvider.php:297-301`; `PageCacheMiddleware.php:14-16` |
| RN-UNIDADEREGIONAL-R03 | `page.cache` não cacheia `cms*`, `api*`, `trabalhe-conosco*`, `associe-se*`, `fale-conosco*`, `_debugbar*` nem AJAX | navegação do CMS/API sempre fresca | `PageCacheMiddleware.php:18-24,74-86` |
| RN-UNIDADEREGIONAL-R04 | `throttle.api` é aplicado apenas nos POST (4 rotas) | GET/DELETE ilimitados (SEC-G1-14) | `Routes/api.php:7,11,18,22` |
| RN-UNIDADEREGIONAL-R05 | A rota `GET /api/v2/unidaderegional/openapi.json` não tem `openapi` no grupo `bindings` | spec acessível sem token | `Routes/api.php:37-38` |
| RN-UNIDADEREGIONAL-R06 | `update` do CMS aceita PUT e POST | forms com `_method=PUT` funcionam | `Routes/cms.php:28-31` |
| RN-UNIDADEREGIONAL-R07 | `destroy` do CMS é DELETE, mas o efeito é arquivar | não apaga fisicamente | `Routes/cms.php:33-36`; `Controllers/UnidadeRegionalCmsController.php:267-288` |

### 3.3 Casos-limite e surpresas

- **`/regional/{slug}` inclui o slug `nacional`** criado pelo seeder; a unidade nacional é
  pública como qualquer outra (`state = 'BR'`).
- **Sitemap inclui unidades inativas**: `SeoPortalController::addRegionalUnits` filtra apenas
  `deleted_at IS NULL`, não `is_active` (`V2/Services/SEO/Controllers/SeoPortalController.php:365-377`),
  enquanto `show()` exige `active()`. URLs do sitemap podem retornar 404.
- **Link “Ver as notícias da regional”** aponta para
  `route('v2.noticias.portal.index', ['regional' => $unidade->slug])`
  (`Views/portal/show.blade.php:293`), mas `NoticiasPortalController@index` não lê `regional`
  (`V2/Modules/Publicacoes/Noticias/Controllers/NoticiasPortalController.php:52-68`): mostra
  todas as publicadas.
- **Rota `GET /cms/unidades-regionais/{id}/noticias`** pode capturar `documentacao`? Não:
  `documentacao` é declarada depois, mas só casa `/{id}/noticias` (2 segmentos). Sem conflito.
- **`GET /cms/unidades-regionais/nova` vs `/{id}/noticias`**: sem conflito (segmentos diferentes).
- **`DELETE /cms/unidades-regionais/gallery/{galleryId}`** conflita com `DELETE /{id}` apenas se
  `{id}` capturasse barras — não captura; portanto `gallery/{id}` só casa a rota específica.
- **AJAX do token** chama `/cms/unidades-regionais/{id}/api-token/gerar` **sem** o prefixo de
  `route()` (string fixa): mudar o prefixo quebra o JS (`Views/cms/edit.blade.php:386,400`).
- **Throttle por prefixo**: dois tokens diferentes com os mesmos 12 primeiros caracteres
  compartilham o balde (probabilidade baixa, mas o desenho não usa o hash).
- **Download remoto (SEC-G4-01)**: `POST /noticias` faz o servidor buscar `cover_image_url` e
  **toda** `<img src>` do Markdown, sem allowlist de esquema/host, sem bloquear IP
  privado/loopback/link-local, com redirects, TLS desligado e sem limite de bytes — superfície de
  SSRF descrita em [`api.md`](api.md) §3.6.
- **Atualização só de tags/status (SEC-G4-02)**: `POST /associados/{id}` devolve
  `Nenhuma alteração detectada.` antes de aplicar `tags`/`status`; é preciso enviar também um campo
  de texto (ex.: `name`).
- **Vínculo sem fronteira (SEC-G4-03)**: `empresa_ids` aceita qualquer associado nacional não
  arquivado e `updateAssociado` exige apenas que o vínculo **exista** (não `active`), sem checar a
  unidade de origem.

### 3.4 Armadilhas de reimplementação

1. **Declarar `page.cache`/`public.cache` no arquivo de rotas** e duplicar o middleware (o
   provider já injeta).
2. **Esquecer `module.active`** no CMS: o módulo desativado continuaria acessível.
3. **Trocar o prefixo da API para `unidade-regional`** (com hífen): o provider usa o nome da
   pasta em minúsculas (`unidaderegional`), e os links do JS/doc usam esse valor.
4. **Aplicar throttle uniforme**: hoje só POST; ampliar muda o contrato de clientes que fazem
   polling de GET.
5. **Contar com o parâmetro `regional`** na rota de notícias: ele não filtra nada no legado.

### 3.5 Divergências e dívidas

- `MODULE.md` lista os endpoints sem mencionar o `bindings` nem a ausência de throttle em
  GET/DELETE (coberto por SEC-G1-14).
- O CMS tem 3 rotas de galeria (`upload`, `sync`, `destroy`) que **nenhuma view chama**
  (`Routes/cms.php:48-61`; busca em `Views/` não encontra `cms.gallery.*`).
- Rotas públicas não declaram nome de cache variando por UF/filtro — não há filtros no portal.

## 4. Integrabilidade

### 4.1 Superfície pública (nomes de rota)

| Símbolo | Tipo | Assinatura | Precondição | Consumidores |
|---|---|---|---|---|
| `v2.unidade-regional.cms.index` | rota | `GET cms/unidades-regionais` | sessão + RBAC | menu lateral, `ModuleProvider` |
| `v2.unidade-regional.cms.create` | rota | `GET .../nova` | `write` | lista |
| `v2.unidade-regional.cms.store` | rota | `POST .../nova` | `write` + CSRF | formulário |
| `v2.unidade-regional.cms.edit` | rota | `GET .../editar/{id}` | `read` | lista, notícias, redirect do update |
| `v2.unidade-regional.cms.update` | rota | `PUT/POST .../editar/{id}` | `write` | formulário |
| `v2.unidade-regional.cms.destroy` | rota | `DELETE .../{id}` | `write` | lista, edição |
| `v2.unidade-regional.cms.archived` | rota | `GET .../arquivadas` | `read` | lista |
| `v2.unidade-regional.cms.restore` | rota | `POST .../{id}/restaurar` | `write` | lista arquivada |
| `v2.unidade-regional.cms.gallery.*` | rotas | 3 endpoints | `write` | **sem consumidor atual** |
| `v2.unidade-regional.cms.api-token.generate/revoke` | rotas | POST | `write` | JS de edição |
| `v2.unidade-regional.cms.news` | rota | `GET .../{id}/noticias` | `read` | edição |
| `v2.unidade-regional.cms.docs` | rota | `GET .../documentacao` | `read` | lista, edição |
| `v2.unidade-regional.cms.save-bg` | rota | `POST .../salvar-imagem-fundo` | `write` | modal da lista |
| `v2.unidade-regional.portal.index/show` | rotas | `GET /regionais`, `/regional/{slug}` | pública | portal, sitemap |
| `v2.unidade-regional.api.*` | rotas | 8 endpoints Bearer | token | integradores externos |

### 4.2 Contrato de UI/JS

- `Views/cms/edit.blade.php` faz `fetch` com caminho literal e `X-CSRF-TOKEN`; espera
  `d.data.raw_token`, `d.data.prefix`, `d.data.expires_at` e `d.ok`.
- `Views/cms/index.blade.php` usa `data-bs-target="#bgImageModal"` e posta `image_id` para
  `save-bg`.
- O portal depende de `page.cache` (GET) — nenhuma chamada AJAX é cacheada.

### 4.3 Ordem e ciclo de vida

1. `V2ServiceProvider::boot()` registra core, depois módulos, depois fallback
   (`V2/Core/Providers/V2ServiceProvider.php:84-137`).
2. O fallback `/{slug}` só é registrado **depois** de todos os módulos, para não capturar
   `/regionais` (`:135-137`).
3. `refreshNameLookups()`/`refreshActionLookups()` são chamados ao final (`:139-143`).

### 4.4 Receitas de uso

```blade
{{-- Link para a edição a partir de qualquer tela CMS --}}
<a href="{{ route('v2.unidade-regional.cms.edit', ['id' => $unidade->id]) }}">{{ $unidade->name }}</a>
```

Resultado: URL `https://<host>/cms/unidades-regionais/editar/<uuid>`.

```bash
# Página pública de uma regional (sujeita a page cache de 5 min)
curl -sI https://<host>/regional/sucessu-df
```

Resultado esperado: `200`, `Cache-Control: public, max-age=60, must-revalidate`,
`X-Cache-Status: HIT|MISS`.

```php
// Exemplo de rota nomeada dentro de um controller
return redirect()->route('v2.unidade-regional.cms.news', ['id' => $unidade->id]);
```

Resultado: `302` para `/cms/unidades-regionais/<uuid>/noticias`.

### 4.5 Anti-uso

- **Chamar `POST /cms/unidades-regionais/{id}/api-token/gerar` como API externa**: exige sessão
  Sentinel + CSRF; sem isso retorna login/419.
- **Usar `GET /api/v2/unidaderegional/openapi.json` esperando autenticação**: é público por
  desenho.
- **Depender de `GET /noticias?regional=`**: parâmetro inexistente no contrato.

### 4.6 Custos e limites

- Page cache: chave por URL completa (`sha1`) → parâmetros de query criam entradas distintas.
- `clearPageCache` usa `KEYS` (bloqueante) + `DEL` em todas as chaves `page:*`.
- Throttle: 30/min por prefixo de token; 100/min por IP; cabeçalhos `X-RateLimit-*` só são
  adicionados nas respostas das rotas com `throttle.api`.
- `per_page` da API não tem teto; o portal fixa 12 e o CMS 15.

## 5. Rotas e contratos HTTP

### 5.1 CMS — `Routes/cms.php` (prefixo `cms/unidades-regionais`, grupo `web` + `module.active:UnidadeRegional`)

| # | Método | URI | Nome | Middleware adicional | Controller@ação | Permissão | Entrada | Resposta / efeito | Fonte |
|---|---|---|---|---|---|---|---|---|---|
| R01 | — | grupo | — | `CmsAuthenticate` | — | RBAC `UnidadeRegional` | — | agrupa 16 endpoints | `Routes/cms.php:3-6` |
| R02 | GET | `/` | `...cms.index` | — | `index` | read | `search`, `state`, `page` | HTML 15/página | `:8-11` |
| R03 | GET | `/nova` | `...cms.create` | — | `create` | read (botão Salvar exige `write`) | — | HTML | `:13-16` |
| R04 | POST | `/nova` | `...cms.store` | CSRF | `store` | write | 13 campos + `tags[]` + logo/capa | cria + flash | `:18-21` |
| R05 | GET | `/editar/{id}` | `...cms.edit` | — | `edit` | read | `{id}` UUID | HTML + preview | `:23-26` |
| R06 | PUT/POST | `/editar/{id}` | `...cms.update` | CSRF | `update` | write | mesmos campos + `gallery_media_ids[]` | atualiza + flash | `:28-31` |
| R07 | DELETE | `/{id}` | `...cms.destroy` | CSRF | `destroy` | write | `{id}` | arquiva + revoga token | `:33-36` |
| R08 | GET | `/arquivadas` | `...cms.archived` | — | `archived` | read | `search`, `page` | HTML (onlyTrashed) | `:38-41` |
| R09 | POST | `/{id}/restaurar` | `...cms.restore` | CSRF | `restore` | write | `{id}` | `deleted_at = null` | `:43-46` |
| R10 | POST | `/{id}/gallery` | `...cms.gallery.upload` | CSRF | `uploadGallery` | write | `images[]` (≤ 4 MB) | insere mídias | `:48-51` |
| R11 | POST | `/{id}/gallery/sync` | `...cms.gallery.sync` | CSRF | `syncGallery` | write | `media_ids[]` | JSON `count` | `:53-56` |
| R12 | DELETE | `/gallery/{galleryId}` | `...cms.gallery.destroy` | CSRF | `destroyGallery` | write | `{galleryId}` | apaga mídia | `:58-61` |
| R13 | POST | `/{id}/api-token/gerar` | `...cms.api-token.generate` | CSRF | `generateApiToken` | write | `{id}` | JSON `raw_token` | `:63-66` |
| R14 | POST | `/{id}/api-token/revogar` | `...cms.api-token.revoke` | CSRF | `revokeApiToken` | write | `{id}` | JSON | `:68-71` |
| R15 | GET | `/{id}/noticias` | `...cms.news` | — | `news` | read | `{id}`, `page` | HTML 15/página | `:73-76` |
| R16 | GET | `/documentacao` | `...cms.docs` | — | `docs` | read | — | HTML de docs | `:78-81` |
| R17 | POST | `/salvar-imagem-fundo` | `...cms.save-bg` | CSRF | `saveBackgroundImage` | write | `image_id` | grava `theme_regionais_bg` | `:83-86` |

### 5.2 Portal — `Routes/web.php` (grupo `web`, sem prefixo)

| # | Método | URI | Nome | Middleware | Controller@ação | Auth | Query | Resposta | Fonte |
|---|---|---|---|---|---|---|---|---|---|
| R18 | — | grupo | — | `web` | — | pública | — | agrupa 2 endpoints | `Routes/web.php:3-5` |
| R19 | GET | `/regionais` | `...portal.index` | `web`, maintenance, `page.cache`, `public.cache` | `index` | pública | `page` | HTML 12/página | `:7-10` |
| R20 | GET | `/regional/{slug}` | `...portal.show` | idem | `show` | pública | — | HTML + 404 | `:12-15` |

### 5.3 API — `Routes/api.php` (prefixo `api/v2/unidaderegional`)

| # | Método | URI | Nome | Middleware | Controller@ação | Auth | Throttle | Entrada | Resposta | Fonte |
|---|---|---|---|---|---|---|---|---|---|---|
| R21 | — | grupo | — | `api`, `bindings` | — | Bearer | — | agrupa 8 endpoints | — | `Routes/api.php:3` |
| R22 | GET | `/noticias` | `...api.noticias.index` | grupo | `UnidadeRegionalNewsApiController@index` | Bearer | — | `per_page` | `200` lista | `:4-5` |
| R23 | POST | `/noticias` | `...api.noticias.store` | grupo + `throttle.api:30,1` + `throttle:100,1` | `@store` | Bearer | 30/min token, 100/min IP | `ApiNewsRequest` | `201` | `:7-9` |
| R24 | POST | `/noticias/{id}` | `...api.noticias.update` | idem | `@update` | Bearer | idem | `ApiNewsRequest` | `200` | `:11-13` |
| R25 | DELETE | `/noticias/{id}` | `...api.noticias.destroy` | grupo | `@destroy` | Bearer | — | `{id}` | `200` | `:15-16` |
| R26 | POST | `/associados` | `...api.associados.store` | grupo + throttles | `UnidadeRegionalEmpresaApiController@store` | Bearer | idem | `ApiEmpresaRequest` | `201` (vincula qualquer associado nacional não arquivado, sem checar origem) | `:18-20` |
| R27 | POST | `/associados/{id}` | `...api.associados.update` | grupo + throttles | `@update` | Bearer | idem | `ApiEmpresaRequest` | `200`; `200 "Nenhuma alteração detectada."` quando só `tags`/`status` mudariam (não aplica) | `:22-24` |
| R28 | GET | `/associados` | `...api.associados.index` | grupo | `@index` | Bearer | — | — | `200` | `:26-27` |
| R29 | DELETE | `/associados/{id}` | `...api.associados.destroy` | grupo | `@destroy` | Bearer | — | `{id}` | `200` | `:29-30` |
| R30 | GET | `/openapi.json` | `...api.openapi` | `api` (fora do grupo `bindings`) | `UnidadeRegionalApiController@openapi` | pública | — | — | `200` spec | `:37-38` |

## 6. Dados e modelo de domínio

Rotas não alteram o schema diretamente; as tabelas escritas por cada uma estão em
[`dados.md`](dados.md) §6. Resumo por rota:

| Rota | Escreve |
|---|---|
| R04/R06 | `v2_unidade_regionais`, pivô de tags, `v2_unidade_regional_media` |
| R07/R09 | `v2_unidade_regionais` (`is_active`, token, `deleted_at`) |
| R10/R11/R12 | `v2_unidade_regional_media` (+ `v2_gallery_media` no R10) |
| R13/R14 | colunas `api_token_*` |
| R17 | `v2_portal_configs` (`theme_regionais_bg`) |
| R23/R24/R25 | `v2_news`, `v2_news_tags`, pivô de tags de notícia, `v2_gallery_media` |
| R26/R27/R29 | `v2_associado_instituicoes`, `v2_unidade_regional_empresas`, tags de associado |
| R19/R20/R22/R28/R30 | somente leitura |

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant B as Browser
  participant M as module.active
  participant A as CmsAuthenticate
  participant P as V2PermissionMiddleware
  participant C as Controller
  B->>M: GET /cms/unidades-regionais
  M-->>B: 404/redirect se módulo inativo
  M->>A: ativo
  A-->>B: 302 login se anônimo
  A->>P: autenticado com papel
  P-->>B: 403/redirect se sem UnidadeRegional
  P->>C: autorizado
  C-->>B: 200 HTML
```

- **FL-UNIDADEREGIONAL-R01 (CMS leitura)**: `page.cache` não intercepta `cms*`; RBAC decide;
  controller pagina.
- **FL-UNIDADEREGIONAL-R02 (CMS escrita)**: CSRF (`web`) → RBAC `write` → controller →
  `clearPageCache` → redirect.
- **FL-UNIDADEREGIONAL-R03 (API)**: `api`+`bindings` → throttle (POST) → Bearer → controller →
  `clearPageCache`.
- **FL-UNIDADEREGIONAL-R04 (portal)**: `page.cache` → (MISS) controller → `public.cache` →
  gravação no Redis.

## 8. UI CMS

Telas, campos e mensagens em [`cms.md`](cms.md). Nesta camada, o contrato relevante é:
escritas exigem `_token` (CSRF) e `_method` quando o form usa PUT/DELETE.

## 9. UI Portal

Detalhe em [`portal.md`](portal.md). Rotas públicas não têm filtros de query além de `page`.

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

| Item | Valor | Efeito | Fonte |
|---|---|---|---|
| `module.active:UnidadeRegional` | ativo se não houver flag | bloqueia CMS com 404/redirect | `V2/Core/Services/ModuleFlagService.php:93-94` |
| RBAC key | `UnidadeRegional` | `read`/`write`/`none` | `V2/Core/Middleware/V2PermissionMiddleware.php:83-110` |
| `active_pattern` | `cms/unidades-regionais*` | casa a URI para o RBAC | `ModuleProvider.php:21` |
| prefixo de API | `api/v2/unidaderegional` | contrato externo | `V2ServiceProvider.php:309-313` |
| throttle POST | `30,1` por token; `100,1` por IP | 429 | `Routes/api.php`; `ThrottleApiToken.php:22` |

## 11. Observabilidade, erros e limites

- `429` com corpo próprio em `throttle.api`; `429` do Laravel em `throttle:100,1`; CSRF inválido
  → `419`.
- `404` de `findOrFail` no CMS retorna página de erro padrão (não há `try/catch`).
- Logs de falha de cache: `[UnidadeRegionalCmsController] Falha ao limpar cache de páginas`.
- Não há log de acesso às rotas públicas.
- `POST /noticias` pode emitir requisições server-side para hosts internos (SSRF) sem log do
  destino; ver [`api.md`](api.md) §3.6.

## 12. Seeds e dados iniciais

Não se aplica: rotas não semeiam. A unidade `NAC` criada pelo seeder aparece em `/regional/nacional`.

## 13. Lacunas

| ID | Lacuna | Onde procurar | Impacto |
|---|---|---|---|
| LG-UNIDADEREGIONAL-R01 | Não há teste de contrato para as 3 rotas de galeria (sem consumidor) | `tests/Feature/.../UnidadeRegional*` | podem ser removidas sem quebra aparente |
| LG-UNIDADEREGIONAL-R02 | Não está claro se `per_page` deve ter teto | `UnidadeRegionalNewsApiController.php:69-71` | abuso de payload |
| LG-UNIDADEREGIONAL-R03 | O link `?regional=` para notícias não é honrado; não há issue registrada | `Views/portal/show.blade.php:293`; `NoticiasPortalController.php:52` | navegação mostra todas as notícias |
| LG-UNIDADEREGIONAL-R04 | Sitemap inclui inativas — não se sabe se é intencional | `SeoPortalController.php:365-377` | 404 no sitemap |

## 14. Checklist de paridade

- [ ] `PC-UNIDADEREGIONAL-R01` — módulo desativado → `GET /cms/unidades-regionais` não chega ao controller (`ModuleAccessMiddleware.php:44-62`).
- [ ] `PC-UNIDADEREGIONAL-R02` — `GET /regionais` retorna `X-Cache-Status` e `Cache-Control: public, max-age=60, must-revalidate` (`PageCacheMiddleware.php:45-70`; `PublicCacheMiddleware.php:83-91`).
- [ ] `PC-UNIDADEREGIONAL-R03` — `POST /noticias` sem CSRF não se aplica (API), mas `POST /cms/.../nova` sem `_token` retorna `419`.
- [ ] `PC-UNIDADEREGIONAL-R04` — 4 POST da API declaram os dois throttles (`Routes/api.php:7-9,11-13,18-20,22-24`).
- [ ] `PC-UNIDADEREGIONAL-R05` — `GET/POST/DELETE` sem throttle além dos POST (`Routes/api.php:4-6,15-16,26-30`).
- [ ] `PC-UNIDADEREGIONAL-R06` — `/openapi.json` responde sem `Authorization` (`Routes/api.php:37-38`).
- [ ] `PC-UNIDADEREGIONAL-R07` — `PUT` e `POST` em `/editar/{id}` levam ao mesmo controller (`Routes/cms.php:28-31`).
- [ ] `PC-UNIDADEREGIONAL-R08` — `DELETE /cms/unidades-regionais/{id}` retorna `302` e não `204` (`Controllers/UnidadeRegionalCmsController.php:287`).
- [ ] `PC-UNIDADEREGIONAL-R09` — `GET /regional/{slug}` de unidade inativa → `404` (`Controllers/UnidadeRegionalPortalController.php:47-51`).

## 15. Evidências consultadas

- `V2/Modules/Conexoes/UnidadeRegional/Routes/cms.php:1-87`
- `V2/Modules/Conexoes/UnidadeRegional/Routes/web.php:1-16`
- `V2/Modules/Conexoes/UnidadeRegional/Routes/api.php:1-38`
- `V2/Modules/Conexoes/UnidadeRegional/ModuleProvider.php:14-33`
- `V2/Core/Providers/V2ServiceProvider.php:84-143,283-313`
- `V2/Core/Middleware/CmsAuthenticate.php:16-44`
- `V2/Core/Middleware/V2PermissionMiddleware.php:22-110`
- `V2/Core/Middleware/ThrottleApiToken.php:22-60`
- `V2/Core/Middleware/PageCacheMiddleware.php:14-91`
- `V2/Core/Middleware/PublicCacheMiddleware.php:30-91`
- `V2/Core/Middleware/ModuleAccessMiddleware.php:39-62`
- `V2/Core/Services/ModuleFlagService.php:72-95`
- `app/Http/Kernel.php:35-67`
- `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalCmsController.php:27-537`
- `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalPortalController.php:30-120`
- `V2/Modules/Conexoes/UnidadeRegional/Views/cms/index.blade.php:28-56`
- `V2/Modules/Conexoes/UnidadeRegional/Views/cms/edit.blade.php:380-420`
- `V2/Modules/Conexoes/UnidadeRegional/Views/portal/show.blade.php:288-298`
- `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasPortalController.php:52-70`
- `V2/Services/SEO/Controllers/SeoPortalController.php:363-385`
