# Notícias (V2) — Rotas e Controladores

> **Slug:** `publicacoes-noticias` · **Versão analisada:** V2.
>
> Caminhos relativos a `lotep/`. As rotas são carregadas por `V2ServiceProvider::mapModuleRoutes()` (`V2/Core/Providers/V2ServiceProvider.php:508-535`), que aplica o namespace `V2\Modules\Publicacoes\Noticias\Controllers`.

---

## 1. Superfícies

| Superfície | Arquivo de rotas | Prefixo | Middleware |
|---|---|---|---|
| CMS | `V2/Modules/Publicacoes/Noticias/Routes/cms.php` | `cms/noticias` | `web` + `module.active:Noticias` (provider) + `v2.authenticate` + `v2.permission:noticias` (arquivo) |
| Portal | `V2/Modules/Publicacoes/Noticias/Routes/web.php` | raiz do domínio | `web` + `MaintenanceMiddleware` (provider) + `web` (arquivo; duplicado) |

Não existe arquivo de rotas de API no módulo.

## 2. Rotas do CMS

`V2/Modules/Publicacoes/Noticias/Routes/cms.php:11-14` abre o grupo:

```php
Route::group([
    'middleware' => ['v2.authenticate', 'v2.permission:noticias'],
    'prefix' => 'cms/noticias',
], function () {
```

| Método | URI | Nome | Controller@action | Linha | Retorno |
|---|---|---|---|---|---|
| GET | `/cms/noticias` | `v2.noticias.cms.index` | `NoticiasCmsController@index` | 17 | View `cms.index` |
| GET | `/cms/noticias/pendentes` | `v2.noticias.cms.pending` | `@pending` | 22 | View `cms.index` (filtrada) |
| GET | `/cms/noticias/novo` | `v2.noticias.cms.create` | `@create` | 27 | View `cms.create` |
| POST | `/cms/noticias/novo` | `v2.noticias.cms.store` | `@store` | 32 | Redirect |
| GET | `/cms/noticias/editar/{id}` | `v2.noticias.cms.edit` | `@edit` | 37 | View `cms.edit` |
| PUT/POST | `/cms/noticias/editar/{id}` | `v2.noticias.cms.update` | `@update` | 42 | Redirect |
| GET | `/cms/noticias/preview/{id}` | `v2.noticias.cms.preview` | `@preview` | 47 | View `cms.preview` |
| GET | `/cms/noticias/preview-iframe/{id}` | `v2.noticias.cms.preview-iframe` | `@previewIframe` | 52 | HTML standalone |
| DELETE | `/cms/noticias/{id}` | `v2.noticias.cms.destroy` | `@destroy` | 57 | Redirect (soft delete) |
| POST | `/cms/noticias/{id}/toggle-highlight` | `v2.noticias.cms.toggle-highlight` | `@toggleHighlight` | 63 | JSON |
| POST | `/cms/noticias/set-super-highlight` | `v2.noticias.cms.set-super-highlight` | `@setSuperHighlight` | 68 | Redirect |
| GET | `/cms/noticias/arquivadas` | `v2.noticias.cms.archived` | `@archived` | 74 | View `cms.archived` |
| POST | `/cms/noticias/{id}/restaurar` | `v2.noticias.cms.restore` | `@restore` | 79 | Redirect |
| POST | `/cms/noticias/{id}/submeter` | `v2.noticias.cms.submit-approval` | `@submitForApproval` | 85 | Redirect |
| POST | `/cms/noticias/{id}/aprovar` | `v2.noticias.cms.approve` | `@approve` | 90 | Redirect |
| POST | `/cms/noticias/{id}/rejeitar` | `v2.noticias.cms.reject` | `@reject` | 95 | Redirect |
| POST | `/cms/noticias/{id}/seo` | `v2.noticias.cms.seo-update` | `@updateSeo` | 101 | Redirect |
| GET | `/cms/noticias/tags` | `v2.noticias.cms.tags.index` | `@tagsIndex` | 107 | JSON |
| POST | `/cms/noticias/tags` | `v2.noticias.cms.tags.store` | `@tagsStore` | 112 | JSON |
| DELETE | `/cms/noticias/tags/{id}` | `v2.noticias.cms.tags.destroy` | `@tagsDestroy` | 117 | JSON |
| GET | `/cms/noticias/categorias` | `v2.noticias.cms.categories` | `@categoriesPage` | 126 | View `cms.categories` |
| GET | `/cms/noticias/categorias/json` | `v2.noticias.cms.categories.json` | `@categoriesIndex` | 131 | JSON |
| POST | `/cms/noticias/categorias` | `v2.noticias.cms.categories.store` | `@categoriesStore` | 136 | JSON 201 ou redirect |
| PUT/POST | `/cms/noticias/categorias/{id}` | `v2.noticias.cms.categories.update` | `@categoriesUpdate` | 141 | JSON ou redirect |
| DELETE | `/cms/noticias/categorias/{id}` | `v2.noticias.cms.categories.destroy` | `@categoriesDestroy` | 146 | JSON ou redirect |
| GET | `/cms/noticias/config` | `v2.noticias.cms.config` | `NoticiasConfigController@index` | 152 | View `cms.config.index` |
| POST | `/cms/noticias/config/sidebar` | `v2.noticias.cms.config.sidebar` | `@saveSidebarConfig` | 157 | Redirect |
| POST | `/cms/noticias/config/approvers` | `v2.noticias.cms.config.approvers.add` | `@addApprover` | 162 | Redirect |
| DELETE | `/cms/noticias/config/approvers/{id}` | `v2.noticias.cms.config.approvers.remove` | `@removeApprover` | 167 | Redirect |

