# Notícias (V2) — Gaps e Dívidas

> **Slug:** `publicacoes-noticias` · **Versão analisada:** V2.
>
> Cada item é rastreável ao arquivo/linha indicado. Itens marcados `[INFERENCE]` foram deduzidos por leitura de código e não por execução.

---

## 1. Riscos prioritários (sinalizados no aceite)

### G1 — `update()` aceita `status` e publica sem aprovação

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:386` e `:415-418`.

```php
$news->status = $data['status'] ?? $news->status;
```

O `NewsRequest` aceita qualquer status válido e o formulário envia `status=published` no botão "Publicar". Quem tem permissão `write` publica direto, sem `pending_approval` e sem revisor. O endpoint `aprovar` existe, mas é contornável.

**Impacto:** o controle editorial é opcional; a auditoria registra `updated` com `old_status`/`new_status`, mas não distingue "publicação aprovada" de "publicação direta".

**Correção no V7:** remover `status` do request de edição e criar Action `PublicarDireto` protegida por `noticias.aprovar` (ver `06-MIGRACAO-MODERNA.md`).

### G2 — `approve()` e `reject()` não invalidam o cache de página

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:592-651`. Nenhuma das duas chama `clearPageCache()`, diferente de `store`, `update`, `destroy` e `restore`.

**Impacto:** a notícia muda de `pending_approval` para `published` (ou `rejected`), mas o portal pode continuar servindo HTML cacheado. O `News::saved` atualiza apenas os índices de busca, não o cache de página.

**Correção:** invalidação por evento `NoticiaPublicada`/`NoticiaRejeitada`, com tags de cache, não varredura de chaves.

### G3 — `Redis::keys` é O(N) e bloqueante

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:1027-1032`.

```php
$keys = $redis->keys($pattern);
if (!empty($keys)) {
    $redis->del($keys);
}
```

`KEYS` percorre todo o keyspace; em Redis com muitas chaves, bloqueia o servidor. `DEL` com múltiplas chaves de slots diferentes falha em Cluster.

**Correção:** cache com tags (`Cache::tags(['noticias','paginas'])->flush()`) ou versão de chave (`page_version`), e `UNLINK` quando aplicável.

### G4 — RBAC `read`/`write` não distingue aprovar nem arquivar

**Onde:** `V2/Core/Middleware/V2PermissionMiddleware.php:101-127`; `NewsRequest::authorize()` retorna `true` (`V2/Modules/Publicacoes/Noticias/Requests/NewsRequest.php:20-23`).

Quem pode editar pode aprovar, rejeitar, publicar e arquivar. Não há nível `aprovar`, nem restrição por autor, nem segregação de funções.

**Impacto:** o fluxo de aprovação perde valor como controle; qualquer redator aprova a própria matéria.

**Correção:** ADR-033 — quatro ações; Policy por ação; testes de middleware (hoje os testes chamam o controller direto).

### G5 — `allPublished` sem limite

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:115-120` (index) e `:151-156` (pending).

```php
$allPublished = News::published()
    ->select('id', 'title', 'published_at')
    ->orderBy('is_super_highlighted', 'desc')
    ->orderBy('published_at', 'desc')
    ->get();
```

O modal de super-destaque carrega **todas** as publicadas em cada render de listagem/pendentes. Custo de memória e tempo cresce linearmente com o acervo.

**Correção:** busca assíncrona no modal (endpoint com paginação/filtro) ou `limit(200)` com busca por título.

## 2. Bugs e comportamentos incorretos

### G6 — Sitemap inclui notícias arquivadas

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasSitemapController.php:54-63` usa `DB::table('v2_news')`, sem `whereNull('deleted_at')`.

**Impacto:** matéria publicada e depois arquivada permanece no `news-sitemap.xml` por até 6 meses (ou até sair do limite de 1000), gerando soft-404 no Google News.

### G7 — Arquivar não remove do índice público

**Onde:** `News` usa `SoftDeletes`; `delete()` executa um `UPDATE` de `deleted_at` via query builder e **não dispara `saved`**. Logo `syncToSearch()` não roda e o registro permanece em `v2_public_search_index`.

**Impacto:** a busca interna leva a uma notícia que o portal não exibe (404 ou sumiço do conteúdo).

### G8 — Recriar tag/categoria após soft delete quebra com UNIQUE

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:820-831` (`tagsStore` → `firstOrCreate(['slug' => $slug])`) e `:901-936` (`categoriesStore`).

