---
id: MOD-PUBLICACOES-NOTICIAS-DADOS
tipo: modulo
onda: 4
status: concluido
unidades: [MOD-PUBLICACOES-NOTICIAS]
fontes:
  - V2/Modules/Publicacoes/Noticias/
depende_de: [CORE-DATA, CORE-BOOT, MOD-CONEXOES-UNIDADEREGIONAL, SVC-GALLERY, SVC-SEARCH, SVC-FEEDRSS, SVC-ACTIVITYLOGGER]
consumidores: [MOD-PUBLICACOES-NOTICIAS, MOD-SUPPORT-NOTICIASIMPORT, MOD-CONEXOES-UNIDADEREGIONAL, MOD-LAYOUTS-HOMEPAGE, SVC-SEARCH, SVC-FEEDRSS, UI-PORTAL, UI-CMS]
criticidade: alta
dono: NoticiasA
---

# Publicações/Notícias — dados

> Anexo do [`spec.md`](spec.md). Responde: quais tabelas o módulo cria, quem mais
> escreve nelas, quais colunas/índices/FKs existem de fato, o que o hook de model
> provoca e o que restou do workflow de aprovação removido.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Cria e mantém seis tabelas (`v2_news`, `v2_news_categories`, `v2_news_tags`, `v2_news_tag_pivot`, `v2_news_gallery_media`, `v2_news_import_keys`) com UUID v4, soft delete apenas em `v2_news` e um `tsvector` mantido por trigger. |
| Entrada | Migrations do módulo (10 arquivos, 2026-07-08 a 2026-09-17); escritas do CMS, da importação e da API regional. |
| Saída | Esquema PostgreSQL; linhas lidas por portal, sitemap, busca, feed e widgets da Homepage. |
| Efeitos colaterais | Trigger `trg_v2_news_search_vector`; índice GIN; hooks `creating/saving/saved/deleted`; renomeação para `backup_*` no rollback. |
| Particularidade central | O `slug` é único por índice de banco **e** recalculado em PHP; a categoria/regional não têm FK (exceto `unidade_regional_id`); o workflow de aprovação foi removido por migration, mas o código ainda o referencia. |
| Como se integra | `loadMigrationsFrom` do `V2ServiceProvider`; models no namespace `V2\Modules\Publicacoes\Noticias\Models`. |
| Se removido, quebra | Portal, CMS, importação, API regional, widgets da Homepage, índices de busca/feed (as linhas ficam órfãs). |
| Superfície pública | Models `News`, `NewsCategory`, `NewsTag`, `NewsImportKey`; tabelas listadas acima. |

## 1. Efeito

### 1.1 O que muda no banco

| Operação | Efeito |
|---|---|
| `migrate` | Cria as seis tabelas, o trigger e o índice GIN, e semeia `nacional`/`regionais` em `v2_news_categories`. |
| `rollback` de `create_v2_news_table` | Renomeia a tabela para `backup_v2_news_<Ymd_His>`, remove índices e a coluna `search_vector`, e **mantém os dados**. |
| `rollback` de `v2_news_categories`, `v2_news_tags`, `v2_news_gallery_media`, `v2_news_import_keys` | Renomeia para `backup_*`; o pivô de tags é **dropado**. |
| `rollback` de `remove_approval_workflow` | Recria as colunas `draft_token`, `reviewer_id`, `rejection_reason`; **não** restaura `v2_news_approvers`/`v2_news_configs`. |

### 1.2 Quem escreve cada tabela

| Tabela | Escritores | Fonte |
|---|---|---|
| `v2_news` | `NoticiasCmsController` (store/update/destroy/restore/highlights/SEO), `NoticiasImportService`, `UnidadeRegionalNewsApiController` (store/update/destroy) | `Controllers/NoticiasCmsController.php:236-350,400-515,524-545,588-683`; `Services/NoticiasImportService.php:263-301`; `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalNewsApiController.php:191-268` |
| `v2_news_categories` | `NoticiasCmsController` (CRUD de categorias), `RegionalIdentityService` (criação/reparo `regional-<sigla>`), migration de seed | `Controllers/NoticiasCmsController.php:776-855`; `V2/Core/Services/RegionalIdentity/RegionalIdentityService.php:14-61` |
| `v2_news_tags` | `NoticiasCmsController` (store/destroy/resolve), `NoticiasImportService::syncTags` | `Controllers/NoticiasCmsController.php:703-749`; `Services/NoticiasImportService.php:418-428` |
| `v2_news_tag_pivot` | `tags()->sync()` no CMS e na importação; `tagsDestroy` só apaga a tag | `Controllers/NoticiasCmsController.php:312-315,484-487` |
| `v2_news_gallery_media` | `gallery()->sync()` no CMS e na importação | `Controllers/NoticiasCmsController.php:317-329,489-501`; `Services/NoticiasImportService.php:431-448` |
| `v2_news_import_keys` | `NoticiasImportService::registerKey()` | `Services/NoticiasImportService.php:451-463` |

