# Requerimentos — Regras de Negócio e Portabilidade

> **Versão analisada:** `v6` — módulo **novo na v6**
> **Slug:** `requerimentos` (`modulos.id = 41`)

---

## 1. Regras de negócio observáveis

| # | Regra | Onde está implementada | Efeito para o usuário |
|---|---|---|---|
| R1 | A raiz da árvore é a categoria com `slug = 'requerimentos'` e `category_contents_id IS NULL` | `CategoryContentTableSeeder.php:206-209` | Define o ponto de entrada do módulo |
| R2 | `/requerimentos` redireciona para a **primeira subcategoria** por `order` | `RequerimentoController@index` | O usuário nunca vê uma URL de listagem “genérica” |
| R3 | Sem subcategorias, o `index()` usa a própria raiz | `RequerimentoController@index` (fallback) | O portal continua funcionando com a raiz |
| R4 | Sem categoria raiz, redireciona para `/` com a mensagem “Categoria "Requerimentos" não encontrada neste ambiente.” | `RequerimentoController@index` | Mensagem de erro amigável |
| R5 | **Unificação**: a listagem mostra o conteúdo de **todas** as subcategorias, independentemente do slug da URL | `RequerimentoController@show` (`whereIn('category_contents_id', $categoryIds)`) | Trocar de aba lateral não muda o conjunto de itens; só muda o título e a categoria exibida no link |
| R6 | Filtro por ano (`whereYear`) e por mês (`whereMonth`) sobre `publication_date` | `RequerimentoController@show` | Reduz a lista |
| R7 | Filtro por texto busca em `title` **ou** `text`, ignorando acentos e maiúsculas | `RequerimentoController@show` (`whereRaw`/`orWhereRaw` com `translate(...) ilike ?`) | Busca tolerante a “acao” vs “ação” |
| R8 | Conteúdo com `publication_date` futura não aparece na listagem | `where('publication_date', '<=', $current_date)` | “Agendamento” de publicação |
| R9 | Sem filtro de mês, aplica-se um teto adicional `publication_date <= now` | `RequerimentoController@show` (branch `else`) | Redundante com R8, mas explicita a intenção |
| R10 | A paginação é de **12 itens por página** e preserva os filtros (`appends($request->query())`) | `RequerimentoController@show` | Paginação sem perder o filtro |
| R11 | Cada abertura do requerimento **incrementa `contents.clicks` em 1** | `RequerimentoController@view` | Métrica de acesso por requerimento |
| R12 | O detalhe só abre para conteúdo **não futuro**; caso contrário redireciona para `/requerimentos` | `RequerimentoController@view` | Não há vazamento de conteúdo agendado |
| R13 | O requerente (`councilmen_id`) é **opcional** | `ContentRequest.php:240-262` + `ContentController.php:657` | Um requerimento pode ser publicado sem vereador associado |
| R14 | Somente vereadores com `status = true` são oferecidos no formulário | `ContentController.php:204` | Vereador com mandato encerrado não aparece no select (mas é preservado na edição) |
| R15 | A listagem do CMS lê a **view SQL**, sem filtro de `status` nem de `publication_date` | `DatatableController@get_requerimentos` | Rascunhos e conteúdos futuros aparecem no painel |
| R16 | O módulo nasce **desligado** (`modulos.status = false`) e é ligado em `/cms/modulos` | `ModulosController` | Operação/adminstração controla a exibição do módulo |
| R17 | A abertura da listagem no CMS incrementa `menu_orders.clicks` | `ContentController@returnIndex` → `menuClicks('requerimentos')` | Telemetria de uso |

## 2. Normalização da busca textual

O termo digitado passa por `clean_slug()`
(`v6/app/Helpers/functions.php:823`), que troca acentos por letras simples
(`á→a`, `ç→c`, …) e remove tudo que não for `[A-Za-z0-9. ]`. Depois é usado
em um `ilike` com `%` ao redor, dentro de um `translate()` que normaliza o
**conteúdo** do banco:

```php
$newTXT = clean_slug($request->texto);
$contentsQuery->where(function ($q) use ($newTXT) {
    $q->whereRaw(
        "translate(lower(title), ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike ?",
        array('%' . $newTXT . '%')
    )->orWhereRaw(
        "translate(lower(text),  ' ''àáâãäéèëêíìïîóòõöôúùüûçÇ', '--aaaaaeeeeiiiiooooouuuucc') ilike ?",
        array('%' . $newTXT . '%')
    );
});
```

