---
id: SVC-FEEDRSS
tipo: servico
onda: 2
status: concluido
unidades: [SVC-FEEDRSS]
fontes:
  - V2/Services/FeedRSS/
depende_de: [SVC-ACTIVITYLOGGER, MOD-SUPPORT-SUPPORTCONFIGS]
consumidores: [MOD-PUBLICACOES-NOTICIAS, MOD-PUBLICACOES-AGENDA, MOD-PUBLICACOES-TRABALHECONOSCO, MOD-INFRAESTRUTURA-FEEDRSS]
criticidade: media
dono: SvcMailOps
---

# Serviço FeedRSS

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Mantém uma tabela-cache plana (`v2_feed_rss_items`) com um item por entidade publicada por qualquer módulo e a serializa em um único feed RSS 2.0 em `GET /feed.xml`. |
| Entrada | `FeedSender::forModule($m)->push(FeedItemDTO)` / `pushFromArray(array)` / `remove($entityType, $entityId)`; `FeedRssService::addItem/removeItem/toggleItem/deleteItem`; `GET /feed.xml`; rotas CMS do módulo de suporte (toggle/delete). |
| Saída | XML RSS 2.0 (`application/rss+xml; charset=utf-8`), linhas em `v2_feed_rss_items`, linhas em `activity_log`, página `cms/v2/feed-rss` (módulo). |
| Efeitos colaterais | Escrita/soft delete na tabela de itens; auditoria de cada escrita via `ActivityLoggerService`; cache de página de 5 min (`page:*`) e `Cache-Control` público de 60 s no `/feed.xml`. |
| Particularidade central | UPSERT por `entity_type + entity_id` sobre uma tabela **com soft delete e unique constraint sem filtro**: um item removido não pode voltar a ser inserido (violação de unique), o que quebra republicação. |
| Como se integra | `V2\Services\FeedRSS\Consumer\FeedSender`, namespace de views `v2-service-feedrss`, rota nomeada `v2.feedrss.feed`, singleton em `config/app.php`. |
| Se removido, quebra | `/feed.xml` (404), painel de monitoramento do módulo Feed RSS, republicação automática de notícias/eventos/vagas no feed, auditoria `feed_rss_*`. |
| Superfície pública | `FeedSender` (`forModule`, `push`, `pushFromArray`, `remove`), `FeedItemDTO` (`fromArray`, `toArray`, 11 getters), `FeedRssService` (`addItem`, `removeItem`, `getFeedItems`, `generateXml`, `getStats`, `toggleItem`, `deleteItem`, `getAllItemsPaginated`, `DEFAULT_LIMIT`), `FeedRssItem` (scopes `enabled/recent/byModule/byEntityType/byEntity`), `FeedRssController@feed`, 1 rota. |

## 1. Efeito

### 1.1 Transformação que o serviço realiza

**Publicação.** `FeedSender::forModule('noticias')->pushFromArray([...])` normaliza o array (injeta `module`, default `published_at = now()` e converte para `Carbon`), cria um `FeedItemDTO`, e `FeedRssService::addItem()` faz `FeedRssItem::updateOrCreate(['entity_type','entity_id'], [...])` — cria ou sobrescreve título, link, descrição, conteúdo, autor, categorias, imagem, `enabled=true` e `published_at` (`V2/Services/FeedRSS/Consumer/FeedSender.php:73-87`; `V2/Services/FeedRSS/Services/FeedRssService.php:39-70`). Em seguida grava `activity_log` com descrição `feed_rss_item_upserted` (`FeedRssService.php:61-67`).

**Remoção.** `remove('news', $id)` faz `FeedRssItem::where(entity_type, entity_id)->delete()`, que no Laravel 5.5 com `SoftDeletes` executa `UPDATE ... SET deleted_at = now()` — não apaga a linha (`FeedRssService.php:79-93`; `vendor/laravel/framework/src/Illuminate/Database/Eloquent/SoftDeletingScope.php:38-44`).

**Leitura pública.** `GET /feed.xml` → `FeedRssController@feed` lê `config('v2.feed_rss.limit')` (default 50), chama `generateXml($limit)`, que consulta `enabled()->recent()->take($limit)`, resolve o nome do site em `PortalConfigsService` (fallback `config('app.name')`) e renderiza `v2-service-feedrss::rss` com `Content-Type: application/rss+xml; charset=utf-8` (`V2/Services/FeedRSS/Controllers/FeedRssController.php:35-43`; `FeedRssService.php:115-143`; `V2/Services/FeedRSS/Routes/web.php:5-8`).

### 1.2 Efeitos sobre dados e sistema

| Efeito | Recurso afetado | Ponto no código | Fonte |
|---|---|---|---|
| Insere/atualiza item do feed com `enabled=true` | `v2_feed_rss_items` | `updateOrCreate` | `V2/Services/FeedRSS/Services/FeedRssService.php:42-59` |
| Marca `deleted_at` (soft delete) ao despublicar | `v2_feed_rss_items` | `->delete()` no builder | `FeedRssService.php:81-83` |
| Audita upsert/remoção/toggle/delete | `activity_log` | `ActivityLoggerService::log()` | `FeedRssService.php:61-67,85-90,191-196,218-222` |
| Serve XML com itens habilitados | resposta HTTP `/feed.xml` | `generateXml` + view | `FeedRssService.php:115-143`; `V2/Services/FeedRSS/Views/rss.blade.php:1-58` |
| Cacheia a resposta por 5 min em Redis e envia `Cache-Control: public, max-age=60` | middleware global de rotas web | `page.cache`, `public.cache` | `V2/Core/Providers/V2ServiceProvider.php:216-220`; `V2/Core/Middleware/PageCacheMiddleware.php:11-14`; `PublicCacheMiddleware.php:88-93` |
| Consulta `PortalConfigsService::getAll()` para `titulo_site` | tabela de configs do portal | `generateXml` | `FeedRssService.php:121-128` |
| Lê estatísticas agregadas no painel | `v2_feed_rss_items` | `getStats()` (4-5 queries) | `FeedRssService.php:150-170` |

### 1.3 Contrato observável