### 1.3 O que **não** acontece

- Nenhuma tabela tem FK para `users` (`author_id`) nem para `v2_gallery_media` (`cover_image_id`, `gallery_media_id`) nem para categorias (`category_id`): a integridade é aplicativa.
- Não há `forceDelete()`: nenhuma linha de `v2_news` é apagada fisicamente pelo módulo.
- Não há tabela de revisão/histórico: só o `activity_log` do `ActivityLogger`.
- Não há `fulltext` do Laravel: a busca usa `search_vector` (trigger) e `ILIKE`.

### 1.4 Contrato observável

| Entrada | Resultado no banco |
|---|---|
| `News::create(['title' => 'X'])` | `id` UUID v4, `slug='x'`, `status='draft'`, `created_at/updated_at`; nenhuma linha em índices de busca até o `saved` terminar. |
| `$news->status = 'published'; $news->save()` | `published_at` preenchido se estava nulo; índice público atualizado e linha no feed **na primeira publicação** (republicar após despublicar falha — ver `spec.md` §3.3 item 16). |
| `$news->delete()` | `deleted_at` preenchido; remoção do índice público e do feed; linha permanece. |
| `$news->restore(); $news->status='draft'; $news->save()` | `deleted_at = null`; remoção do feed (não publicado). |
| `NoticiasImportKey::create([...])` com `key_hash` repetido | violação de `unique` (`v2_news_import_keys_key_hash_unique`). |

## 2. Abordagem

### 2.1 Estratégia

Migrations no padrão V2 (`DB::beginTransaction()` + `SET lock_timeout = '3s'` + `try/catch`
com `Log`), rollback não destrutivo (`Schema::rename` para `backup_*`, exceto o pivô de
tags, que é dropado). O esquema nasceu em quatro ondas de migration: criação (jul/08),
campos editoriais + normalização de UUID (jul/22), pivô de galeria + remoção do
aprovação (jul/26), vínculo regional (ago/06) e chaves de importação (set/17).

```mermaid
flowchart TB
  A[2026_07_08 create v2_news + tsvector + GIN] --> B[2026_07_22_000001 categories + seed]
  B --> C[2026_07_22_000002 tags + tag_pivot]
  C --> D[2026_07_22_000003 campos editoriais]
  D --> E[2026_07_22_000004 pivot -> uuid]
  E --> F[2026_07_22_000005 categories/tags/category_id -> uuid]
  F --> G[2026_07_26_000001 gallery pivot]
  G --> H[2026_07_26_000002 remove approval]
  H --> I[2026_08_06 unidade_regional_id + FK]
  I --> J[2026_09_17 import keys]
```

### 2.2 Mecânica de uma escrita (CMS store)

1. `NewsRequest` valida; `store()` instancia o model com `id` já definido.
2. `creating`: se `id` vazio gera `v2_uuid()`; se `slug` vazio usa `str_slug(title)`; se `status=published` e `published_at` vazio usa `now()`.
3. `saving`: garante unicidade do slug (loop de `SELECT` com sufixo) e, se `published` e `published_at` não sujo, mantém o valor ou usa `now()`.
4. `INSERT` em `v2_news`; trigger `BEFORE INSERT OR UPDATE OF title, subtitle, summary, slug` recalcula `search_vector`.
5. `saved`: `NewsSearchSender::sync()` (upsert nos dois índices) e `FeedSender` (upsert/remoção).
6. Controller sincroniza tags e galeria, grava auditoria, limpa cache.

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