Pontos de atenção:

- O `translate()` é **hardcoded** e precisa incluir **toda** letra acentuada
  que possa aparecer no conteúdo. Uma letra ausente quebra a correspondência
  daquela palavra (ex.: `ü`, `ñ` não estão na lista).
- O segundo argumento do `translate()` é uma string de mesmo comprimento do
  primeiro; um erro de contagem gera `PostgreSQL: unterminated quoted string`
  ou mapeamento incorreto.
- O `clean_slug()` **não** remove a letra `ç` do texto digitado de forma
  idêntica ao `translate()` do banco (`ç→c` vs `ç→c` — ok), mas **não**
  translada `ñ` (o `clean_slug` troca `ñ→n`; o `translate()` do banco **não**
  inclui `ñ`), criando uma assimetria: “mañana” virá “mana” na busca, mas o
  conteúdo “mañana” permanece “mañana” no banco e **não** é encontrado.

## 3. Redimensionamento de slug (colisão de slug) e o autor

O slug do conteúdo é gerado a partir do título no `store()`:

```php
$verificaSlug = Content::where('slug', str_slug($request->get('title')))->get();
$maxid = Content::max('id') + 1;
if (count($verificaSlug) > 0) $slug = $slug . "-" . $maxid;
```

No `update()` (`v6/app/Http/Controllers/Prefecture/ContentController.php:973-991`)
há duas estratégias de desambiguação:

- Se a categoria pai for a **raiz** (`$categoria_pai == 1`) e o slug mudou, o
  slug colidente vira `{slug}-{id}`.
- Caso contrário, o slug colidente vira `pub{id}-{slug}`.

Esse é o mecanismo que garante a unicidade da URL
`/requerimentos/{categoria}/{slug}` usada por `requerimentos.view`. O
`slug1` da URL é a **categoria ativa** (não a categoria real do conteúdo) — o
que torna a URL estável mesmo quando o conteúdo é movido de categoria.

Sobre o **autor**: o CMS permite requerimento sem vereador, mas a coluna
`requerente` da view SQL é preenchida pelo `LEFT JOIN` em `councilmen`; se o
vereador for excluído logicamente (`deleted_at` não nulo), o `LEFT JOIN` devolve
`NULL` e o painel exibe `-`.

## 4. Armadilhas e riscos técnicos

| # | Armadilha | Onde | Impacto |
|---|---|---|---|
| A1 | `whereRaw` com **`translate()` hardcoded** | `RequerimentoController@show` | Busca falha silenciosamente para acentos não mapeados |
| A2 | Montagem de `whereRaw` concatenando `clean_slug($request->texto)` **direto na string** — sem binding `?` — é o padrão usado em `Portal\AtasEPontosController` (`v6/app/Http/Controllers/Portal/AtasEPontosController.php:80`): `... ilike '%" . $newTXT . "%'"` | `AtasEPontosController` (não neste módulo) | **Injeção SQL potencial** naquele controller. Em `RequerimentoController` o binding `?` **é** usado — este módulo está seguro nesse ponto; a lição é não copiar o padrão do `AtasEPontosController` |
| A3 | Duplicação do bloco de filtros/Ajax em **6** temas (`modelo1`…`modelo6`) | `*/requerimentos/show.blade.php` | Correção de bug exige editar 6 arquivos; risco de divergência |
| A4 | `view()` **não valida a categoria** — busca o `Content` só por `slug` | `RequerimentoController@view` (`Content::where('slug', $slug2)->first()`) | Um slug de conteúdo de **outro** módulo pode ser aberto sob `/requerimentos/{cat}/{slug}` |
| A5 | `row_number()` na view não é **estável** entre requisições | view `requerimentos` (`value`) | A coluna `value` não pode ser usada como identificador nem em ordenações paginadas |
| A6 | A view SQL **não** filtra `status` nem `publication_date` | view `requerimentos` | CMS mostra rascunhos e conteúdo futuro; portal não |
| A7 | Portal e CMS usam **conjuntos diferentes** de categorias: portal = filhas diretas (`whereHas`); view = toda a árvore (`WITH RECURSIVE`) | `RequerimentoController@show` vs. view SQL | Conteúdo em sub-subcategorias aparece no CMS e **não** no portal |
| A8 | Parâmetro `texto` concatenado na URL **sem `encodeURIComponent`** | JS de `show.blade.php` | Termos com `&` ou `#` quebram a requisição |
| A9 | `str_limit()` (Laravel ≤5.5) | `list.blade.php` | Quebra na v7 (Laravel 11/12) |
| A10 | `slug1` da URL é ignorado na validação | `RequerimentoController@view` | Parâmetro decorativo |
| A11 | `contents.councilmen_id` está em uma tabela **genérica** compartilhada por todos os módulos de conteúdo | `v6/database/migrations/2026_06_12_100000_add_councilmen_id_to_contents_table.php` | Requerimentos é o único consumidor; a coluna “suja” o modelo genérico |
| A12 | A view SQL é mantida por **4 migrations** com semânticas diferentes | migrations `2024_10_08`, `2025_08_12`, `2026_06_09`, `2026_06_12` | `migrate:rollback` parcial deixa a view em estado inconsistente; o `down()` da última migration não recria a coluna `requerente` |
| A13 | `slug` do conteúdo pode colidir com slugs de **outros** módulos (a verificação é global: `Content::where('slug', ...)`) | `ContentController::store` | A URL pública fica ambígua entre módulos |
| A14 | `publication_date` é `timestamp(0) without time zone`, mas a view faz `::timestamp with time zone` | view `requerimentos` | Conversão depende do fuso do servidor PostgreSQL; o `to_char` pode exibir o dia errado perto da meia-noite |
| A15 | O `index()` do portal depende da existência da raiz `requerimentos` | `RequerimentoController@index` | Ambiente novo sem seed quebra o portal com redirect para `/` |
| A16 | Permissões `requerimentos.*` não existem para o papel `administrador` | `roles.permissions` no banco | Somente `super-admin` opera o módulo |