| Entrada | Resultado |
|---|---|
| `push` de entidade nova | 1 `INSERT`; aparece em `/feed.xml`; auditoria `feed_rss_item_upserted` com `type='feed_rss_service'`, `module`, `entity='<entity_type>:<entity_id>'` |
| `push` de entidade existente (não deletada) | 1 `UPDATE`; `enabled` volta a `true` mesmo se o suporte o havia desabilitado |
| `push` de entidade cujo item foi removido antes | `QueryException` de unique `uq_v2_feed_rss_items_entity` (o `updateOrCreate` não enxerga a linha com `deleted_at`) |
| `remove` de item existente | `UPDATE deleted_at`; retorna `true` (int > 0); auditoria `feed_rss_item_removed` |
| `remove` de item inexistente | nenhum efeito; `false`; sem auditoria |
| `GET /feed.xml` com itens | `200`, XML válido, até `limit` itens em `published_at DESC` |
| `GET /feed.xml` sem itens | `200` com **um** `<item>` placeholder: título `Nenhum conteúdo disponível no momento` (`rss.blade.php:50-56`) |
| `GET /feed.xml` segunda vez em < 5 min | `200` com `X-Cache-Status: HIT` e o mesmo corpo (inclusive itens removidos nesse intervalo) |
| `toggleItem($id, false)` | `UPDATE enabled`; item some do feed; auditoria `feed_rss_item_disabled` |
| `toggleItem($id inexistente)` | `null`, sem auditoria |
| `deleteItem($id)` | soft delete; `true`; auditoria `feed_rss_item_deleted` |
| Painel `cms/v2/feed-rss` sem ser suporte/super-admin | redirect `v2.cms.dashboard` + flash `Acesso restrito ao suporte.` |

### 1.4 O que o serviço deliberadamente NÃO faz

- **Não gera feed por módulo**: `generateXml()` ignora `module`; é um único feed consolidado (`FeedRssService.php:101-107`). Não há query string (`?module=`, `?limit=`) suportada.
- **Não consulta os models de origem**: o feed lê exclusivamente `v2_feed_rss_items`; se o módulo não publicar/remover, o feed não reflete o estado real (`README.md:8`).
- **Não invalida cache**: nenhum `Cache::forget`/limpeza de `page:*` em `addItem/removeItem/toggleItem/deleteItem`. A atualização do feed depende de outro controller limpar o cache de páginas (Notícias/Agenda fazem; Trabalhe Conosco e o painel de suporte não).
- **Não pagina, filtra nem autentica** `/feed.xml`; não há ETag/Last-Modified condicional.
- **Não remove fisicamente** linhas: `removeItem`/`deleteItem` são soft delete.
- **Não valida** `link`, `image_url` ou tamanho de `description`; não sanitiza HTML de `content`.
- **Não publica sozinho**: nenhum model chama `FeedSender` por observer global; os hooks são declarados em cada model (News, Evento, JobOpening).

### 1.5 Efeito por perfil

| Perfil | Efeito |
|---|---|
| Público/leitor RSS | recebe até 50 itens de todos os módulos, com cache de 5 min; não vê itens removidos/desabilitados após o cache expirar. |
| CMS (editor) | ao salvar/publicar, dispara `push` pelo hook do model; ao despublicar, `remove`. |
| Suporte (`config('v2.support.email')`/super-admin) | acessa o painel, vê estatísticas, habilita/desabilita e remove itens. |
| Módulo de busca (`SVC-SEARCH`) | não é acionado pelo FeedRSS; cada model sincroniza busca separadamente. |

## 2. Abordagem

### 2.1 Estratégia e por quê

Tabela-cache desnormalizada + DTO + adaptador de consumo. Os módulos não conhecem a tabela: publicam via `FeedSender`, que monta um `FeedItemDTO` e delega ao singleton `FeedRssService`. `[I]` A desnormalização evita joins entre módulos no momento da requisição do feed e permite ao suporte desabilitar um item sem tocar no conteúdo original (por isso `enabled` e o painel). O custo é a sincronização imperativa: cada model precisa lembrar de publicar e remover.

### 2.2 Mecânica passo a passo

```mermaid
flowchart TD
  A["Model saved (News/Evento/JobOpening)"] --> B{"publicado/ativo?"}
  B -->|"sim"| C["FeedSender::forModule(m)->pushFromArray(dados)"]
  B -->|"não"| D["FeedSender::forModule(m)->remove(entity_type, id)"]
  C --> E["module = m; published_at default now(); new Carbon"]
  E --> F["FeedItemDTO::fromArray"]
  F --> G["FeedRssService::addItem"]
  G --> H["FeedRssItem::updateOrCreate(entity_type+entity_id)"]
  H --> I["ActivityLoggerService::log(item, 'feed_rss_item_upserted')"]
  D --> J["FeedRssService::removeItem -> UPDATE deleted_at"]
  J --> K["log(..., 'feed_rss_item_removed')"]
  L["GET /feed.xml"] --> M["PageCache (page:sha1, 5 min)"]
  M -->|HIT| N["XML cacheado"]
  M -->|MISS| O["FeedRssController@feed -> limit config"]
  O --> P["getFeedItems: enabled + recent + take"]
  P --> Q["PortalConfigsService.titulo_site (try/catch)"]
  Q --> R["view v2-service-feedrss::rss -> XML"]
  R --> S["PublicCache: Cache-Control 60s"]
```

1. Hook `saved` do model decide publicar ou remover (`V2/Modules/Publicacoes/Noticias/Models/News.php:292-318`; `Evento.php:104-140`; `JobOpening.php:130-163`), sempre dentro de `try/catch` que só loga.
2. `FeedSender::__construct` guarda o módulo e resolve `FeedRssService` via `app()` (`FeedSender.php:26-30`).
3. `pushFromArray` injeta `module`, `published_at` e delega (`FeedSender.php:73-87`).
4. `addItem` faz o UPSERT e audita (`FeedRssService.php:39-70`).
5. Requisição pública: middleware `page.cache` pode responder do Redis antes do controller (`PageCacheMiddleware.php:31-40`).
6. `generateXml` consulta itens, resolve nome do site, calcula `pubDate` (primeiro item) e `buildDate` (agora), e renderiza a view (`FeedRssService.php:115-143`).
7. `FeedRssController` devolve `200` com `Content-Type` explícito; `public.cache` adiciona `Cache-Control` (`FeedRssController.php:40-42`).

### 2.3 Trechos de código pivô

```php
// V2/Services/FeedRSS/Services/FeedRssService.php:39-59 — a chave do UPSERT é
// (entity_type, entity_id); `module` é apenas um atributo, não parte da identidade.
$item = FeedRssItem::updateOrCreate(
    [
        'entity_type' => $dto->getEntityType(),
        'entity_id'   => $dto->getEntityId(),
    ],
    [
        'module'       => $dto->getModule(),
        'title'        => $dto->getTitle(),
        'link'         => $dto->getLink(),
        'description'  => $dto->getDescription(),
        'content'      => $dto->getContent(),
        'author'       => $dto->getAuthor(),
        'categories'   => $dto->getCategories(),
        'image_url'    => $dto->getImageUrl(),
        'enabled'      => true,
        'published_at' => $dto->getPublishedAt(),
    ]
);
```