- **UUID gerado em PHP, não no banco** `[C]`: `v2_uuid()` (v4) é chamado nos hooks; não há `DEFAULT` no esquema. Migrar dados exige gerar id na aplicação.
- **`tsvector` em trigger** `[C]`: evita depender do ORM; funciona também para escritas da API regional. Pesos A/B/B/C privilegiam título e resumo.
- **Pivô de tags sem timestamps** `[C]`: a tabela foi criada só com as duas colunas + PK; `sync()` funciona, mas não há auditoria de quando a relação nasceu.
- **Pivô de galeria com `sort_order`** `[C]`: ordena a galeria no portal (`->orderBy('sort_order')`), mas o `sync()` não escreve `timestamps` apesar de as colunas existirem.
- **Soft delete como arquivo, só em notícias** `[C]`: `News` usa `SoftDeletes` e preserva o registro para restauração, mantendo a URL reservada (o slug continua único). Categorias e tags têm a coluna `deleted_at`, mas não usam a trait: apagá-las é físico.
- **Remoção do workflow por migration** `[C]`: as tabelas de aprovação e as três colunas foram renomeadas/removidas sem migração de dados; a decisão foi "só `draft` ou `published`" (comentário da migration), mas o código não foi limpo.

### 2.4 Trechos de código pivô

```php
// V2/Modules/Publicacoes/Noticias/Migrations/2026_07_08_000001_create_v2_news_table.php:32-55
Schema::create('v2_news', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->string('title', 255);
    $table->string('slug', 255)->unique();
    $table->string('subtitle', 500)->nullable()->comment('Linha fina / resumo curto');
    $table->text('content')->nullable()->comment('AST JSON do ContentRichEditor (Tiptap)');
    $table->text('summary')->nullable()->comment('Resumo em texto puro para listagens e SEO');
    $table->string('status', 20)->default('draft')->comment('draft | published');
    $table->string('author_id', 36)->nullable()->comment('FK para users (V2 - UUID)');
    $table->timestamp('published_at')->nullable();
    $table->jsonb('metadata')->nullable()->comment('SEO data + metadados flexíveis');
    // cover_image_id, timestamps, softDeletes, índices...
});

DB::statement("ALTER TABLE v2_news ADD COLUMN search_vector tsvector;");
```

**Efeito.** `author_id` e `cover_image_id` nascem como `varchar(36)` (não `uuid`) e **sem FK**; `status` é `varchar(20)` com default `draft`. O comentário já nasce desatualizado (não prevê `pending_approval`).

```php
// V2/Modules/Publicacoes/Noticias/Migrations/2026_07_08_000001_create_v2_news_table.php:58-87
CREATE OR REPLACE FUNCTION v2_news_search_vector_update() RETURNS trigger AS $$
BEGIN
    NEW.search_vector :=
        setweight(to_tsvector('portuguese', coalesce(NEW.title, '')), 'A') ||
        setweight(to_tsvector('portuguese', coalesce(NEW.subtitle, '')), 'B') ||
        setweight(to_tsvector('portuguese', coalesce(NEW.summary, '')), 'B') ||
        setweight(to_tsvector('portuguese', coalesce(NEW.slug, '')), 'C');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_v2_news_search_vector
BEFORE INSERT OR UPDATE OF title, subtitle, summary, slug
ON v2_news FOR EACH ROW
EXECUTE PROCEDURE v2_news_search_vector_update();

CREATE INDEX IF NOT EXISTS idx_v2_news_search_vector
ON v2_news USING GIN(search_vector);
```

**Efeito.** Só as quatro colunas listadas disparam o recálculo; alterar `content` **não** atualiza o vetor (o corpo nunca entra na busca full-text). O índice GIN existe, mas nenhum código do módulo o consulta (§3.3).

```php
// V2/Modules/Publicacoes/Noticias/Migrations/2026_08_06_000001_add_unidade_regional_id_to_v2_news_table.php:9-19
Schema::table('v2_news', function (Blueprint $table) {
    $table->uuid('unidade_regional_id')->nullable()->after('category_id');
    $table->foreign('unidade_regional_id')
          ->references('id')
          ->on('v2_unidade_regionais')
          ->onDelete('set null');
    $table->index('unidade_regional_id', 'idx_v2_news_unidade_regional_id');
});
```

**Efeito.** É a **única** FK da tabela. Apagar (hard delete) uma unidade regional desvincula as notícias — o soft delete da unidade não toca nas notícias.