O `firstOrCreate` respeita o global scope de `SoftDeletes`, então não encontra a linha arquivada; tenta inserir; o índice UNIQUE de `slug` (`V2/Modules/Publicacoes/Noticias/Migrations/2026_08_19_000002_create_v2_news_tags_table.php:24` e `V2/Modules/Publicacoes/Noticias/Migrations/2026_08_19_000001_create_v2_news_categories_table.php:24`) rejeita → `QueryException` 500.

O mesmo ocorre em `resolveTagIds()` (`:846-862`) ao digitar o nome de uma tag arquivada.

### G9 — Laço de slug de categoria/tag pode não terminar

**Onde:** `V2/Modules/Publicacoes/Noticias/Models/NewsCategory.php:95-102` e `V2/Modules/Publicacoes/Noticias/Models/NewsTag.php:71-78`.

```php
$query = static::where('slug', $slug);
if (!empty($model->id)) {
    $query->where('id', '!=', $model->id);
}
while ($query->exists()) {
    $slug = $baseSlug . '-' . $counter;
    $counter++;
}
```

O `where` é montado **uma vez** com o slug original. Se esse slug já existir (e não for o próprio registro), `exists()` continua `true` para sempre: o laço não termina e a requisição estoura timeout/consumo de CPU. `[INFERENCE]` por leitura do builder do Eloquent 5.5.

O `News` faz certo, reconstruindo `static::where('slug', $slug)` a cada iteração (`V2/Modules/Publicacoes/Noticias/Models/News.php:353-363`).

### G10 — Slug do formulário é ignorado

**Onde:** `store()` (`V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:243-357`) e `update()` (`:378-500`) nunca atribuem `$data['slug']`. O hook `saving` mantém o slug existente.

**Impacto:** o campo "Slug (URL)" e o botão "Regenerar" não têm efeito após a criação; o `unique` do request valida algo que não será gravado.

### G11 — `updateSeo()` apaga campos omitidos

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:776-783` monta `$metadata['seo']` do zero. Um POST parcial grava `null` em `json_ld`, `image_id`, `image_url` etc.

### G12 — Preview de notícia publicada com token inválido abre normalmente

**Onde:** `News::isAccessible()` retorna `true` para `published` antes de checar o token (`V2/Modules/Publicacoes/Noticias/Models/News.php:417-429`). A rota `/noticia/{slug}/preview` mostra o banner "[Preview]" mesmo sem token válido.

Não vaza rascunho, mas a semântica do token fica inconsistente e o `noindex` não é aplicado.

### G13 — Token de preview sem expiração e em query string

Token persistido, sem TTL, sem assinatura, vazando em logs/Referer. Restaurar gera novo token (bom), mas não há revogação explícita além de publicar.

### G14 — Busca do portal filtra apenas a página atual

**Onde:** `V2/Modules/Publicacoes/Noticias/Views/portal/index.blade.php:222-305` filtra os cartões renderizados. Com 12 por página, termos de outras páginas não são encontrados; o resultado parece "nenhuma notícia".

### G15 — E-mail de submissão é síncrono e pode atrasar a resposta

**Onde:** `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:574` chama `notifyApprovers()` no request; `NewsNotificationService` renderiza a view e envia um e-mail por aprovador, sequencialmente (`V2/Modules/Publicacoes/Noticias/Services/NewsNotificationService.php:73-128`). Sem fila, sem timeout explícito.

### G16 — `submitForApproval()` não limpa cache

A notícia sai de rascunho/rejeitada e entra em pendente; irrelevante para o portal (não publicada), mas inconsistente com o restante das mutações.

## 3. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| Enviar para aprovação pela UI | Rota e método existem; **não há botão** no formulário. |
| Aprovar/rejeitar pela UI | Rotas existem; a listagem não tem botões de aprovar/rejeitar nem modal de rejeição renderizado (o JS procura `#rejectModal`, ausente nas views atuais). |
| `sidebar_items` | Salvo e lido em `NoticiasConfigController`, mas não foi encontrado consumo pelo layout/sidebar. Configuração efetivamente morta. |
| `News::$statusIcons`, `$statusColors` | Usados nas views CMS; ok. |
| `MarkdownToTiptapConverter` | Não referenciado por controllers/rotas deste módulo; provável uso em importação/CLI externa. |
| `V2/Modules/Publicacoes/Noticias/Views/cms/includes/form.blade.php` | Include não usado por `create`/`edit` (ambos usam `_editor-content`). |
| `is_highlighted` no portal | As views públicas analisadas **não** usam `is_highlighted` nem `is_super_highlighted`; os flags são lidos apenas no CMS. O efeito visual depende de outro módulo/bloco (ex.: home), não encontrado nesta fatia. |
| `v2_news_configs` | Apenas uma chave; sem cache. |