O que mostra: o `enabled => true` no bloco de atualização reabilita automaticamente um item que o suporte havia desabilitado. Dois módulos que usem o mesmo par (`entity_type`, `entity_id`) colidem — não há namespace por módulo na chave.

```php
// V2/Services/FeedRSS/Services/FeedRssService.php:79-92 — `removeItem` usa delete() em
// query builder; com SoftDeletes isso vira UPDATE deleted_at (não remove a linha).
$deleted = FeedRssItem::where('entity_type', $entityType)
    ->where('entity_id', $entityId)
    ->delete();

if ($deleted && $this->activityLogger) {
    $this->activityLogger->log(new FeedRssItem(), 'feed_rss_item_removed', [
        'type'   => 'feed_rss_service',
        'entity' => $entityType . ':' . $entityId,
    ]);
}

return $deleted;
```

O que mostra: a auditoria usa um `FeedRssItem` **vazio** como sujeito → `subject_id` nulo na `activity_log` (a timeline daquele item não mostra a remoção). E, combinado com `updateOrCreate`, produz a armadilha de republicação (§3.4 item 1).

```php
// V2/Services/FeedRSS/Services/FeedRssService.php:101-117 — leitura e limite; não há
// filtro por módulo nem cache próprio.
public function getFeedItems($limit = self::DEFAULT_LIMIT)
{
    return FeedRssItem::enabled()
        ->recent()
        ->take($limit)
        ->get();
}

public function generateXml($limit = self::DEFAULT_LIMIT)
{
    $items = $this->getFeedItems($limit);
    $supportEmail = config('v2.support.email');
    $siteName = config('app.name');
```

O que mostra: `recent()` = `orderBy('published_at','desc')` (`FeedRssItem.php:109-112`); `enabled()` = `where('enabled', true)` (`:98-101`). O limite vem do controller por `config('v2.feed_rss.limit')` e não é higienizado além de `(int)`.

```php
// V2/Services/FeedRSS/Views/rss.blade.php:26-49 — formato efetivo de cada <item>.
@forelse($items as $item)
<item>
    <title>{{ $item->title }}</title>
    <link>{{ $item->link }}</link>
    <guid isPermaLink="false">urn:uuid:{{ $item->id }}</guid>
    <description>{{ $item->description }}</description>
    <pubDate>{{ $item->published_at->toRssString() }}</pubDate>
    <source url="{{ $feedUrl }}">{{ $siteName }}</source>
    <dc:creator>{{ $item->author ?: $siteName }}</dc:creator>
@if(!empty($item->categories))
@foreach($item->categories as $category)
            <category>{{ $category }}</category>
@endforeach
@endif
@if(!empty($item->image_url))
            <media:content url="{{ $item->image_url }}" medium="image" type="image/jpeg">
                <media:description type="plain">{{ $item->title }}</media:description>
            </media:content>
            <enclosure url="{{ $item->image_url }}" type="image/jpeg" length="0"/>
@endif
@if(!empty($item->content))
            <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[{!! $item->content !!}]]></content:encoded>
@endif
</item>
```

O que mostra: `title`, `link`, `description`, `author`, `categories` são escapados por `{{ }}`; `content` é injetado cru dentro de CDATA (um `]]>` no conteúdo quebra o XML). `media:content` e `enclosure` declaram `image/jpeg` e `length="0"` independentemente do arquivo real.

```php
// V2/Modules/Publicacoes/Noticias/Models/News.php:301-317 — padrão de publicação:
// publica só se status publicado; caso contrário remove; tudo dentro de try/catch.
FeedSender::forModule('noticias')->pushFromArray([
    'entity_type'  => 'news',
    'entity_id'    => $model->id,
    'title'        => $model->title,
    'link'         => $model->getPublicUrl() ?: url('/noticia/' . $model->slug),
    'description'  => $model->summary ?: ($model->subtitle ?: ''),
    'published_at' => $model->published_at ?: $model->created_at,
    'content'      => $model->summary ?: null,
    'author'       => $model->author ? $model->author->name : null,
    'categories'   => ['notícias'],
    'image_url'    => $model->getCoverImageUrl(),
]);
} else {
    FeedSender::forModule('noticias')->remove('news', $model->id);
}
```

O que mostra: o contrato de campos usado por Notícias. Agenda acrescenta `target_audience` às categorias e usa `strip_tags` em `description`/`content` (`Evento.php:121-131`); Trabalhe Conosco usa `entity_type='job-opening'` e categorias fixas `['vagas','emprego','trabalhe-conosco']` (`JobOpening.php:143-158`).

```php
// V2/Services/FeedRSS/Controllers/FeedRssController.php:35-43 — o limite é lido da
// config e o XML sai com Content-Type próprio; `$this->seo` é injetado e nunca usado.
public function feed()
{
    $limit = (int) config('v2.feed_rss.limit', FeedRssService::DEFAULT_LIMIT);
    $xml = $this->feedRssService->generateXml($limit);

    return response($xml, 200, [
        'Content-Type' => 'application/rss+xml; charset=utf-8',
    ]);
}
```

### 2.4 Algoritmos e custos

- `addItem`: 1 `SELECT` + 1 `INSERT` ou 1 `UPDATE` + 1 `INSERT` em `activity_log`. O `UPDATE` de um item existente grava todos os campos, mesmo inalterados.
- `removeItem`: 1 `UPDATE` em massa (pode afetar mais de uma linha se houver duplicatas históricas — impossível pela unique) + 1 `INSERT` de auditoria.
- `/feed.xml`: 1 consulta de itens (`WHERE enabled = true ORDER BY published_at DESC LIMIT n`; índices em `enabled` e `published_at`), 1-2 consultas de `PortalConfigsService::getAll()`, render Blade. Custo independente de a rota ser HIT no `page.cache`.
- `getStats`: `COUNT(*)` total, `COUNT` enabled, `COUNT` disabled, `GROUP BY module`, 1 `first()` — 5 queries por carregamento do painel.
- Sem N+1: a view usa apenas atributos do item.

### 2.5 Estado e ciclo de vida

| Estado do item | Como entra | Visível no feed? | Como sai |
|---|---|---|---|
| Ativo | `addItem` (insert/update) | sim, se `enabled` | `remove` ou `toggleItem(false)` |
| Desabilitado | `toggleItem($id,false)` | não | `toggleItem(true)` ou novo `addItem` (força `enabled=true`) |
| Desabilitado + republicado | `addItem` | sim (reabilita) | — |
| Removido (soft delete) | `removeItem`/`deleteItem` | não | **não há caminho** de restauração no serviço (nem `restore` exposto) |
| Removido + `addItem` do mesmo par | — | — | falha com unique violation |