```php
// V2/Modules/Publicacoes/Noticias/Migrations/2026_07_26_000002_remove_approval_workflow.php:26-48
foreach (['v2_news_approvers', 'v2_news_configs'] as $table) {
    if (Schema::hasTable($table)) {
        Schema::rename($table, 'backup_' . $table . '_' . date('Y_m_d_His'));
        Log::info("[Migration] Tabela '{$table}' renomeada para backup.");
    }
}

if (Schema::hasTable('v2_news')) {
    Schema::table('v2_news', function (Blueprint $table) {
        if (Schema::hasColumn('v2_news', 'draft_token')) {
            $table->dropIndex('idx_v2_news_draft_token');
            $table->dropColumn('draft_token');
        }
        if (Schema::hasColumn('v2_news', 'reviewer_id')) { $table->dropColumn('reviewer_id'); }
        if (Schema::hasColumn('v2_news', 'rejection_reason')) { $table->dropColumn('rejection_reason'); }
    });
}
```

**Efeito.** As tabelas de aprovação são **renomeadas**, não apagadas; as colunas são dropadas com guarda `hasColumn`. O `down()` recria as colunas mas **não** desfaz a renomeação das tabelas — rollback parcial (ver §5).

```php
// V2/Modules/Publicacoes/Noticias/Migrations/2026_09_17_000001_create_v2_news_import_keys_table.php:29-36
Schema::create('v2_news_import_keys', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->char('key_hash', 64)->unique();
    $table->string('regional', 10)->index();
    $table->text('source_url');
    $table->string('news_id', 36)->nullable()->index();
    $table->timestamps();
});
```

**Efeito.** `key_hash` é a única restrição de unicidade e é o que garante idempotência sob concorrência. `news_id` não tem FK e sobrevive ao soft delete da notícia; não há cascata.

### 2.5 Consultas e custos

- `News::saving` faz 1 `SELECT EXISTS` por tentativa de slug (normalmente 1; N em colisão).
- `News::saved` faz 1 upsert em `v2_public_search_index` (só publicado), 1 `DELETE` quando não publicado, 1 upsert em `v2_cms_links_index` e 1 `updateOrCreate` em `v2_feed_rss_items` (ou `INSERT` que viola o unique se a linha estiver soft-deleted) ou soft delete.
- `NewsCategory::ordered()` = `ORDER BY sort_order, name` (sem índice composto; tabela pequena).
- `News::scopeByRegional` usa `OR` entre FK e expressão JSONB (`metadata->source_unidade_regional_id`), o que impede uso de índice para a segunda perna.

## 3. Particularidades

### 3.1 O que só existe aqui

- **`search_vector` com trigger e pesos** é exclusivo desta tabela no módulo; nenhum outro módulo de publicação replica.
- **`v2_news_import_keys`** é a única tabela de idempotência de importação do sistema.
- **Pivô de galeria com `news_id` varchar(36)** diverge do tipo `uuid` usado no restante do esquema (a migration de correção de UUID cobriu o pivô de tags, não o de galeria).

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-NOTICIAS-D01 | `v2_news.slug` é `unique` e nunca é reaproveitado, porque a linha arquivada continua ocupando o valor. | Restaurar mantém a URL; criar outra com o mesmo título gera sufixo. | `Migrations/2026_07_08_000001:35`; `Models/News.php:272-283` |
| RN-NOTICIAS-D02 | `metadata` é `jsonb` e guarda `seo` **e** rastreabilidade (`source`, `source_url`, `import_key_hash`, `tags`, `source_unidade_regional_id`, `source_markdown_hash`). | Apagar `metadata` perde SEO e o vínculo regional alternativo. | `Services/NoticiasImportService.php:393-415`; `Controllers/NoticiasCmsController.php:270-310` |
| RN-NOTICIAS-D03 | `v2_news_gallery_media` tem PK composta e `sort_order`; `sync()` substitui o conjunto inteiro. | Reenviar o formulário sem `gallery_ids` zera a galeria (comportamento do `update`). | `Controllers/NoticiasCmsController.php:489-501` |
| RN-NOTICIAS-D04 | `v2_news_categories.parent_id` não tem FK nem índice. | Categoria órfã é possível; a hierarquia é lógica. | `Migrations/2026_07_22_000001:17-26` |
| RN-NOTICIAS-D05 | `is_system` é o único guarda de integridade de categoria, verificado no controller, não no banco. | `UPDATE`/`DELETE` direto no banco ignora a proteção. | `Controllers/NoticiasCmsController.php:802-855` |
| RN-NOTICIAS-D06 | `v2_news_categories` e `v2_news_tags` têm coluna `deleted_at`, mas os models **não** usam `SoftDeletes`: `delete()` é físico. O `saving` de ambos reescreve o slug com sufixo. | Recriar uma tag com o mesmo nome gera slug `nome-1`; `tagsDestroy` apaga a linha e deixa pivôs órfãos; apagar categoria apaga a linha e deixa filhas com `parent_id` órfão. | `Models/NewsTag.php:3-8`; `Models/NewsCategory.php:3-8`; `Migrations/2026_07_22_000002:17-23`; `Controllers/NoticiasCmsController.php:721-725,831-855` |
| RN-NOTICIAS-D07 | `v2_news_import_keys` não tem soft delete e não é limpa por nenhum fluxo. | Reimportar exige apagar a chave manualmente. | `Migrations/2026_09_17_000001:29-36` |
| RN-NOTICIAS-D08 | `content` é `text` com JSON Tiptap; não há coluna JSON nem validação de esquema no banco. | Conteúdo inválido é aceito pelo banco; o renderer devolve HTML vazio/erro. | `Migrations/2026_07_08_000001:37`; `Services/NoticiasImportService.php:363-391` |

