# Notícias — Regras de Negócio e Portabilidade

## 1. Regras não óbvias

| # | Regra | Onde está implementada | Consequência observável |
|---|---|---|---|
| R1 | Só aparece no portal conteúdo com `status = true` **e** `publication_date <= now()` | `Content::scopeByStatus` (`v6/app/Http/Model/Prefecture/Content.php:139-144`) e `NewsTrait::getContentNews` (`:28-40`) | Rascunhos e matérias agendadas somem do portal, mas continuam no CMS |
| R2 | O portal lista apenas conteúdos cuja **categoria é filha** da categoria "Notícias" (id 1) | `Content::scopeByCategoryContentNews` (`:212-224`) | Conteúdos gravados na categoria 1 são invisíveis no portal |
| R3 | A view SQL `noticias` (CMS) usa o mesmo filtro `cc.category_contents_id = 1` | `v6/database/migrations/2024_07_01_094338_udate_noticias_view.php` | Datatable do CMS e portal compartilham o mesmo conjunto (hoje vazio no banco) |
| R4 | A listagem por categoria restringe a **subcategoria** específica | `CategoryContent::scopeByCategoryNews` (`v6/app/Http/Model/Prefecture/CategoryContent.php:122-140`) | `/noticias/{slug}` filtra por `slug` da subcategoria |
| R5 | `highlights = 1` coloca a matéria no carrossel; `carousel = 1` marca a matéria como pertencente ao carrossel rotativo | `ContentController::setHighlights` (`:1510-1546`) — desmarcar destaque **zera** `carousel` | Marcas são mutuamente dependentes |
| R6 | Ao marcar `status`, para `anuncios`, `super-banners` e `transmissoes` os irmãos são desligados | `ContentController::setStatus` (`:1548-1587`) | Regra de "item único" reaproveitada de outros módulos |
| R7 | A ordenação (`order`) é reescrita em bloco ao gravar | `ContentController::store` (`:682-692`) e `update` (`:1004-1024`) | Itens entre o novo e o antigo `order` são deslocados em ±1 |
| R8 | Slug deve ser único; a desambiguação é `-<id>` (criação) ou `pub<id>-` (edição fora da categoria 1) | `ContentController::store` (`:665-668`) e `update` (`:973-991`) | URLs antigas podem mudar de formato |
| R9 | Excluir um conteúdo apaga também os `menu_items` que apontam para ele | `ContentController::destroy` (`:1320-1367`) | O item de menu some do portal |
| R10 | `clicks` é incrementado em read-modify-write | `NewsController::updateClicks` (`:137-142`) | Perde contagem sob concorrência |
| R11 | `quantidade_itens` define quantas matérias a categoria mostra na home; categorias com `quantidade_itens <= 0` são ignoradas | `NewsTrait::getCategoriesNews` (`:66-75`) | No banco atual todos os valores são `NULL` -> nenhuma faixa é exibida |
| R12 | `layout` da categoria "noticias" define o layout de listagem; valores fora de 1..3 caem para 2 | `NewsTrait::getLayoutContentActived` (`:88-96`) | Layout "default" é o 2 |
| R13 | A galeria só aparece se `galery = 1`; o `highlight` do arquivo separa imagem de destaque (1) de galeria (NULL) | `Content::archiveHighlight` / `archiveGallery` (`:170-186`) | `archives.highlight = 0` é usado por "a-cidade" (`archiveAcidade`) |

## 2. Acoplamentos

### Dependências (este módulo -> outros)

| Depende de | Tipo | Onde |
|---|---|---|
| `category_contents` | dados | Classificação pai/filha de todas as listagens |
| `archives` | dados | Imagem de destaque, galeria, PDF inline |
| `links` | dados | Vídeo único por conteúdo |
| `menu_items` | dados | Exclusão cascata manual; menu do portal |
| `configurations` | configuração | `noticias`, `aviso_noticias`, `layout` |
| `layouts` | configuração | `carousel-noticias` (modelo5) |
| `menu_orders` | métrica | `menuClicks($slug)` contabiliza o acesso ao menu (`v6/app/Helpers/functions.php:865-872`) |
| `types_sizes` | dados | `type_size_id` do arquivo de destaque (banners) |
| `contents` (auto-relação) | dados | `secretarias` (por slug) e `orgaos` (por `orgaos_id`) |
| `city_councilors` | dados | `councilman()` (módulo de requerimentos) |