`published_at` é o único critério de ordenação; `created_at`/`updated_at` do item refletem a última sincronização, não a publicação.

## 3. Particularidades

### 3.1 O que é exclusivo deste serviço

- É a única fonte do `/feed.xml` e a única tabela que consolida entidades de módulos diferentes com um identificador genérico (`entity_type` + `entity_id`).
- É o único ponto do V2 que expõe um feed RSS 2.0 com namespaces `atom`, `media`, `dc` e `content` (`rss.blade.php:2-5`).
- É o único serviço cujo contrato de escrita é disparado por hooks `saved`/`deleted` de models de módulos (News, Evento, JobOpening), não por controllers.

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

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| `RN-FEEDRSS-01` | A identidade é `(entity_type, entity_id)`; `module` não participa | colisão entre módulos que reusem o mesmo `entity_type`; troca de módulo no DTO sobrescreve o item existente | `FeedRssService.php:42-46`; migration `:44` |
| `RN-FEEDRSS-02` | `addItem` sempre grava `enabled = true` | desabilitar pelo suporte não é permanente: o próximo save da entidade reabilita | `FeedRssService.php:56` |
| `RN-FEEDRSS-03` | Remoção é soft delete; a unique ignora `deleted_at` | republicar a mesma entidade depois de remover falha com unique violation | `FeedRssService.php:81-83`; migration `:42-44`; `SoftDeletingScope.php:38-44` |
| `RN-FEEDRSS-04` | Feed único, sem filtro por módulo nem query string | não existe `/noticias/feed.xml`; `module` só serve ao painel/estatísticas | `FeedRssService.php:101-107`; `Routes/web.php:5-8` |
| `RN-FEEDRSS-05` | Limite = `config('v2.feed_rss.limit')`, default 50 | mudar `V2_FEED_RSS_LIMIT` muda o tamanho sem deploy | `FeedRssController.php:37`; `config/v2.php:13-15` |
| `RN-FEEDRSS-06` | Ordenação por `published_at DESC`, sem desempate | itens com mesmo instante têm ordem indefinida | `FeedRssService.php:103-106`; `FeedRssItem.php:109-112` |
| `RN-FEEDRSS-07` | `content` vai cru em CDATA; demais campos escapados | HTML é renderizado pelo leitor; `]]>` quebra o documento | `rss.blade.php:46-48`; `rss.blade.php:28-34` |
| `RN-FEEDRSS-08` | Nenhuma invalidação de cache de página ao escrever no feed | `/feed.xml` pode ficar até 5 min desatualizado | `FeedRssService.php:39-93,179-225`; `PageCacheMiddleware.php:11-14` |
| `RN-FEEDRSS-09` | `FeedSender` propaga exceções (diferente de `MailService`) | os hooks dos models precisam de `try/catch`; se não houver, o save do conteúdo lança após persistir | `FeedSender.php:49-64`; `News.php:300-319` |
| `RN-FEEDRSS-10` | Toda escrita auditada quando `ActivityLoggerService` está injetado | singleton do provider sempre injeta; instanciar `new FeedRssService()` perde a auditoria | `FeedRssService.php:28-31`; `Providers/FeedRssServiceProvider.php:28-32` |

### 3.3 Casos-limite

| Caso | Comportamento | Fonte |
|---|---|---|
| Tabela vazia | 1 `<item>` placeholder sem `guid`/`link` de conteúdo | `rss.blade.php:50-56` |
| `published_at` nulo em linha manipulada | `->toRssString()` em `null` → erro na view; `pubDate` do canal também falha | `rss.blade.php:32`; `FeedRssService.php:138-140` `[I]` |
| `content` ausente | `<content:encoded>` omitido | `rss.blade.php:46-48` |
| `image_url` ausente | `media:content` e `enclosure` omitidos | `rss.blade.php:40-45` |
| `categories` vazio/nulo | nenhum `<category>` | `rss.blade.php:35-39` |
| `author` nulo | `dc:creator` recebe `siteName` | `rss.blade.php:34` |
| `limit = 0` | nenhum item → placeholder | `FeedRssService.php:105` |
| Item desabilitado pelo suporte | some do feed imediatamente na próxima renderização (após cache) | `FeedRssItem.php:98-101` |
| Remoção seguida de republicação | unique violation, capturada pelo `try/catch` do model e logada como falha de RSS | §3.2 `RN-FEEDRSS-03` |
| `PortalConfigsService` indisponível | `catch` silencioso; usa `config('app.name')` | `FeedRssService.php:121-128` |
| `supportEmail` nulo | `<webMaster> (Site)</webMaster>` com parênteses vazios | `rss.blade.php:16-17` |
| XML com `&` ou `<` no título | escapado por `{{ }}` | `rss.blade.php:28` |
| Cache Redis fora do ar | `Log::warning` e renderização normal, sem `X-Cache-Status` | `PageCacheMiddleware.php:62-65` |
| Concorrência no `updateOrCreate` | duas requisições simultâneas para a mesma entidade podem colidir na unique; a segunda falha `[I]` | `FeedRssService.php:42` |

### 3.4 Armadilhas de reimplementação

1. **Soft delete + unique sem filtro é o bug central.** No Laravel 5.5, `updateOrCreate` aplica o global scope `whereNull(deleted_at)`; como a unique `uq_v2_feed_rss_items_entity` cobre `(entity_type, entity_id)` sem condição, o `INSERT` falha depois de qualquer `remove`. Para paridade de comportamento, a reimplementação precisa reproduzir a falha **ou** o time decide corrigir (delete físico ou unique parcial). Ver `LG-FEEDRSS-01`.
2. **`entity_type` é contrato público, não enum**: `news`, `evento`, `job-opening`. Renomear quebra o UPSERT e o `remove` dos hooks existentes.
3. **A ordem do hook importa**: `static::saved` roda dentro do `save()`; um `push` sem `try/catch` faz o `save()` lançar mesmo com a linha já gravada. Os três models atuais protegem com `try/catch` e apenas logam (`News.php:300-319`).
4. **`description` é texto escapado**: notícias passam `summary`/`subtitle` crus (podem conter HTML? `[I]` não; `summary` é texto) e Agenda/Trabalhe usam `strip_tags`. Colocar HTML em `description` faz as tags aparecerem literalmente no leitor.
5. **Cache de página**: qualquer teste de integração que publique e consulte `/feed.xml` pode receber corpo antigo por 5 min (`HIT`). Limpar via `v2:clear-page-cache` (`V2/Core/Console/Commands/ClearPageCache.php:21-46`) ou aguardar.
6. **`FeedItemDTO::fromArray` exige** as chaves `module`, `entity_type`, `entity_id`, `title`, `link`, `description`, `published_at`; ausência gera erro de índice/`new Carbon(null)` (`FeedItemDTO.php:180-195`).