### 3.3 Casos-limite e comportamentos surpreendentes

1. **Trigger e `content`**: editar só o corpo não atualiza `search_vector`; a busca pública textual usa apenas título/subtítulo/resumo/slug.
2. **`ALTER COLUMN ... TYPE uuid USING id::uuid`** (`fix_pivot_uuid_types`, `fix_tags_and_categories_uuid`) falha se houver valor não-UUID — não há saneamento prévio. Em base recém-criada com `v2_uuid()` funciona.
3. **`down()` de `fix_tags_and_categories_uuid`** não converte `v2_news.category_id` de volta para `varchar(36)`, embora o `up()` o tenha convertido: rollback deixa o esquema misto.
4. **`dropIndex('idx_v2_news_draft_token')`** pressupõe o nome do índice criado por uma migration de aprovação que **não está no repositório**; se o índice não existir, o `up()` lança e a transação reverte. Em bases onde a coluna existe, o nome precisa conferir.
5. **Trigger sem `DROP FUNCTION` no `down()` de terceiros**: `down()` de `create_v2_news` remove o trigger e a função, mas se a tabela foi renomeada para `backup_*`, o trigger some junto; recriar exige rodar o `up()` novamente.
6. **Pivô de galeria `news_id` varchar** aceita qualquer string de até 36 caracteres; não há validação de UUID.
7. **`metadata.seo.image_url` é caminho `/storage/...`, não URL absoluta** no CMS (`Controllers/NoticiasCmsController.php:283-288`), enquanto a capa é servida por `StorageService::url()`; dois contratos de imagem convivem.
8. **Delete físico de categoria/tag**: como os models não usam `SoftDeletes`, `DELETE /cms/noticias/categorias/{id}` remove a linha (depois de zerar `category_id` das notícias) e `DELETE /cms/noticias/tags/{id}` remove a tag; o pivô `v2_news_tag_pivot` não é limpo e as subcategorias não são reparentadas — órfãos silenciosos (`Controllers/NoticiasCmsController.php:721-725,831-855`).

### 3.4 Armadilhas de reimplementação

1. **Criar FK em `category_id`** muda o comportamento: hoje excluir categoria desvincula (`null`), e a FK exigiria `SET NULL` explícito.
2. **Adicionar `unique` em `v2_news_import_keys(news_id)`** quebraria a reimportação de itens repetidos por URL distinta? Não — a chave é por hash; mas qualquer unicidade extra em `regional` quebraria múltiplos itens da mesma regional.
3. **Usar `uuid` no pivô de galeria** sem migrar `gallery_media_id` (bigint) mantém a incompatibilidade de tipos; o par é heterogêneo por design.
4. **Contar com cascata** em `news_id` (import keys, pivôs): não existe; apagar notícia fisicamente deixa pivôs órfãos.
5. **Esperar que `metadata` seja `array` sempre**: `News::$casts` garante array no model; consultas via `DB::table` recebem string JSON (o sitemap decodifica manualmente).

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