### Dependentes (outros -> este)

| Depende de | Onde |
|---|---|
| Home de todos os layouts | `lastNewsForLayout()` + `getContentsByParentCategory()` |
| Bloco de destaques do modelo5 | `v6/resources/views/portal/layouts/modelo5/home/destaques/carousel-noticias.blade.php` |
| Busca global do portal | `v6/public/assets/js/custom/utilities/helpers/search.js` |
| Menu do portal | `v6/app/Providers/MenuServiceProvider.php:308` (bloco "Mais Conteúdos") |
| API de dados abertos | `v6/app/Http/Controllers/Swagger/V1/Portal/NewController.php` |

## 3. Armadilhas

### 3.1 `session()` dentro de scopes Eloquent — vazamento de estado

`v6/app/Http/Model/Prefecture/Content.php:212-243`:

```php
public function scopeByCategoryContentNews($query, $category = null)
{
    session(['categorySlugSession' => $category]);
    $query->whereHas('categoryContents', function ($query) {
        $category = session('categorySlugSession');
        if (!empty($category) && isset($category) && $category != 'noticias') {
            $query->byCategoryNews($category);
        }
        $query->where('category_contents_id', 1);
    });
    return $query;
}

public function scopeByLimit($query, $limit)
{
    if (!empty($limit) && $limit > 0) {
        session(['limit' => $limit]);
        $query->whereHas('categoryContents', function ($query) {
            $limit = session('limit');
            $query->limit($limit);
        });
    }
    return $query;
}
```

Problemas:

1. **Escopo lateral em query builder**: a closure lê a sessão **durante**
   a execução da query, e não no momento da chamada do scope. O valor pode
   ser sobrescrito por outra requisição concurrenta no mesmo processo (PHP-FPM
   com sessão em arquivo/Redis compartilhado).
2. **Contaminação entre requisições**: `categorySlugSession` permanece na
   sessão do visitante. Só `NewsController::indexContentsAjax` faz
   `session()->pull('categorySlugSession')`
   (`v6/app/Http/Controllers/Portal/NewsController.php:113`); as demais rotas
   **não** limpam.
3. **`limit()` dentro de `whereHas`**: aplica `LIMIT` ao *sub-select* de
   categorias, não aos conteúdos — o efeito pretendido (limitar notícias) não
   ocorre.

### 3.2 `scopeByStatusNoticias` avalia apenas o primeiro registro

`v6/app/Http/Model/Prefecture/Content.php:145-160`:

```php
public function scopeByStatusNoticias($query)
{
    $publication_dates = $query->get()->pluck(['publication_date'])->toArray();
    foreach ($publication_dates as $publication_date) {
        if (isset($publication_date) && $publication_date != null) {
            return $query->where('status', true);
        } else {
            return $query;
        }
    }
}
```

O `return` dentro do `foreach` sai no primeiro item: ou o filtro
`status = true` é aplicado a todo o lote, ou nenhum filtro é aplicado.
Além disso, `$query->get()` **executa a query** dentro do scope, duplicando
o custo.

### 3.3 `limit()` antes de `paginate()`

Em `NewsTrait::getContentNews` e `getContentNewsAjax` o `limit()` é aplicado
antes do `paginate()`/`get()`. No `paginate()` o Laravel recalcula o limite
via `forPage()`, descartando o `limit` anterior; no `get()` o `limit` anterior
é substituído. Resultado: `$limit` é inócuo nas listagens paginadas.

### 3.4 SQL com interpolação direta

- `PublicationController::showPublication` e `export` montam `whereRaw` com
  `translate(...) ilike '%' . $newTXT . '%'` concatenando o texto do usuário
  após `clean_slug()` — sem binding (afeta o módulo Publicações, ver
  `../publicacoes/05-REGRAS-E-PORTABILIDADE.md`).
- `PortalController::searchContents` / `searchingContents` também usam
  `whereRaw` (busca global do portal).

### 3.5 N+1 em listagens

`NewsTrait::getMainContent()` (`:20-26`) carrega `categoryContents` e
`archives` com `with()`, mas várias views usam
`$content->categoryContents->categoryContentsBelongings` (pai da categoria),
que **não** está no `with()` -> uma query extra por linha.