### 3.5 Dívidas e divergências

- `README.md` lista `Views/cms/index.blade.php` como parte do serviço (`V2/Services/FeedRSS/README.md:54`), mas o arquivo não existe: o painel vive em `V2/Modules/Infraestrutura/FeedRss/Views/cms/index.blade.php` e é servido pelo módulo (`FeedRssSupportController.php:71`).
- `README.md:12` afirma "Soft Delete: Itens removidos vão para backup, sem perda de dados" — verdade para a linha, falso na prática funcional, porque impede a reinserção.
- `FeedRssController` injeta `SEOService` e nunca o usa (`FeedRssController.php:24-28`).
- `FeedRssService` aceita `ActivityLoggerService $activityLogger = null`, mas o provider sempre injeta; `new FeedRssService()` (fora do container) desliga auditoria silenciosamente.
- O painel de suporte não invalida `page:*` ao habilitar/desabilitar/remover (`FeedRssSupportController.php:81-123`).

## 4. Integrabilidade

### 4.1 API pública

| Símbolo | Assinatura | Retorno | Exceções | Consumidores |
|---|---|---|---|---|
| `FeedSender::forModule` | `static forModule($module): self` | instância | — | News, Evento, JobOpening |
| `FeedSender::push` | `push(FeedItemDTO $dto)` | `void` | `QueryException` (unique), `Exception` de Carbon/DB | módulos |
| `FeedSender::pushFromArray` | `pushFromArray(array $data)` | `void` | idem; erro se faltar chave obrigatória | módulos |
| `FeedSender::remove` | `remove($entityType, $entityId)` | `void` | `QueryException` | módulos |
| `FeedRssService::addItem` | `addItem(FeedItemDTO $dto): FeedRssItem` | item persistido | `QueryException` | `FeedSender::push` |
| `FeedRssService::removeItem` | `removeItem($entityType, $entityId): bool` | `true` se ≥1 linha atualizada | `QueryException` | `FeedSender::remove` |
| `FeedRssService::getFeedItems` | `getFeedItems($limit = 50): Collection` | coleção de `FeedRssItem` | `QueryException` | `generateXml` |
| `FeedRssService::generateXml` | `generateXml($limit = 50): string` | XML | erro de view/DB | `FeedRssController@feed` |
| `FeedRssService::getStats` | `getStats(): array` (`total, enabled, disabled, by_module, last_item, last_date, updated_at`) | array | `QueryException` | módulo Feed RSS |
| `FeedRssService::toggleItem` | `toggleItem($id, $enabled): FeedRssItem|null` | item ou `null` | `QueryException` | módulo Feed RSS |
| `FeedRssService::deleteItem` | `deleteItem($id): bool` | `true`/`false` | `QueryException` | módulo Feed RSS |
| `FeedRssService::getAllItemsPaginated` | `getAllItemsPaginated($perPage = 20): LengthAwarePaginator` | paginador | `QueryException` | módulo Feed RSS |
| `FeedItemDTO::__construct` | `($module, $entityType, $entityId, $title, $link, $description, Carbon $publishedAt, $content = null, $author = null, array $categories = [], $imageUrl = null)` | — | `TypeError` se `published_at` não for `Carbon` | `FeedSender` |
| `FeedItemDTO::fromArray` | `static fromArray(array $data): self` | DTO | erro de índice/`Carbon` inválido | `FeedSender::pushFromArray` |
| `FeedItemDTO::toArray` | `(): array` com `published_at` em ISO-8601 | array | — | debug/log (sem consumidor) |

### 4.2 Componentes Blade e contrato de UI

| Item | Valor |
|---|---|
| View de saída | `v2-service-feedrss::rss` (`V2/Services/FeedRSS/Views/rss.blade.php`) |
| Variáveis obrigatórias | `$items`, `$siteName`, `$siteUrl`, `$feedUrl`, `$description`, `$supportEmail`, `$logoUrl`, `$pubDate`, `$buildDate` (`FeedRssService.php:130-142`) |
| Estrutura | `<?xml ...?>` na primeira linha, `<rss version="2.0">` com `xmlns:atom`, `xmlns:media`, `xmlns:dc`; `<channel>` com title/link/description/language/atom:link/pubDate/lastBuildDate/generator/ttl/webMaster/managingEditor/image; `<item>` conforme §2.3 |
| DOM/IDs/`data-*` | não se aplica (documento XML, não HTML) |
| Slots/eventos JS | nenhum |
| Consumidor | `FeedRssService::generateXml()` apenas; nenhum `@include` em outra view |

Painel (módulo consumidor, não o serviço): view `v2-feedrss::cms.index`, cards de total/habilitados/desabilitados/URL, distribuição por módulo, tabela paginada com toggle e delete (`V2/Modules/Infraestrutura/FeedRss/Views/cms/index.blade.php:1-80`).

### 4.3 JS e ciclo de vida

Não se aplica ao serviço. O painel usa formulários POST/DELETE clássicos com `@csrf` no módulo; não há JS específico do FeedRSS.

### 4.4 Ordem de inicialização e registro

1. `V2\Services\FeedRSS\Providers\FeedRssServiceProvider` está registrado em `config/app.php:187` e faz `singleton(FeedRssService::class)` injetando `ActivityLoggerService` (`FeedRssServiceProvider.php:26-33`).
2. `V2ServiceProvider::boot()` carrega migrations (`V2_feed_rss_items`), views no namespace `v2-service-feedrss` e rotas web com `web + MaintenanceMiddleware + page.cache + public.cache` (`V2/Core/Providers/V2ServiceProvider.php:168-191,216-220`).
3. `ActivityLoggerService` deve existir antes da primeira resolução de `FeedRssService`: o provider registra uma closure que faz `$app->make(ActivityLoggerService::class)` no momento da resolução (`FeedRssServiceProvider.php:28-32`); se a classe não existir, a falha ocorre nessa resolução, não no `register()`.
4. Models que publicam (`News`, `Evento`, `JobOpening`) resolvem `FeedSender` em runtime; a tabela e a rota já precisam existir. As migrations seguem a ordem `2026_06_14_100000` (activity log) → `2026_06_30` (mail) → `2026_07_20_000001` (feed items).
5. `routes/web.php` não importa `Route` (usa o alias global), então depende do bootstrap do framework, não de um `use` explícito (`V2/Services/FeedRSS/Routes/web.php:1-8`).