**Ordem de declaração importa** porque `{id}` é um segmento curinga:

- Não há `GET /cms/noticias/{id}`, portanto `tags`, `categorias`, `config` e `arquivadas` não são capturados como ID.
- `POST /cms/noticias/set-super-highlight` é declarado depois de `POST /{id}/toggle-highlight`; os padrões não colidem (o segundo exige dois segmentos).
- `PUT/POST /categorias/{id}` e `DELETE /categorias/{id}` convivem com `GET /categorias/json`; a rota `json` é declarada antes das de `{id}`, evitando que `json` seja tratado como ID.

## 3. Rotas do portal

`V2/Modules/Publicacoes/Noticias/Routes/web.php:15-42`.

| Método | URI | Nome | Controller@action | Linha | Retorno |
|---|---|---|---|---|---|
| GET | `/noticias` | `v2.noticias.portal.index` | `NoticiasPortalController@index` | 20 | View `portal.index` |
| GET | `/noticia/{slug}` | `v2.noticias.portal.show` | `@show` | 26 | View `portal.show` |
| GET | `/noticia/{slug}/preview` | `v2.noticias.portal.preview` | `@preview` | 32 | View `portal.show` com `previewMode` |
| GET | `/news-sitemap.xml` | `v2.noticias.sitemap.news` | `NoticiasSitemapController@index` | 38 | `application/xml` |

Observações:

- O docblock do arquivo cita `v2.maintenance`, mas o arquivo declara apenas `web`. O bloqueio de manutenção vem do grupo do provider (`MaintenanceMiddleware`), não do arquivo.
- `/noticia/{slug}` e `/news-sitemap.xml` convivem com o fallback `/{slug}` do BasicPage, registrado **depois** dos módulos (`V2/Core/Providers/V2ServiceProvider.php:489-498`).
- O preview usa query string `?token=`; não há rota assinada nem expiração.

## 4. Controladores e ações

### 4.1 `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php`

| Ação | Linha | Comportamento |
|---|---|---|
| `index` | 64 | Filtros `status`, `category_id`, `tag_id`, `search`; ordenação `title|created_at|published_at|status`; paginação 15; carrega `coverImage`, `category`, `tags`; conta status; carrega todas as publicadas para o modal de super-destaque. |
| `pending` | 141 | Igual à listagem, restrita a `pendingApproval()` + `recent()`. |
| `archived` | 168 | `onlyTrashed()`, ordena por `deleted_at`, pagina 15. |
| `restore` | 186 | Restaura e força `draft` + novo `draft_token`; audita; limpa cache. |
| `create` | 217 | Categorias ordenadas, tags, nome do usuário Sentinel, categoria padrão `nacional`. |
| `store` | 243 | Gera UUID/slug, status do request, autor, token se draft, SEO padrão, capa, tags, galeria; audita; limpa cache. |
| `edit` | 359 | Carrega notícia com relações e renderiza `cms.edit` (nunca a listagem). |
| `update` | 378 | Atualiza conteúdo/status/categoria/SEO, publica se receber `status=published`, grava rejeição, preserva galeria ausente; audita; limpa cache. |
| `preview` | 503 | Renderiza a view de preview do CMS e chama `SEOService::bindModel()`. |
| `previewIframe` | 524 | Renderiza HTML standalone com o AST convertido; usado no iframe do editor. |
| `submitForApproval` | 552 | Só `draft`/`rejected`; muda status; audita; notifica aprovadores. |
| `approve` | 592 | Só `pending_approval`; publica, seta revisor, anula token; audita. |
| `reject` | 626 | Só `pending_approval`; exige `rejection_reason`; audita. |
| `destroy` | 664 | Soft delete; audita; limpa cache. |
| `toggleHighlight` | 694 | Inverte `is_highlighted`; audita; limpa cache; JSON `{success,is_highlighted}`. |
| `setSuperHighlight` | 720 | Zera todas, marca uma (ou nenhuma); audita; limpa cache. |
| `updateSeo` | 753 | Valida e substitui `metadata.seo`; audita; limpa cache. |
| `tagsIndex` / `tagsStore` / `tagsDestroy` | 801/813/835 | JSON; `firstOrCreate` por slug; soft delete. |
| `categoriesPage` | 876 | Categorias com `children` e `parent`, tags. |
| `categoriesIndex` | 889 | JSON de categorias ordenadas. |
| `categoriesStore/Update/Destroy` | 901/938/984 | Bloqueia `is_system`; ao remover, desvincula notícias (`category_id = null`). |
| `clearPageCache` | 1015 | Privado; apaga `laravel:page:*` no Redis. |