| Ponto | Afirma | Realidade |
|---|---|---|
| `2026_07_26_000002:9-16` | "Notícias agora têm apenas status draft ou published"; remove tabelas e colunas | `News` mantém `STATUS_PENDING_APPROVAL` e `$validStatuses` com três valores (`Models/News.php:87-99`); `NewsRequest` aceita `pending_approval`; a API regional grava esse status. |
| `Models/News.php:22-27` | propriedades `draft_token`, `reviewer_id`, `rejection_reason` | Colunas dropadas; restam apenas no docblock e em views mortas (`Views/cms/index.blade.php:194-205`, `Views/cms/preview.blade.php:33-98`). |
| `Views/cms/index.blade.php:204` e `preview.blade.php:34,94` | `$item->getPreviewUrl()` | Método não existe em nenhum model (confirmado por busca); a condição `draft_token` é sempre falsa, então o `@if` curto-circuita na listagem; em `preview.blade.php` as chamadas ficam em views sem rota. |
| `Views/cms/index.blade.php:194` | status `rejected` com `rejection_reason` | Status não é válido no model; a coluna não existe. |
| `Migrations/2026_07_08_000001:39-40` | status `draft | published` | Três status válidos no PHP. |
| `down()` de `remove_approval_workflow` | "Reverte a remoção" | Só recria colunas; as tabelas ficam como `backup_*`. |

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Tipo | Assinatura/atributos | Consumidores |
|---|---|---|---|
| `News` | model | `$table='v2_news'`, `$incrementing=false`, `$keyType='string'`, `SoftDeletes` | CMS, importação, API regional, portal, widgets, feed, busca |
| `News::$fillable` | atributo | 18 campos (`id`…`unidade_regional_id`) | idem |
| `News::$casts` | atributo | `metadata:array`, `published_at:datetime`, `content_date:date`, flags boolean | idem |
| `NewsCategory` | model | `$table='v2_news_categories'`, **sem** `SoftDeletes` (coluna `deleted_at` não usada) | CMS, portal, identidade regional |
| `NewsTag` | model | `$table='v2_news_tags'`, **sem** `SoftDeletes` (coluna `deleted_at` não usada) | CMS, importação |
| `NewsImportKey` | model | `$table='v2_news_import_keys'`, sem soft delete | importação, testes |
| Relações | métodos | `author`, `coverImage`, `category`, `unidadeRegional`, `tags`, `gallery` | views/controllers |

### 4.2 Contrato de UI/JS (quando aplicável)

Não se aplica a este anexo: os contratos de tela estão em [`cms.md`](cms.md) e [`portal.md`](portal.md). O único contrato de dados consumido por JS é o conjunto `data-*` da listagem pública descrito no [`spec.md`](spec.md) §4.2.

### 4.3 Ordem e ciclo de vida

1. Migrations do módulo rodam depois das do Core (o provider as carrega no boot).
2. `create_v2_news` exige `v2_uuid()` disponível (helper do Core).
3. `add_unidade_regional_id` exige `v2_unidade_regionais` já criada (módulo Conexões).
4. `create_v2_news_gallery_pivot` exige `v2_gallery_media` (serviço Gallery) apenas logicamente — não há FK.
5. `create_v2_news_import_keys` é independente.

### 4.4 Receitas de uso

**Consultar publicadas com a capa e a regional:**

```php
$news = \V2\Modules\Publicacoes\Noticias\Models\News::published()
    ->where('published_at', '<=', now())
    ->with(['coverImage', 'category.parent', 'unidadeRegional'])
    ->orderBy('published_at', 'desc')
    ->get();
```

**Recriar a categoria regional de uma unidade (idempotente):**

```php
$category = app(\V2\Core\Services\RegionalIdentity\RegionalIdentityService::class)
    ->newsCategoryFor($unidade);   // cria ou corrige regional-<sigla>
```

**Verificar idempotência antes de importar:**

```php
use V2\Modules\Publicacoes\Noticias\Models\NewsImportKey;
$hash = hash('sha256', 'regional:pe:https://www.sucesupe.org.br/post/x');
$jaImportado = NewsImportKey::where('key_hash', $hash)->exists();
```

### 4.5 Anti-uso

1. **Apagar fisicamente uma notícia** (`forceDelete`): deixa pivôs e chaves órfãos e não remove a entrada do feed (o evento `deleted` só roda no delete normal; `forceDeleted` não está tratado).
2. **Gravar `category_id` apontando para categoria de outra regional** sem passar pelo `RegionalIdentityService`: o escopo regional continua funcionando pela FK `unidade_regional_id`, mas o filtro por categoria do portal mostra a notícia na categoria errada.
3. **Assumir `timestamps` no pivô de tags**: `sync()` não os escreve; consultas que ordenem por `created_at` do pivô recebem `NULL`.

### 4.6 Custos e limites