### 4.5 Receitas de uso

**R1 — publicar no hook `saved` (padrão canônico).**

```php
use V2\Services\FeedRSS\Consumer\FeedSender;

static::saved(function ($model) {
    try {
        if ($model->status === News::STATUS_PUBLISHED) {
            FeedSender::forModule('noticias')->pushFromArray([
                'entity_type'  => 'news',
                'entity_id'    => $model->id,
                'title'        => $model->title,
                'link'         => $model->getPublicUrl() ?: url('/noticia/' . $model->slug),
                'description'  => $model->summary ?: '',
                'published_at' => $model->published_at ?: $model->created_at,
            ]);
        } else {
            FeedSender::forModule('noticias')->remove('news', $model->id);
        }
    } catch (\Exception $e) {
        \Log::error('[News] Falha ao sincronizar feed RSS: ' . $e->getMessage());
    }
});
```

Resultado esperado: item visível em `/feed.xml` após o cache expirar; auditoria `feed_rss_item_upserted`.

**R2 — remover no hook `deleted`.**

```php
FeedSender::forModule('noticias')->remove('news', $model->id);
```

Resultado: `deleted_at` marcado; item desaparece do feed.

**R3 — operação de suporte (toggle).**

```http
POST /cms/v2/feed-rss/{id}/toggle
Content-Type: application/x-www-form-urlencoded

enabled=0&_token=...
```

Resultado: flash `Item "<título>" desabilitado com sucesso!` e redirect para `v2.feedrss.support.index` (`FeedRssSupportController.php:88-98`).

**R4 — consumir o feed.**

```bash
curl -i https://portal/feed.xml
# 200, Content-Type: application/rss+xml; charset=utf-8,
# X-Cache-Status: MISS|HIT, Cache-Control: public, max-age=60, must-revalidate
```

### 4.6 Anti-uso

1. **Publicar sem `try/catch` dentro de model events.** `FeedSender::push` não engole exceções; o save do conteúdo (já persistido) retorna 500 quando o feed falha (ex.: unique violation após remoção).
2. **Reutilizar `entity_type` entre módulos.** A unique não inclui `module`; dois módulos com `entity_type='news'` e o mesmo UUID sobrescrevem o item um do outro.
3. **Confiar em `remove` como definitivo.** A linha permanece; republicar o mesmo par quebra por unique. Se a reimplementação quiser permitir retorno, precisa de purge físico ou unique parcial (`WHERE deleted_at IS NULL`) — mudança de contrato a registrar.
4. **Esperar HTML em `<description>`.** O campo é escapado; use texto puro e reserve `content` para HTML.

### 4.7 Custos e limites

| Limite | Valor | Fonte |
|---|---|---|
| Itens por feed | `V2_FEED_RSS_LIMIT` (default 50) | `config/v2.php:13-15`; `FeedRssController.php:37` |
| Tamanho de `title` | 255 | migration `:32` |
| `link`/`image_url` | 500 | migration `:33,38` |
| `entity_type`/`module` | 50 | migration `:29-30` |
| `entity_id` | 36 (UUID) | migration `:31` |
| Cache de página | 5 min (Redis) | `PageCacheMiddleware.php:13-14` |
| Cache HTTP | `max-age=60, must-revalidate` | `PublicCacheMiddleware.php:88` |
| Paginação do painel | 20 por página | `FeedRssService.php:233-236` |
| Throttle | nenhum | `Routes/web.php:5-8` |

## 5. Rotas e endpoints

| Método | URI | Nome | Middleware | Controller@ação | Permissão | Efeito | Fonte |
|---|---|---|---|---|---|---|---|
| GET | `/feed.xml` | `v2.feedrss.feed` | `web` + `MaintenanceMiddleware` + `page.cache` + `public.cache` | `FeedRssController@feed` | pública | devolve RSS 2.0 com até `limit` itens habilitados; cache de 5 min | `V2/Services/FeedRSS/Routes/web.php:5-8`; `V2/Core/Providers/V2ServiceProvider.php:216-220` |

Rotas do **módulo consumidor** `MOD-INFRAESTRUTURA-FEEDRSS` (mesmo URL space, dono diferente): `GET cms/v2/feed-rss` (`v2.feedrss.support.index`), `POST cms/v2/feed-rss/{id}/toggle` (`v2.feedrss.support.toggle`), `DELETE cms/v2/feed-rss/{id}` (`v2.feedrss.support.destroy`), todas com `CmsAuthenticate` e checagem `isSupportUser()` (`V2/Modules/Infraestrutura/FeedRss/Routes/cms.php:5-24`; `FeedRssSupportController.php:39-51`).

## 6. Dados e migrations

### `v2_feed_rss_items`

| Coluna | Tipo | Null/default | Observação |
|---|---|---|---|
| `id` | uuid | PK | `v2_uuid()` no evento `creating` (`FeedRssItem.php:79-88`) |
| `module` | varchar(50) | NOT NULL | ex.: `noticias`, `agenda`, `trabalhe-conosco`; índice |
| `entity_type` | varchar(50) | NOT NULL | `news`, `evento`, `job-opening` |
| `entity_id` | varchar(36) | NOT NULL | UUID da origem |
| `title` | varchar(255) | NOT NULL | escapado no XML |
| `link` | varchar(500) | NOT NULL | URL pública |
| `description` | text | null | texto; escapado |
| `content` | text | null | HTML; CDATA |
| `author` | varchar(255) | null | `dc:creator`, fallback `siteName` |
| `categories` | jsonb | null | cast `array`; um `<category>` por item |
| `image_url` | varchar(500) | null | `media:content` + `enclosure` |
| `enabled` | boolean | default `true` | índice; suporte pode desabilitar |
| `published_at` | timestamp | null | índice; ordenação |
| `created_at`/`updated_at` | timestamps | — | última sincronização |
| `deleted_at` | timestamp | null | soft delete |

Constraints: unique `uq_v2_feed_rss_items_entity (entity_type, entity_id)`; índices `idx_v2_feed_rss_items_module`, `idx_v2_feed_rss_items_enabled`, `idx_v2_feed_rss_items_published_at`; sem FK (`V2/Services/FeedRSS/Migrations/2026_07_20_000001_create_v2_feed_rss_items_table.php:27-48`).

A migration `up()` **dropa** tabelas `backup_v2_feed_rss_items_%` preexistentes antes de criar a tabela (`:22-25`) e usa transação + `lock_timeout`; `down()` renomeia para `backup_v2_feed_rss_items_<Ymd_His>` (`:62-73`).