### 4.2 `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasPortalController.php`

| Ação | Linha | Comportamento |
|---|---|---|
| `index` | 52 | `published()` + filtros opcionais de categoria/tag; paginação 12; categorias raiz + filhas; SEO de listagem. |
| `show` | 90 | Busca por slug publicado; SEO; render do AST; relacionadas por tag com fallback; prev/next. |
| `resolveAdjacentNews` | 160 | Comparação composta `(COALESCE(published_at, created_at), id)` — determinística. |
| `adjacentReference` | 195 | Normaliza `published_at` (Carbon ou string) para `Y-m-d H:i:s`. |
| `preview` | 218 | Exige `token` válido via `News::isAccessible()`; SEO com prefixo `[Preview]`. |

### 4.3 `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasSitemapController.php`

| Ação | Linha | Comportamento |
|---|---|---|
| `index` | 24 | Purge oportunista entre 03:00–03:10; `Cache::remember` com TTL de 360 minutos; `Content-Type: application/xml`. |
| `generateSitemap` | 46 | `DB::table('v2_news')` com `status=published` e `published_at >= -6 months OR NULL`, limite 1000; monta `urlset` com `news:news`, `publication_date`, `title` e `keywords` do metadata. |

### 4.4 `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasConfigController.php`

| Ação | Linha | Comportamento |
|---|---|---|
| `index` | 39 | Lista aprovadores com usuário, `sidebar_items` e todos os usuários. |
| `addApprover` | 64 | Valida `user_id` existente em `users`, vigência opcional; impede duplicado ativo. |
| `removeApprover` | 107 | Soft delete. |
| `saveSidebarConfig` | 131 | `NewsConfig::setValue('sidebar_items', $itens)`. |

## 5. Form Request

`V2/Modules/Publicacoes/Noticias/Requests/NewsRequest.php`.

| Campo | Regra | Efeito |
|---|---|---|
| `title` | `required|string|max:255` | Obrigatório. |
| `subtitle` | `nullable|string|max:500` | Linha fina. |
| `content` | `nullable|string` | AST JSON; validado extra como JSON. |
| `summary` | `nullable|string|max:1000` | Resumo. |
| `status` | `required|string|in:draft,pending_approval,published,rejected` | Define o estado final. |
| `slug` | `nullable|string|max:255` (+`unique` no update) | Validado, mas ignorado pelo controller. |
| `metadata` | `nullable|array` | SEO. |
| `cover_image_id` | `nullable|string|max:36` | Capa. |
| `category_id` | `nullable|string|max:36` | Categoria; a UI marca como obrigatória, o backend não. |

Comportamentos:

- `authorize()` retorna `true` — a autorização real é do middleware (`V2/Modules/Publicacoes/Noticias/Requests/NewsRequest.php:20-23`).
- `prepareForValidation()` preenche `slug` a partir do título e `status=draft` quando ausente (`:118-135`).
- `withValidator()` valida que `content` é JSON válido quando string (`:86-95`).
- A regra `unique` de slug só é adicionada quando há `{id}` na rota (`:48-58`), mas como o controller não usa `slug` no update, ela é inócua.

## 6. Permissões exigidas

| Operação | Como é autorizada |
|---|---|
| Acessar qualquer rota CMS | `v2.authenticate` → exige Sentinel + papel; delega ao permission middleware. |
| Leitura CMS (GET/HEAD) | Permissão `read` ou `write` no módulo `Noticias`. |
| Escrita CMS (POST/PUT/DELETE) | Permissão `write`. |
| Aprovar/rejeitar/publicar/arquivar | Também `write` — **não há nível `aprovar` nem `arquivar`**. |
| Acesso irrestrito | E-mail em `config('v2.support.email')` ou papel `super-admin` (`V2/Core/Middleware/V2PermissionMiddleware.php:70-72`). |
| Módulo desativado | `module.active:Noticias` responde 404/redirect antes do controller. |

O middleware ignora o argumento textual `noticias`: resolve o módulo pelo `active_pattern` do `ModuleProvider` (`'cms/noticias*'`, `V2/Modules/Publicacoes/Noticias/ModuleProvider.php:41`). A chave canônica resultante é `Noticias`.

## 7. API Resources / presenters

Não existem. As respostas JSON são o próprio model serializado:

- Tags: `response()->json($tags)` — expõe `id`, `name`, `slug`, timestamps.
- Categorias: `response()->json($category, 201)` e `response()->json($category)`.
- Highlight: payload controlado `{success, is_highlighted}`.

Isso significa que campos internos (`deleted_at`, `metadata`) podem vazar em futuras serializações de model; no V7, usar resources explícitos (`06-MIGRACAO-MODERNA.md`).