### 3.6 Controller genérico com `if` encadeado

`ContentController::genericIndex` (`:91-119`) e `genericGenerate`
(`:163-273`) são uma cadeia de `if ($slug === '...')`. Acrescentar um módulo
exige editar o controller central — acoplamento forte entre módulos.

### 3.7 `Route::` dentro de controller

`ContentRequest::rules()` chama `Route::getCurrentRoute()->getName()`
(`v6/app/Http/Requests/ContentRequest.php:76` e `:104`) para decidir se a
slug é obrigatória. Isso acopla a validação ao nome da rota — renomear a rota
quebra a validação silenciosamente.

### 3.8 Escrita de arquivo fora do Storage

`ContentController::pathUpdate` (`:373-435`) grava com `file_put_contents()`
e `@mkdir()`, ignorando o disco do Laravel; `uploadImageContent` (`:535-556`)
usa `$file->move(public_path() ...)`.

### 3.9 Slug do conteúdo pode colidir com slugs de categorias

`StaticPageRepository::structureUpInsert` (`v6/app/Repositories/Eloquent/Prefecture/StaticPage/StaticPageRepository.php:25-42`)
verifica explicitamente a colisão com `category_contents.slug`; o
`ContentController` **não** faz essa verificação.

## 4. Código duplicado entre v5 e v6

| Trecho | v5 | v6 | Observação |
|---|---|---|---|
| `ContentController` (CMS) | `v5/app/Http/Controllers/Prefecture/ContentController.php` | `v6/app/Http/Controllers/Prefecture/ContentController.php` | Diferença de ~1400 linhas; a v6 acrescenta `sanitizeLink()`, `linha_fina`, `councilmen_id`, `view_pdf`, `pathUpdate`, auditoria enriquecida |
| `Content` (model) | `v5/app/Http/Model/Prefecture/Content.php` | `v6/app/Http/Model/Prefecture/Content.php` | Diferença de 10 linhas: `linha_fina`, `councilmen_id` e a relação `councilman()` |
| `NewsController` (portal) | `v5/app/Http/Controllers/Portal/NewsController.php` | `v6/app/Http/Controllers/Portal/NewsController.php` | Única diferença: ordenação da galeria (`created_at desc` -> `order asc`) |
| `NewsTrait` | `v5/app/Http/Traits/NewsTrait.php` | `v6/app/Http/Traits/NewsTrait.php` | Mantido praticamente idêntico |

## 5. Onde a v6 divergiu da v5

| Tema | v5 | v6 | Impacto |
|---|---|---|---|
| Galeria da notícia | ordenada por `created_at desc` | ordenada por `order asc` | A ordem manual passa a valer |
| Slug do conteúdo | gerado por `str_slug` | por `slugfy()` + desambiguação `-<id>` / `pub<id>-` | URLs mais previsíveis |
| `linha_fina` | inexistente | novo campo (max 300) | Subtítulo nas listagens |
| `councilmen_id` | inexistente | novo campo | Vinculação de matéria a um vereador |
| Link de vídeo | salvo cru | sanitizado por `sanitizeLink()` | Evita `https:///rota-interna` |
| Auditoria | genérica | com título/texto/posição antes e depois | Rastreabilidade |
| Publicações (módulo irmão) | sem rota de export | `GET /publicacoes/export` | Nova capacidade de exportação |

## 6. Recomendações de portabilidade

1. Remover `session()` dos scopes; passar o valor por closure ou por
   parâmetro do método de consulta.
2. Substituir `scopeByStatusNoticias` por um `whereHas` equivalente ou
   remover o scope (não há consumidor no portal).
3. Aplicar `limit` **após** a paginação ou via `paginate($perPage)`.
4. Trocar `whereRaw` por bindings (`whereRaw(..., [$termo])`).
5. Extrair o `ContentController` genérico para um serviço
   (`ContentService`) com uma tabela de configuração por slug
   (view, regras, campos), eliminando a cadeia de `if`.
6. Mover a verificação de slug para uma tabela `contents_slug_reservas` ou
   para uma coluna `reserved` em `category_contents`.
7. Usar `Storage::disk()` em `pathUpdate` e `uploadImageContent`.
8. Criar índices compostos: `contents (category_contents_id, publication_date desc)`,
   `contents (slug)`, `archives (contents_id, highlight, order)`.