Model: `$incrementing = false`, `$keyType = 'string'`, `fillable` com todos os campos de escrita, `casts` `categories => array`, `enabled => boolean`, `published_at => datetime` (`FeedRssItem.php:31-74`). Escritores: `FeedRssService::addItem/toggleItem/deleteItem/removeItem`; nenhum outro ponto do repositório escreve na tabela.

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

| Chave | Default | Efeito | Fonte |
|---|---|---|---|
| `v2.feed_rss.limit` (`V2_FEED_RSS_LIMIT`) | `50` | número máximo de itens no XML | `config/v2.php:13-15`; `FeedRssController.php:37` |
| `v2.support.email` (`V2_SUPPORT_EMAIL`) | `suporte@maxima.inf.br` | `webMaster`/`managingEditor` do canal | `config/v2.php:4-7`; `FeedRssService.php:118`; `rss.blade.php:16-17` |
| `app.name` | `env('APP_NAME','Laravel')` | fallback de `siteName` quando `titulo_site` não existe | `FeedRssService.php:120-128` |
| `activitylog.table_name` | `activity_log` | destino da auditoria | `config/activitylog.php:4` |

Permissões: `/feed.xml` é pública. O painel do módulo não usa permissão de módulo: checa `super-admin` ou igualdade com `v2.support.email` (`FeedRssSupportController.php:39-51`) e o middleware de permissão bloqueia o grupo `SUPPORT` para outros e-mails (`V2/Modules/Infraestrutura/FeedRss/ModuleProvider.php:20-38`; `V2/Core/Middleware/V2PermissionMiddleware.php:47-63`).

## 8. Observabilidade e erros

| Situação | Sinal | Texto/detalhe | Fonte |
|---|---|---|---|
| Falha de sync no save de notícia | `Log::error` no model | `[News] Falha ao sincronizar feed RSS: <msg>` | `News.php:315-317` |
| Falha de sync no save de evento | `Log::debug`/`Log::error` | `[Evento] Hook saved: RSS push OK para id=…` / `[Evento] Falha ao sincronizar feed RSS: …` | `Evento.php:132-139,153-155` |
| Falha de sync em vaga | `Log::error` | `[JobOpening] Falha ao sincronizar feed RSS: <msg>` | `JobOpening.php:160-162` |
| Unique violation na republicação | `QueryException` capturada pelo model e logada | mensagem do Postgres (`duplicate key value violates unique constraint "uq_v2_feed_rss_items_entity"`) | §3.2 `RN-FEEDRSS-03` |
| `PortalConfigsService` indisponível | silêncio (catch vazio) | usa `app.name` | `FeedRssService.php:126-128` |
| Redis fora do ar | `Log::warning` do middleware | `[PageCache] Redis indisponivel, servindo sem cache: <msg>` | `PageCacheMiddleware.php:62-65` |
| Acesso ao painel por não-suporte | flash + redirect | `Acesso restrito ao suporte.` | `FeedRssSupportController.php:61-64,109-112` |
| Item inexistente no toggle | flash | `Item não encontrado.` | `FeedRssSupportController.php:91-92` |
| Rollback da migration | renomeia tabela | `backup_v2_feed_rss_items_<timestamp>` | migration `:62-73` |

Auditoria: todas as escritas do serviço geram linha em `activity_log` com `properties.type` `feed_rss_service` (push/remove) ou `feed_rss_support` (toggle/delete) e descrições `feed_rss_item_upserted`, `feed_rss_item_removed`, `feed_rss_item_enabled`, `feed_rss_item_disabled`, `feed_rss_item_deleted` (`FeedRssService.php:61-67,85-90,191-196,218-222`). A remoção usa sujeito vazio, portanto `subject_id` nulo.

## 9. Consumidores (mapa de integração)

| Consumidor | O que usa | Como usa | Risco de quebra |
|---|---|---|---|
| `MOD-PUBLICACOES-NOTICIAS` | `FeedSender` | `saved`: publica se `STATUS_PUBLISHED`, senão remove; `deleted`: remove (`News.php:292-330`) | alto — mudar nomes de `entity_type`/campos ou lançar exceção |
| `MOD-PUBLICACOES-AGENDA` | `FeedSender` | `saved`: publica se `is_active`, categorias `agenda`,`eventos`(+`target_audience`); senão remove (`Evento.php:104-155`) | alto |
| `MOD-PUBLICACOES-TRABALHECONOSCO` | `FeedSender` | `saved`/`deleted` com `job-opening` (`JobOpening.php:130-180`) | alto |
| `MOD-INFRAESTRUTURA-FEEDRSS` | `getStats`, `getAllItemsPaginated`, `toggleItem`, `deleteItem` | painel CMS restrito ao suporte (`FeedRssSupportController.php:59-123`) | médio — assinaturas e chaves de `getStats` |
| `SVC-DASHBOARD` | nome da classe `FeedRssItem` | mapa de entidades do dashboard (`V2/Services/Dashboard/DashboardStatsService.php:677`) | baixo |
| `SVC-ACTIVITYLOGGER` | `log()` | recebe cada escrita do feed | médio — se auditoria falhar, a escrita do feed lança? Não: o log do Spatie pode lançar e o `addItem` não protege `[I]` |

## 10. Lacunas

| ID | Lacuna | Impacto | Onde procurar |
|---|---|---|---|
| `LG-FEEDRSS-01` | O comportamento desejado para republicação após remoção (falhar como hoje ou restaurar) não está documentado; não há teste que cubra o fluxo | decisão de paridade: manter bug ou corrigir com unique parcial/delete físico | `tests/` inexistente; README afirma soft delete "sem perda" |
| `LG-FEEDRSS-02` | Não há feed por módulo nem filtros; não se sabe se consumidores externos esperam `/noticias/feed.xml` | reimplementação pode precisar de rotas extras | logs de acesso / SEO |
| `LG-FEEDRSS-03` | Não há testes automatizados (`README.md:76-80` cita `phpunit --filter FeedRSS`, mas não há suíte no repo) | paridade manual | `tests/` |
| `LG-FEEDRSS-04` | Não se sabe a cardinalidade real de `v2_feed_rss_items` em produção; sem purge de linhas soft-deleted | tabela cresce; `getStats` degrada | banco de produção |
| `LG-FEEDRSS-05` | `media:content` fixa `image/jpeg`; não se sabe se há imagens PNG/WebP publicadas | leitores podem ignorar a imagem | dados de produção |

## 11. Checklist de paridade