- `search_vector` duplica o texto de título/subtítulo/resumo/slug no índice GIN; atualizado a cada escrita dessas colunas.
- `scopeByRegional` com `orWhere` JSONB não usa índice; para grandes volumes, considerar índice de expressão.
- As tabelas de pivô não têm FK: `sync()` é a única barreira contra órfãos.

## 5. Dados e modelo de domínio — fluxos e estados

```mermaid
stateDiagram-v2
  [*] --> draft: INSERT (default)
  draft --> published: UPDATE status
  pending_approval --> published: UPDATE manual
  published --> draft: UPDATE status
  draft --> archived: UPDATE deleted_at
  published --> archived: UPDATE deleted_at
  pending_approval --> archived: UPDATE deleted_at
  archived --> draft: UPDATE deleted_at=NULL + status=draft
```

O que a remoção do workflow deixou no banco e no código:

| Artefato | Estado atual | Fonte |
|---|---|---|
| `v2_news_approvers`, `v2_news_configs` | renomeadas para `backup_*_<timestamp>`; sem leitores | `Migrations/2026_07_26_000002:26-32` |
| `v2_news.draft_token` | removida (era UUID, com índice `idx_v2_news_draft_token`) | `:34-40` |
| `v2_news.reviewer_id` | removida | `:41-43` |
| `v2_news.rejection_reason` | removida | `:44-46` |
| `News::STATUS_PENDING_APPROVAL` + labels/cores/ícones | **permanecem** | `Models/News.php:88,96-135` |
| `NewsRequest` aceitando `pending_approval` | permanece; mensagem cita `rejected` inexistente | `Requests/NewsRequest.php:36,70` |
| API regional criando em `pending_approval` | permanece | `V2/Modules/Conexoes/UnidadeRegional/Controllers/UnidadeRegionalNewsApiController.php:214` |
| Views com `rejected`, `rejection_reason`, `draft_token`, `getPreviewUrl()` | permanecem (código morto) | `Views/cms/index.blade.php:194-205`; `Views/cms/preview.blade.php:33-98` |
| `NoticiasPortalController::preview` chamando `isAccessible()` | permanece; método inexistente → 500 | `Controllers/NoticiasPortalController.php:150-166` |

## 6. Rotas e contratos HTTP

Não se aplica a este anexo (ver [`rotas.md`](rotas.md)). A única relação rota↔dado discutida aqui é que `DELETE /cms/noticias/{id}` grava `deleted_at` e `POST .../restaurar` o limpa.

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

| Chave | Efeito sobre os dados | Fonte |
|---|---|---|
| `v2.regional_operator.forced_news_category_slug` | escolhe a categoria de fallback (default `regionais`) quando a regional não tem `regional-<sigla>` | `config/v2.php:54`; `Controllers/NoticiasCmsController.php:946-952` |
| `filesystems.media_disk` | disco onde os arquivos de `v2_gallery_media` são gravados na importação | `config/filesystems.php:24`; `V2/Services/Gallery/Services/GalleryService.php:55` |
| `cache.prefix` | prefixo das chaves apagadas após escrita | `config/cache.php:89` |

Não há seed de notícias; as categorias `nacional` e `regionais` são dados de migration (§9 do [`spec.md`](spec.md)).

## 8. Observabilidade

- Escritas de importação e falhas: `Log::info('[NoticiasImport] Execução concluída', [...])` / `Log::error('[NoticiasImport] Falha ao importar item', [...])`.
- Falhas do trigger/índice não são logadas pelo banco; falhas de busca/feed são engolidas com `[News]`.
- Auditoria de linha (`activity_log`): `created`, `updated`, `archived`, `restored`, `imported`, `category_created/updated/deleted`.
- Não há auditoria de alteração de `metadata`/tags: o `updated` registra só status antigo/novo.

## 9. Seeds e dados iniciais

| Seeder/migration | Tabela | Registros | Idempotência |
|---|---|---|---|
| `2026_07_22_000001_create_v2_news_categories_table::seedDefaultCategories` | `v2_news_categories` | `Nacional` e `Regionais`, `is_system=true` | **não** idempotente: `insert` direto; rodar em base com as linhas viola `slug unique` |
| `V2/Core/Migrations/2026_09_14_000002_seed_v2_regional_role` | `v2_roles` | perfil `regional` com `Noticias`/`Associados` = `write` | sim (busca por slug e atualiza) |
| `RegionalIdentityService::syncAll()` (runtime) | `v2_news_categories` | uma `regional-<sigla>` por unidade ativa | sim (`firstOrCreate`/update condicional) |