## 4. Riscos de dados

| Risco | Detalhe |
|---|---|
| `cover_image_id` string ↔ `v2_gallery_media.id` inteiro | O `belongsTo` funciona por comparação textual, mas a migração de dados precisa normalizar; o pivot já foi convertido para bigint em `V2/Modules/Publicacoes/Noticias/Migrations/2026_08_24_130000_fix_gallery_media_id_type_on_v2_news_gallery_pivot.php`. |
| `author_id`/`reviewer_id` string ↔ `users.id` inteiro | `belongsTo` compara tipos; em PostgreSQL, `'1' = 1` é erro de tipo em comparação de colunas, mas o Eloquent busca por valor e o driver pode converter. Não há teste cobrindo nome do autor quando `author_id` é UUID. |
| `category_id` sem FK | Categoria soft-deleted continua referenciada; `news()` filtra notícias sem escopo de categoria, então a matéria pode exibir categoria arquivada. |
| `v1_id` UNIQUE parcial | Importadores concorrentes podem colidir; não há `ON CONFLICT` no módulo. |
| `metadata` sem schema | JSON livre; `seo` pode vir com tipos inesperados e quebrar `updateSeo`/`bindModel`. |
| `content` é `text`, não `jsonb` | Não há validação estrutural no banco; AST inválido só falha no renderer. |
| Soft delete sem cascade | Pivots e referências permanecem após arquivar; tags/categorias arquivadas deixam pivots órfãos. |

## 5. Perguntas em aberto para o negócio

1. **Aprovação é obrigatória para toda notícia ou apenas para determinadas categorias/autores?** O código permite publicar direto; a UI chama o botão de "Publicar".
2. **Quem pode aprovar?** A lista `v2_news_approvers` é usada apenas para e-mail; a autorização de rota não consulta essa lista.
3. **O autor pode aprovar a própria matéria?** Não há regra; `NewsNotificationService` apenas evita notificar o autor da ação.
4. **O que `is_highlighted` e `is_super_highlighted` controlam no portal?** As views deste módulo não os consomem; o consumidor está fora da fatia.
5. **O slug pode mudar?** A UI permite editar, o backend ignora. Se a resposta for "sim", é preciso política de redirect 301.
6. **Qual o prazo de retenção de arquivadas?** Hoje o soft delete é indefinido e não há purge.
7. **O token de preview deve expirar?** Hoje é permanente até publicar/restaurar.
8. **Qual o limite aceitável de itens no seletor de super-destaque?** Hoje carrega todas as publicadas.
9. **A busca pública deve cobrir apenas título/resumo ou o corpo inteiro?** O índice público recebe `summary` + categoria, não o texto renderizado.
10. **Notícias importadas da V1 precisam manter autoria/fonte?** As colunas existem, mas a UI atual não as edita (`legacy_author`/`source` não aparecem no formulário; `source` aparece em `_editor-content` como "Fonte", `legacy_author` não).

## 6. Mapa entre versões

- Este dossiê cobre a implementação **V2** (`lotep/V2`). O portal legado (v5/v6) tem dossiê próprio em `../noticias/`; não há reuso de código entre eles.
- A migração V2 → V7 não é um port linha a linha: o V7 é greenfield e o conteúdo será migrado manualmente (ADR-042). O que se preserva é o **comportamento** descrito em `00-RESUMO.md` e `05-REGRAS-E-PORTABILIDADE.md`.
- As colunas de importação V1 (`v1_id`, `imported_at`, `legacy_author`, `source`) existem na V2, mas **não devem** ser replicadas no V7.

## 7. Dívidas técnicas transversais

| Dívida | Consequência |
|---|---|
| Controller de 1041 linhas | Baixa coesão, difícil testar, regras duplicadas. |
| Regras em hooks de model | Efeitos colaterais em qualquer `save()`, inclusive em testes e importadores. |
| Sem transações em operações compostas (galeria + tags + notícia + auditoria) | Falha parcial deixa estado inconsistente. |
| `Log::info` manual em store/update/destroy/index | Ruído; no V7, observabilidade nativa (ADR-024). |
| `flash()` acoplado ao controller | Dificulta reuso em API. |
| CSS/JS inline e duplicação de estilos | CSP e manutenção. |
| Sem paginação em `allPublished` e `tagsIndex`/`categoriesIndex` | Crescimento silencioso. |
| `NewsRequest` valida `slug` que não é usado; valida `content` como string | Contrato enganoso. |