## 5. Acoplamentos

### Dependências (o módulo depende de)

| Alvo | Tipo | Motivo |
|---|---|---|
| `contents` + `category_contents` | schema | Armazenamento e árvore de categorias |
| `councilmen` | schema | Requerente |
| `archives` | schema | Anexos |
| `Content`, `CategoryContent`, `CityCouncilor`, `Archive` | models | Eloquent |
| `has_access()` (`v6/app/Library/Helper/appHelpers.php:22`) | helper | Permissões |
| `menuClicks()` (`v6/app/Helpers/functions.php:865`) | helper | Telemetria de menu |
| `clean_slug()` (`v6/app/Helpers/functions.php:823`) | helper | Normalização da busca |
| `translateMonth()` (`v6/app/Helpers/functions.php:93`) | helper | Rótulos dos meses no filtro |
| `portal.components.view_pdf.files-list` | view | Download/embed dos anexos |
| `Configuration.key = 'layout'` | config | Escolha do tema do portal |
| PostgreSQL | banco | View SQL, `translate`, `to_char`, `EXTRACT` |

### Dependências de outros módulos (o módulo é usado por)

| Módulo | Relação |
|---|---|
| `vereadores` | `contents.councilmen_id` referencia `councilmen.id` |
| Menu do portal (`menu_items`) | Item “Requerimentos” com `category_contents_id = 37` |
| Dashboard / `menu_orders` | Telemetria de acesso ao módulo |

## 6. Código duplicado / copiado

| Bloco | Onde se repete |
|---|---|
| Filtros + Ajax | `modelo1`…`modelo6` → `*/requerimentos/show.blade.php` |
| Menu lateral | `nav.blade.php` (modelo1, 2, 5, 6) e `pub-nav.blade.php` (modelo3, 4) |
| Bloco de listagem | `list.blade.php` idêntico nos 6 temas |
| Padrão de view SQL | `requerimentos`, `sessoes`, `transmissoes`, `pauta-do-dia`, `atas-e-pontos` |
| Padrão de busca com `translate()` | `RequerimentoController@show` e `AtasEPontosController@show` (com implementação insegura) |

## 7. Divergência v5 → v6 (específica deste módulo)

O módulo **não existe na v5**: não há rota, migration, model, view, seed ou
controller de requerimentos em `v5/`. A única ocorrência de
“requerimento” na v5 é o módulo **Requerimento de Diárias**
(`v5/routes/basepro.php:186`, `v5/routes/web.php:189`), que é outro produto
(requisição de diárias de viagem) com tabelas próprias (`req_diarias`) e não
tem relação funcional com os requerimentos de vereador.

O mapa completo v5 → v6 está em `08-GAPS-E-DIVIDAS.md`.