## 10. UI CMS

Não se aplica: telas em [`cms.md`](cms.md). Impacto de dados nas telas: paginação 15, contadores por status (não escopados na tela de arquivadas) e exclusão de categoria desvinculando notícias.

## 11. UI Portal

Não se aplica: telas em [`portal.md`](portal.md). Impacto de dados: listagem sem paginação; sitemap lendo `DB::table` (inclui arquivadas).

## 12. Lacunas

- `LG-NOTICIAS-D01` — Esquema original do workflow (nomes/colunas de `v2_news_approvers`/`v2_news_configs`, e o índice `idx_v2_news_draft_token`) não está no repositório: a migration que o criou foi removida. Impacto: o `down()`/`dropIndex` não pode ser verificado.
- `LG-NOTICIAS-D02` — Não há evidência de migração de dados de `draft_token` para outro campo; notícias em `pending_approval` podem existir em produção sem transição.
- `LG-NOTICIAS-D03` — Volume real de `v2_news` e cardinalidade de `metadata.tags` não estão no repositório; os custos de `scopeByRegional` são estimados.

## 13. Checklist de paridade

- [ ] `PC-NOTICIAS-D01` — `v2_news.slug` deve ter `unique` e `v2_news_search_vector` deve ser GIN. Fonte: `Migrations/2026_07_08_000001:35,84-87`.
- [ ] `PC-NOTICIAS-D02` — Trigger deve atualizar `search_vector` apenas em `title/subtitle/summary/slug`. Fonte: `:75-81`.
- [ ] `PC-NOTICIAS-D03` — `unidade_regional_id` deve ter FK com `ON DELETE SET NULL`. Fonte: `2026_08_06_000001:12-17`.
- [ ] `PC-NOTICIAS-D04` — Excluir categoria deve deixar `category_id = NULL` nas notícias. Fonte: `Controllers/NoticiasCmsController.php:846-849`.
- [ ] `PC-NOTICIAS-D05` — `v2_news_import_keys.key_hash` deve ser `char(64) unique`. Fonte: `2026_09_17_000001:31`.
- [ ] `PC-NOTICIAS-D06` — `restore()` deve limpar `deleted_at` e gravar `status=draft`. Fonte: `Controllers/NoticiasCmsController.php:164-180`.
- [ ] `PC-NOTICIAS-D07` — Migrations de UUID devem ser capazes de converter os pivôs e categorias existentes. Fonte: `2026_07_22_000004:17-21`; `2026_07_22_000005:17-31`.
- [ ] `PC-NOTICIAS-D08` — Categorias de sistema não devem poder ser editadas nem removidas. Fonte: `Controllers/NoticiasCmsController.php:806-810,838-842`.
- [ ] `PC-NOTICIAS-D09` — Após `remove_approval_workflow`, nenhuma das três colunas deve existir. Fonte: `2026_07_26_000002:35-48`.

## 14. Evidências consultadas

- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_08_000001_create_v2_news_table.php:25-141`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000001_create_v2_news_categories_table.php:11-83`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000002_create_v2_news_tags_table.php:11-60`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000003_add_new_fields_to_v2_news_table.php:11-67`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000004_fix_pivot_uuid_types.php:11-48`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_22_000005_fix_tags_and_categories_uuid.php:11-62`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_26_000001_create_v2_news_gallery_pivot_table.php:16-62`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_07_26_000002_remove_approval_workflow.php:20-87`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_08_06_000001_add_unidade_regional_id_to_v2_news_table.php:9-28`.
- `V2/Modules/Publicacoes/Noticias/Migrations/2026_09_17_000001_create_v2_news_import_keys_table.php:23-67`.
- `V2/Modules/Publicacoes/Noticias/Models/News.php`, `Models/NewsCategory.php`, `Models/NewsTag.php`, `Models/NewsImportKey.php`.
- `V2/Modules/Publicacoes/Noticias/Controllers/NoticiasCmsController.php:236-350,400-545,588-683,703-855`.
- `V2/Modules/Publicacoes/Noticias/Services/NoticiasImportService.php:205-208,263-301,363-463`.
- `V2/Core/Services/RegionalIdentity/RegionalIdentityService.php:14-122`.
- `V2/Services/Gallery/Services/GalleryService.php:53-128`.
- `V2/Services/Search/Services/PublicIndexerService.php:21-72` e `V2/Services/Search/Services/CMSIndexerService.php:21-45`.
