# Mais Conteúdos — Regras de Negócio e Portabilidade

## 1. Regras de negócio

| # | Regra | Onde está implementada |
|---|---|---|
| M1 | Uma categoria pertence a "Mais Conteúdos" se `status_create = 0` **e** `category_contents_id IS NULL` | `v6/app/Providers/MenuServiceProvider.php:31` |
| M2 | Toda categoria criada pelo CMS nasce com `status_create = 0` | `v6/app/Http/Controllers/Prefecture/CategoryContentController.php:456` |
| M3 | O slug é gerado por `str_slug($name)`; se já existir sob o mesmo pai, vira `cat<maxId+1>_<slug>` | `…/CategoryContentController.php:450` e `:467-471` |
| M4 | A posição é rebalanceada: categorias com `order >= novo` recebem `+1` (exceto `super-banners`) | `…/CategoryContentController.php:473-480` |
| M5 | Categorias de sistema (`status_create = 1`) não podem ser excluídas | `…/CategoryContentController.php:702` |
| M6 | Categoria com conteúdos vinculados não pode ser excluída | `…/CategoryContentController.php:707-717` |
| M7 | Ao excluir uma categoria, seus `menu_items` são removidos | `…/CategoryContentController.php:709-717` |
| M8 | O formulário de conteúdo só oferece as categorias "Mais Conteúdos" e suas subcategorias | `v6/app/Http/Controllers/Prefecture/ContentController.php:341-342` |
| M9 | Conteúdos só aparecem no portal se `status = true` e a data de publicação não for futura | `v6/app/Http/Controllers/Portal/PortalController.php:348` |
| M10 | A listagem do portal pagina 12 conteúdos por página | `…/PortalController.php:360` |
| M11 | Cada visita a um conteúdo incrementa `contents.clicks` | `…/PortalController.php:352` |
| M12 | As regras de validação mudam conforme a categoria pai (`noticias`, `transparencia-fiscal`) | `v6/app/Http/Requests/CategoryContentRequest.php` |
| M13 | `description` só é gravada quando o pai é `transparencia-fiscal` | `…/CategoryContentController.php:461-464` |

## 2. Acoplamentos

### Dependências de saída (o módulo usa)

| Módulo | Forma |
|---|---|
| **Menus (CMS)** | `MenuServiceProvider` monta o item "Mais Conteúdos"; sem o item `menu_orders.slug = 'categoria-conteudo'` (módulo 54) o submenu não aparece |
| **Categorias de Conteúdo** | `CategoryContentController` cria/edita/exclui as categorias |
| **Conteúdos** | `ContentController` publica os conteúdos e limita o select às categorias do módulo |
| **Registry de módulos** | `modulos.status` decide se o menu é exibido |
| **Portal (genérico)** | `PortalController@contentList` resolve as views `generic.*` |
| **Transparência** | `display_type` (`grouped`/`direct`) e `description` são consumidos pelo módulo de transparência |
| **Layout do portal** | As views genéricas existem por modelo (`modelo1`..`modelo6`, `default`) |

### Dependências de entrada (quem usa as categorias "Mais Conteúdos")

| Consumidor | Uso |
|---|---|
| `ContentController` | Select de categoria do CRUD de conteúdos |
| `MenuServiceProvider` | Submenu do AdminLTE |
| `PortalController` | Rotas `portal/{slug}` |
| `FooterServiceProvider` | Itens de rodapé ligados a `menu_items.category_contents_id` |
| `functions.php` (`getResultSearch`) | Agrupa resultados de busca por categoria (ids fixos 1..12, 32) |

## 3. Armadilhas

### A1 — "Mais Conteúdos" não é uma entidade

Não há tabela, model, controller ou rota do módulo. Qualquer código que procure
`CategoryContent::where('slug', 'mais-conteudos')` não encontra nada: o slug
`mais-conteudos` existe **apenas** na tabela `modulos` (registry do CMS).

### A2 — Recorte repetido em 6 lugares

`CategoryContent::where('status_create', 0)->whereNull('category_contents_id')` está
duplicado em:

| Arquivo:linha |
|---|
| `v6/app/Providers/MenuServiceProvider.php:31` |
| `v6/app/Http/Controllers/Prefecture/CategoryContentController.php:52` |
| `…/CategoryContentController.php:63` |
| `…/CategoryContentController.php:561` |
| `v6/app/Http/Controllers/Prefecture/ContentController.php:341` |
| `…/ContentController.php:361` |

### A3 — `status_create` é booleano, mas é filtrado com `0`

`where('status_create', 0)` funciona porque o PDO envia o parâmetro como texto e o
PostgreSQL coage `'0'` → `false`. Em SQL literal (`status_create = 0`) o PostgreSQL
**erra** (`operator does not exist: boolean = integer`). Risco de quem portar a
consulta.

### A4 — `getAllDescendants` não detecta ciclos

`v6/app/Http/Model/Prefecture/CategoryContent.php` implementa a recursão sem
proteção: um ciclo A→B→A provoca recursão infinita. Nada impede o CMS de criar
esse ciclo, pois o formulário permite escolher qualquer "Categoria Pai".

### A5 — SQL interpolado no controller do portal

```php
$anos = DB::select("select extract(year from publication_date) as ano from contents WHERE category_contents_id = {$category->id} AND deleted_at IS NULL GROUP BY ano order by ano desc;");
```

O valor vem do Eloquent (inteiro), mas a construção é textual. O mesmo padrão (com
`$_GET['ano']` interpolado) aparece em
`v6/app/Http/Controllers/Portal/AtasEPontosController.php:60` — ali sim há risco de
injeção.

### A6 — `catch (\Throwable $th) {}` silencioso