- [ ] `PC-FEEDRSS-01` — `pushFromArray` cria linha com `module`, `entity_type`, `entity_id`, `enabled=true` e `published_at` (default `now()`) (`FeedSender.php:73-87`; `FeedRssService.php:42-59`).
- [ ] `PC-FEEDRSS-02` — segundo `push` do mesmo par atualiza a mesma linha (contagem em `v2_feed_rss_items` não aumenta) e reabilita `enabled` (`FeedRssService.php:42-59`).
- [ ] `PC-FEEDRSS-03` — `remove` marca `deleted_at` e o item some de `GET /feed.xml` (`FeedRssService.php:81-83`; `FeedRssItem.php:98-101`).
- [ ] `PC-FEEDRSS-04` — `GET /feed.xml` devolve `200`, `Content-Type: application/rss+xml; charset=utf-8` e XML com `<rss version="2.0">` (`FeedRssController.php:40-42`; `rss.blade.php:1-5`).
- [ ] `PC-FEEDRSS-05` — itens aparecem em `published_at DESC` e no máximo `config('v2.feed_rss.limit')` (`FeedRssService.php:101-107`).
- [ ] `PC-FEEDRSS-06` — feed vazio produz exatamente 1 `<item>` com título `Nenhum conteúdo disponível no momento` (`rss.blade.php:50-56`).
- [ ] `PC-FEEDRSS-07` — cada item tem `guid` `urn:uuid:<id>` com `isPermaLink="false"` e `pubDate` RFC 2822 (`rss.blade.php:30-32`).
- [ ] `PC-FEEDRSS-08` — `content` presente gera `<content:encoded><![CDATA[...]]></content:encoded>`; ausente omite o elemento (`rss.blade.php:46-48`).
- [ ] `PC-FEEDRSS-09` — `image_url` presente gera `media:content` **e** `enclosure` `type="image/jpeg" length="0"` (`rss.blade.php:40-45`).
- [ ] `PC-FEEDRSS-10` — título/descrição com `&`/`<` aparecem escapados (`&amp;`/`&lt;`) (`rss.blade.php:28,31`).
- [ ] `PC-FEEDRSS-11` — cada escrita do serviço gera linha em `activity_log` com `v2_audit=true` e `properties.type` correspondente (`FeedRssService.php:61-67,191-196`).
- [ ] `PC-FEEDRSS-12` — `getStats()` devolve `total`, `enabled`, `disabled`, `by_module`, `last_item`, `last_date`, `updated_at` (`FeedRssService.php:150-170`).
- [ ] `PC-FEEDRSS-13` — requisição repetida em < 5 min devolve `X-Cache-Status: HIT` (`PageCacheMiddleware.php:35-40`).

## 12. Checklist de extração

| Item | Resposta |
|---|---|
| A1–A4 | §1.1–1.2. |
| A5 | §1.3 com resultado por entrada. |
| A6 | §1.4 (sem feed por módulo, sem invalidação de cache, sem validação). |
| A7 | §1.5. |
| B1–B3 | §2.1–2.2. |
| B4 | §2.3 com 6 trechos (39-59, 79-92, 101-117, rss 26-49, News 301-317, controller 35-43). |
| B5–B6 | §2.4–2.5. |
| B7 | §2.5 e §3.4 item 3: sem transação entre item e auditoria; hooks com `try/catch`. |
| C1–C2 | §3.1. |
| C3 | §3.2 (`RN-FEEDRSS-01..10`). |
| C4–C5 | §3.3. |
| C6 | §3.4. |
| C7 | §3.5 (README cita view inexistente; SEOService morto; auditoria opcional). |
| D1–D3 | §4.1, §4.4. |
| D4 | §4.2. |
| D5–D6 | §4.5–4.6. |
| D7 | §4.7. |
| D8 | §9. |
| D9 | Extensão: novo módulo publica com `entity_type` próprio; não há registro central de tipos (texto livre). |
| E1–E6 | §5; `GET /feed.xml` sem parâmetros, sem throttle, cache de página e HTTP. |
| E7 | Sem rotas dinâmicas; URI fixa `/feed.xml`. |
| F1–F6 | §6; volume = 1 linha por entidade publicada + linhas soft-deleted sem purge. |
| G1–G6 | Não se aplica ao serviço; painel descrito em §4.2 (módulo). |
| H1–H6 | A saída não é página do portal; H3 parcialmente: o feed é conteúdo de SEO/descoberta, com `atom:link`, `ttl`, `webMaster`; H6 = cache de página + HTTP. |
| I1–I8 | Não se aplica (sem componente interativo/JS). |
| J1–J2 | §7. |
| J3–J5 | Não se aplica ao serviço; `ModuleProvider` do módulo consumidor em `V2/Modules/Infraestrutura/FeedRss/ModuleProvider.php:18-38`. |
| K1–K5 | §8; sem dados sensíveis no feed (apenas conteúdo público). |
| L1–L3 | Não se aplica: sem seeder; dados vêm dos hooks dos módulos. |
| M1–M4 | §11, §10, §13, lint/índices emitidos. |

## 13. Evidências consultadas

- `V2/Services/FeedRSS/Services/FeedRssService.php` (1-237, integral).
- `V2/Services/FeedRSS/Consumer/FeedSender.php` (1-88).
- `V2/Services/FeedRSS/DTOs/FeedItemDTO.php` (1-218).
- `V2/Services/FeedRSS/Models/FeedRssItem.php` (1-150).
- `V2/Services/FeedRSS/Controllers/FeedRssController.php` (1-44).
- `V2/Services/FeedRSS/Routes/web.php` (1-8); `Providers/FeedRssServiceProvider.php` (1-34).
- `V2/Services/FeedRSS/Views/rss.blade.php` (1-59).
- `V2/Services/FeedRSS/Migrations/2026_07_20_000001_create_v2_feed_rss_items_table.php` (1-74).
- `V2/Services/FeedRSS/README.md` (1-87).
- `V2/Core/Providers/V2ServiceProvider.php:168-248`; `config/app.php:185-187`; `config/v2.php:4-15`.
- `V2/Core/Middleware/PageCacheMiddleware.php:11-91`; `PublicCacheMiddleware.php:31-95`.
- `vendor/laravel/framework/src/Illuminate/Database/Eloquent/SoftDeletingScope.php:32-45`; `Builder.php:836-843`.
- `V2/Modules/Publicacoes/Noticias/Models/News.php:292-332`; `V2/Modules/Publicacoes/Agenda/Models/Evento.php:104-160`; `V2/Modules/Publicacoes/TrabalheConosco/Models/JobOpening.php:130-185`.
- `V2/Modules/Infraestrutura/FeedRss/Controllers/FeedRssSupportController.php:39-123`; `Routes/cms.php:5-24`; `ModuleProvider.php:18-38`; `Views/cms/index.blade.php:1-80`.
- `V2/Services/Dashboard/DashboardStatsService.php:677`.