`v6/app/Http/Controllers/Portal/PortalController.php:366` — qualquer erro no
`contentList` (inclusive "variável indefinida") é engolido e a resposta fica em
branco (HTTP 200 sem conteúdo). Isso mascarou o bug do `$null` documentado em
`v6/.agents/docs/mais-conteudos.md`.

### A7 — `redirect()` dentro do listener de menu

`v6/app/Providers/MenuServiceProvider.php:33-34`:

```php
if (!Sentinel::check()) {
    redirect()->route('login');
}
```

O `redirect()` cria um `RedirectResponse` e o **descarta** — não redireciona. O
middleware `check.logged` é que de fato protege as telas.

### A8 — Colisão de nomes de rota

`POST /cms/ajust-categorias` usa o nome `categoria-conteudo.edit`, que é o mesmo do
`Route::resource` (registrado depois) — ver `03-ROTAS-E-CONTROLADORES.md`.

### A9 — Rotas do resource sem `check.logged`

`Route::resource('categoria-conteudo', ...)` (`v6/routes/basepro.php:173`) não aplica
o middleware `check.logged`, e o `RouteServiceProvider` só aplica o grupo `web`
(`v6/app/Providers/RouteServiceProvider.php:84-89`). Diferente de
`configuracoes`, que aplica `->middleware('check.logged')` explicitamente. A proteção
real depende de o `AdminLTE`/controller bloquear — o que **não** ocorre.

### A10 — Upload de `custom_icon` fora do Storage

```php
$dest = public_path('uploads/icons');
if (!file_exists($dest)) { mkdir($dest, 0755, true); }
$file->move($dest, $filename);
```

Gravação direta em `public/`, sem `Storage`, sem limpeza e sem validação de tipo ou
tamanho.

### A11 — Slug duplicado corrigido com `max(id)+1`

`$maxid = CategoryContent::max('id') + 1;` — frágil sob concorrência: duas
crições simultâneas podem gerar o mesmo slug.

### A12 — Regras condicionais por categoria pai no Request

`CategoryContentRequest` consulta `CategoryContent::find($request->get('category_contents_id'))->slug`
para decidir as regras. Se a categoria pai não existir, `->slug` em `null` gera erro.

### A13 — Mensagem de banco vazada na API

`apiStore`/`apiUpdate` devolvem `['message' => $th->getMessage()]` — o erro do
PostgreSQL vai para o cliente.

### A14 — Bloco do portal desativado com `@if(false)`

`v6/resources/views/portal/layouts/modelo5/home/transparencia/transparencia-fiscal-grid.blade.php:117`
mantém um bloco inteiro de código morto.

### A15 — `display_type` só é usado pela Transparência

A coluna foi criada para o grid de transparência (`grouped`/`direct`); não há
validação nem uso genérico.

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

| Arquivo | v5 vs v6 |
|---|---|
| `app/Http/Controllers/Prefecture/CategoryContentController.php` | **Divergente** (a v6 tem API interna, `display_type`, `custom_icon`, `description`) |
| `app/Http/Model/Prefecture/CategoryContent.php` | **Divergente** (campos novos no `$fillable`) |
| `app/Providers/MenuServiceProvider.php` | **Divergente** (registry de módulos, novos cases) |
| `resources/views/prefecture/category-contents/*` | **Divergente** |
| `database/migrations/2018_04_01_180916_create_category_contents_table.php` | Idêntico |
| `database/migrations/2021_02_18_123218_add_status_to_category_contents.php` | Idêntico |
| `database/migrations/2026_06_01_100000_add_display_type_and_custom_icon_to_category_contents.php` | Idêntico |
| `database/migrations/2026_06_04_100000_add_description_to_category_contents_table.php` | **Só na v6** |

## 5. Onde a v6 divergiu da v5

| Item | v5 | v6 |
|---|---|---|
| Tabela `modulos` | Não existe | `mais-conteudos` (id 54, status 1) |
| `menu_orders` | Sem item `categoria-conteudo` ligado ao módulo | Item `id = 42`, `modulo_id = 54` |
| `resources` | `conteudo` e `categoria-conteudo` | `id = 73` e `id = 74`, `modulo_id = 54` |
| API de categorias | Inexistente | `POST/PUT/DELETE /cms/categories` (`apiStore`, `apiUpdate`, `apiDestroy`) + `ShowResource` |
| Campos da categoria | `name`, `slug`, `status_create`, `order`, `quantidade_itens`, `layout`, `category_contents_id`, `status` | + `display_type`, `custom_icon`, `description` |
| Migration `description` | Ausente | `2026_06_04_100000_add_description_to_category_contents_table.php` |
| Regra de validação da descrição | Ausente | `nullable|max:500` quando o pai é `transparencia-fiscal` |
| Upload de `custom_icon` | Ausente | `public/uploads/icons/<timestamp>_<nome>` |
| Menu | Sem gate `modulos.status` | `MenuOrder::with('modulo')` + `modulos.status` obrigatório |

## 6. Regras que NÃO podem mudar na reconstrução

1. Uma categoria criada pelo operador **precisa** aparecer automaticamente no menu
   "Mais Conteúdos" (sem etapa adicional).
2. Categorias de sistema não podem ser excluídas pelo CMS.
3. Categoria com conteúdo não pode ser excluída (e o conteúdo não pode ser apagado
   junto).
4. Ao excluir, os itens de menu vinculados são removidos.
5. O formulário de conteúdo só oferece as categorias do módulo.
6. No portal, a profundidade da URL (`/portal/{slug}[/{slug2}[/{slug3}]]`) define o
   nível de visualização (subcategorias → conteúdos → conteúdo).
7. Conteúdo não publicado ou com data futura não aparece.
8. A ordenação das categorias é definida pelo operador e persistida.
