# Órgãos — Regras de Negócio e Portabilidade

> Escopo: regras não óbvias, acoplamentos, armadilhas, riscos de performance e divergências v5/v6.

---

## 1. Regras de negócio não óbvias

| # | Regra | Onde está | Efeito observável |
|---|---|---|---|
| R1 | Um órgão é um `Content` com `category_contents_id = 32` | `v6/database/seeds/CategoryContentTableSeeder.php:180-185` | Não existe entidade "Órgão" isolada; apagar a categoria apaga o conceito |
| R2 | A **categoria raiz** é identificada por `category_contents_id IS NULL` | `category_contents.id = 32` tem `category_contents_id` NULL | A view SQL da v5 filtra por isso; a da v6 não |
| R3 | O `slug` da categoria é o único chave de roteamento | `ContentController::genericIndex` / `genericGenerate` | Trocar o slug quebra listagem, formulário, view SQL, API e menu |
| R4 | A URL pública do órgão é `/a-instituicao/orgaos/{slug}` | `v6/routes/web.php:82` + `MenuItemPresenter` (caso `id == 32`) | O prefixo `orgaos` é literal, não derivado |
| R5 | O `slug` do conteúdo é gerado pelo servidor a partir do `title` | `ContentController::generateSlug()` (linha 584), chamada em `store()` (linha 619) | O campo `slug` do formulário é decorativo; a colisão resolve com o sufixo `-<maxid+1>` |
| R6 | A ordenação padrão dos órgãos é `created_at DESC` | view `a_orgaos` (`row_number() OVER (ORDER BY c.created_at DESC)`) | O `order` do `contents` **não** é usado na grade do CMS |
| R7 | A lista de notícias do órgão só mostra itens com `publication_date <= now()` | `PrefectureController@getIndex` (linhas 73-74) | Filtro aplicado **em memória** (Coleção), não no SQL |
| R8 | A agenda do órgão mostra no máximo 5 eventos, ativos e não expirados | `PrefectureController@getIndex` (linhas 75-78) | `status = '1'`, `final_date >= date('Y-m-d')`, `orgaos_id = <id>` |
| R9 | Cada visita à página do órgão incrementa `contents.clicks` | `GetInformationsPrefectureTrait::updateClicks` (linha 104) | Alimenta `interacoes` na API e a ordenação `orderByDesc('clicks')` |
| R10 | Excluir um órgão remove os `menu_items` que apontam para ele | `ContentController::destroy` (linhas 1334-1340) e `genericDestroy` (1377-1383) | O flash "Foram excluídos os itens de menu referentes à esse conteúdo." é exibido |
| R11 | O grupo "Órgãos" da API inclui a própria categoria e suas subcategorias | `Swagger\V1\Portal\Controller::getContent()` (`orWhereHas` direto e aninhado) | Se alguém criar subcategorias de "Órgãos", elas entram na API automaticamente |
| R12 | A API ordena por `publication_date DESC, clicks DESC` | `Swagger\V1\Portal\Controller::getContent()` | Órgãos sem `publication_date` (todos os 3 atuais) vão para o fim |
| R13 | A listagem lateral de notícias só aparece se `modulos['noticias']` for verdadeiro | `orgaosNews.blade.php` (linha 1) | Se o módulo Notícias for desligado, a seção inteira desaparece |
| R14 | Se `getStatusDisplayNews()['displayNews'] != 0`, a lista é substituída por uma imagem de aviso | `orgaosNews.blade.php` | Configuração global de exibição de notícias |
| R15 | O módulo só aparece no menu lateral do CMS se `modulos.status` **e** `has_access("orgaos.index")` forem verdadeiros | `v6/app/Providers/MenuServiceProvider.php` | Dupla condição; desligar `modulos.status` oculta o item sem remover a permissão |
| R16 | `modulos.status` de `orgaos` **controla também** o select "Órgãos" do formulário de notícias | `v6/resources/views/prefecture/contents/noticias/form.blade.php:76-88` | Com `status = false`, o operador não consegue vincular notícia a órgão |

## 2. Acoplamentos

### 2.1 Dependências de outros módulos

| Módulo | Dependência | Tipo | Quebra se... |
|---|---|---|---|
| **Notícias** | `contents.orgaos_id`; select "Órgãos" no formulário; `Content::orgaos()` | dado + view | `modulos.status` de `orgaos` estiver `false` |
| **Agenda de eventos** | `calendar_events.orgaos_id`; `Content::eventosOrgaos()` | dado | A coluna for removida |
| **Menus do site** | `menu_items.category_contents_id = 32`; `MenuItemPresenter` (caso `id == 32`) | dado + código hardcoded | O presenter for refatorado sem manter o caso 32 |
| **A Instituição** | Mesma rota `/a-instituicao/*` e mesma view `informacoes-instituicao.a-instituicao.show` | rota + view | A rota `a-instituicao/{slug}` for capturada antes de `a-instituicao/orgaos/{slug}` |
| **Módulos (CMS)** | `modulos.status`, `menu_orders.modulo_id = 17`, `resources.modulo_id = 17` | flag | `ModulosServiceProvider` cachear `modulos` sem invalidação |
| **Dados abertos (Swagger)** | `Swagger\V1\Portal\Controller::getContent()` | código compartilhado | O filtro por `orWhereHas` mudar de semântica |
| **Arquivos / galeria** | `archives.contents_id`, `archives.highlight` | dado | — |
| **Transparência fiscal (página estática)** | `OrganizationalStructureController` usa `StaticPage` — **não** usa órgãos | convergência de nomes | Confundir `/estrutura-organizacional` com o módulo Órgãos |

### 2.2 Dependências do módulo em direção a outros módulos

| Quem depende do Órgãos | Como |
|---|---|
| Menu do portal | `MenuItemPresenter::getLink()` monta a URL dos órgãos |
| Formulário de notícias | Select "Órgãos" alimentado por `getContentByCategoryWith('orgaos', ['title as name', 'id as valor'])` |
| Formulário de eventos | `calendar_events.orgaos_id` |
| API de dados abertos | `Swagger\V1\Portal\OrgaoController` |

> Detalhe do select de órgãos no formulário de notícias:
> `ContentController::genericGenerate()` (linha 194) carrega
> `'orgaos' => $this->getContentByCategoryWith('orgaos', ['title as name', 'id as valor'])`.
> A consulta é `Content::with('linksUnique')->whereHas('categoryContents', ...)->get($gets)` — **sem
> `limit`**, ou seja, todos os órgãos e seus links são carregados para popular um `<select>`.

## 3. Armadilhas

### 3.1 SQL e consultas

| # | Armadilha | Local | Impacto |
|---|---|---|---|
| A1 | `DB::table("a_orgaos")->get()` materializa a view inteira no PHP | `v6/app/Http/Controllers/Prefecture/DatatableController.php:259` | Cresce linearmente com a tabela de conteúdos; o Yajra fatia em memória, sem `LIMIT` no SQL |
| A2 | `Content::with(...)->where('secretaria', $slug1)->orderBy('id','DESC')->paginate(6)` é executado **para toda** página de órgão, mas o resultado nunca é usado | `v6/app/Http/Controllers/Portal/PrefectureController.php:38` | Query inútil (retorna vazio para slugs de órgão) em todas as visitas ao portal institucional |
| A3 | `$contentNew->orgaos` (hasMany) é carregado **sem limite** e filtrado em memória | `v6/app/Http/Controllers/Portal/PrefectureController.php:73-74` | A lista de notícias do órgão carrega todas as notícias vinculadas, para então mostrar todas |
| A4 | `getContentByCategoryWith('orgaos', ...)` sem `limit` para popular um `<select>` | `v6/app/Http/Controllers/Prefecture/ContentController.php:194` | Query sem limite no formulário de notícias |
| A5 | `whereRaw()` sem reatribuição | `v6/app/Http/Controllers/Swagger/V1/Portal/OrgaoController.php` | Filtros de `titulo`/`descricao` nunca aplicados (bug funcional; ver `09-API-E-DADOS-ABERTOS.md`) |
| A6 | `caseInsensitiveAccentFreeSearch()` concatena o termo no SQL **sem binding** | `v6/app/Http/Controllers/Swagger/V1/Portal/Controller.php` | `lower(TRANSLATE(...)) like '%termo%'` — o valor vem do usuário; risco de SQL injection mitigado apenas pelo escape do driver |
| A7 | `Content::where('slug', str_slug($title))->get()` para detectar colisão | `v6/app/Http/Controllers/Prefecture/ContentController.php:665` | Carrega todas as linhas com o mesmo slug para testar `count() > 0` |
| A8 | `Content::max('id') + 1` para gerar o sufixo anti-colisão | `v6/app/Http/Controllers/Prefecture/ContentController.php:666` | Não é atômico; duas gravações simultâneas podem colidir |
| A9 | View SQL mantida por `CREATE OR REPLACE VIEW` em migration | `v6/database/migrations/2023_04_10_145838_create_view_to_orgaos.php`, `v6/database/migrations/2024_11_29_093120_update_view_orgaos.php` | Mudança de schema em `contents` quebra a view silenciosamente |
| A10 | `row_number() OVER (ORDER BY c.created_at DESC)` recalculado a cada consulta | view `a_orgaos` | Custo extra desnecessário para um simples índice |
| A11 | `Content::query()->whereHas(...)` executado **duas vezes** (directExists e nestedExists) antes de qualquer filtro | `v6/app/Http/Controllers/Swagger/V1/Portal/Controller.php::getContent()` | Duas queries extras por requisição à API |

### 3.2 Acoplamento de view SQL com PHP

A view `a_orgaos` expõe `value`, `titulo`, `id`, `galeria`, `slug`. Qualquer mudança de nome de coluna
na view quebra o `datatable.js` **silenciosamente** (o `data: 'titulo'` vira `undefined`).
A coluna `value` **não existe** em `contents` — ela é o `row_number()`; quem a consome é o `unshift` de
`runDatatables`.

### 3.3 presenters e hardcodes

```php
// v6/app/Http/Presenters/MenuItemPresenter.php
} elseif ($this->categoryContent->id == 7) {
    return url('a-instituicao/' . $this->categoryContent->slug . $content_slug);
} elseif ($this->categoryContent->id == 32) {
    return url('a-instituicao/' . $this->categoryContent->slug . $content_slug);
}
```

O `id` da categoria está **hardcoded** no presenter. Se o `id` 32 mudar de valor em um ambiente novo (o
seeder usa `updateOrCreate` por `slug`, mas o id é auto-incremento), os links dos órgãos passam a apontar
para `/orgaos/<slug>` — rota inexistente.

O mesmo `id` está hardcoded em `CheckRoutePermission` para casos especiais
(`'diario-config.index'`, `'painel.feriados-municipais'`, etc.), mas **não** para órgãos: lá a permissão é
derivada do segmento da URL.

### 3.4 Sessão, cache e estado global

| Recurso | Local | Risco |
|---|---|---|
| `Cache::rememberForever('modulos')` | `v6/app/Providers/ModulosServiceProvider.php` | O cache só é invalidado por `Cache::pull('modulos')` em `ModulosController::update`. Mudança direta no banco não reflete até o pull |
| `session(['categorySlugSession' => $category])` | `Content::scopeByCategoryContentNews` | Estado em sessão usado dentro de um scope — não afeta o módulo Órgãos, mas mostra o padrão |
| `session(['limit' => $limit])` | `Content::scopeByLimit` | Idem |
| `menuClicks($slug)` | `v6/app/Helpers/functions.php` | Incrementa contadores de clique do menu por `slug` — usado em `returnIndex` e `returnData` |

### 3.5 Autorização permissiva demais (verificado no middleware)

`v6/app/Http/Middleware/CheckRoutePermission.php`:

```php
$openRoutes = [
    'login', 'logout', 'datatable', 'dashboard', 'error', 'ajax', 'info-request-status-update'
];
$resourceRoutes = [
    'edit' => 'update', 'create' => 'store', 'delete' => 'destroy',
];
...
$open = array_where($accessArr, function ($value, $key) use ($openRoutes, $resourceRoutes) {
    if (in_array($value, $openRoutes) || in_array($value, $resourceRoutes)) {
        return true;
    }
});

if (!empty($open)) {
    return true;
}
```

Consequências **para o módulo Órgãos**:

| Rota | Nome | `accessArr` | Resultado |
|---|---|---|---|
| `GET /cms/conteudos/orgaos` | `listar-conteudo` | `['listar-conteudo']` | exige `orgaos.index` (via `hasAccessSegment`) |
| `GET /cms/conteudos/cadastrar/orgaos` | `cadastrar-conteudo` | `['cadastrar-conteudo']` | exige `orgaos.create` |
| `GET /cms/conteudos/editar/orgaos/{id}` | `editar-conteudo` | `['editar-conteudo']` | exige `orgaos.edit` |
| `GET /cms/datatable/orgaos` | `datatable.orgaos` | `['datatable','orgaos']` | `'datatable'` ∈ `$openRoutes` -> **aberta** |
| `POST /cms/conteudo` | `conteudo.store` | `['conteudo','store']` | `'store'` ∈ `$resourceRoutes` -> **aberta** |
| `PUT /cms/conteudo/{id}` | `conteudo.update` | `['conteudo','update']` | `'update'` ∈ `$resourceRoutes` -> **aberta** |
| `DELETE /cms/conteudo/{id}` | `conteudo.destroy` | `['conteudo','destroy']` | `'destroy'` ∈ `$resourceRoutes` -> **aberta** |
| `DELETE /cms/conteudo/delete/{id}` | `generic.conteudo.delete` | `['generic','conteudo','delete']` | `'delete'` ∈ `$resourceRoutes` -> **aberta** |

O caso especial `if ($access == "generic.conteudo.delete") $access = "conteudo.delete";` está **depois**
do `return true` acima, ou seja, é **código inalcançável** para essa rota.

>Ou seja: qualquer usuário logado no CMS — mesmo sem `orgaos.create`/`orgaos.edit`/`orgaos.delete` — pode
> criar, editar e excluir órgãos (e qualquer outro conteúdo), contornando as permissões. Isso é um padrão
> **herdado do CRUD genérico**, não específico do módulo Órgãos, mas o módulo é afetado por ele.

### 3.6 Rotas `Route::any` no portal

`Route::any('orgaos/{slug}', ...)` aceita GET/POST/PUT/PATCH/DELETE. Um `POST` na URL do órgão executa o
mesmo `getIndex()` e devolve a página completa (com incremento de `clicks`).

## 4. Riscos de performance aplicáveis a este módulo

| Cenário | Custo | Observação |
|---|---|---|
| Abrir a página de um órgão com N notícias vinculadas | N linhas carregadas + N renders de `<li>` | Sem paginação e sem `LIMIT` |
| Abrir a grade do CMS de órgãos | Tabela `contents` inteira via view | `DB::table(...)->get()` + Yajra em memória |
| Abrir o formulário de notícias | Todos os órgãos + `linksUnique` carregados | `getContentByCategoryWith('orgaos', ...)` sem `limit` |
| Consumir `GET /api/v1/orgaos` sem `limite` | Todos os órgãos com `archives` (eager load) | `with('archives')` sem `limit` |
| Consumir `GET /api/v1/orgaos?titulo=X` | Mesma carga + 2 queries extras de `exists()` | O filtro é ignorado (bug), então o resultado é maior do que o esperado |

O caso **correto** (referência de padrão) é o da agenda de eventos:
`->where('orgaos_id', $contentNew->id)->limit(5)->get()` — limite aplicado no SQL.

## 5. Código duplicado / copiado entre v5 e v6

| Arquivo | v5 x v6 |
|---|---|
| `v6/app/Http/Controllers/Swagger/V1/Portal/OrgaoController.php` | **Idêntico** (`diff` retorna vazio) — o bug do `whereRaw` foi herdado |
| `v6/app/Http/Resources/Swagger/V1/Portal/OrgaoResource.php` | Idêntico |
| `v6/app/Http/Requests/Swagger/V1/Portal/OrgaoRequest.php` | Idêntico |
| `v6/resources/views/prefecture/contents/orgaos/index.blade.php` | **Idêntico** (`diff` vazio) |
| `v6/resources/views/prefecture/contents/orgaos/create.blade.php` | **Idêntico** (`diff` vazio) |
| `v6/resources/views/prefecture/contents/orgaos/form.blade.php` | **Divergente**: v5 mostra o link de exibição com `url('a-prefeitura/orgaos/'.$content->slug)`; v6 usa `url('a-instituicao/orgaos')` + campo `slug` + `route('a-instituicao-orgaos.index', ...)` |
| `v6/public/assets/js/custom/contents/orgaos/datatable.js` | **Divergente**: v5 monta a URL do portal em JS com guarda `if(data.slug)`; v6 interpola direto `` `/a-instituicao/orgaos/${data.slug}` `` |
| `v6/app/Http/Controllers/Portal/PrefectureController.php` | Divergente: v5 usa o prefixo de rota `a-prefeitura` e a view `informacoes-prefeitura.a-prefeitura.show`; v6 usa `a-instituicao` e `informacoes-instituicao.a-instituicao.show` |
| `v6/app/Http/Controllers/Prefecture/DatatableController.php` | Divergente: v5 lê `DB::table("orgaos")`; v6 lê `DB::table("a_orgaos")` |

## 6. Onde a v6 divergiu da v5

| Tema | v5 | v6 | Provável motivo |
|---|---|---|---|
| Nome da view SQL | `public.orgaos` | `public.a_orgaos` | Padronização com as demais views (`a_cidade`, `a_instituicao`); a migration `2025_05_19_165859_update_orgaos_view.php` **só existe na v5** |
| Filtro `cc.category_contents_id IS NULL` | **presente** na view `orgaos` | **ausente** na view `a_orgaos` | A v6 nunca aplicou esse filtro; a v5 o introduziu em 2025-05-19 ao renomear a view. Efeito: na v5, se existir uma subcategoria de "Órgãos", seus conteúdos **não** aparecem na grade; na v6 aparecem |
| Coluna `slug` na view | adicionada por `2026_03_09_000000_update_views_to_include_slug.php` | adicionada por `2024_11_29_093120_update_view_orgaos.php` | Necessidade do botão "Acessar órgão" na grade |
| Prefixo de rota do portal | `/a-prefeitura/orgaos/{slug}` | `/a-instituicao/orgaos/{slug}` | Rebranding de "A Prefeitura" para "A Instituição" |
| Diretório de views do portal | `informacoes-prefeitura/a-prefeitura/` | `informacoes-instituicao/a-instituicao/` | Mesma razão do rebranding |
| Formulário do CMS | link fixo (`url('a-prefeitura/orgaos/'.$content->slug)`) | campo `slug` + `route('a-instituicao-orgaos.index', ...)` | Padronização com os demais módulos |
| JS da grade | URL montada em JS com guarda `if(data.slug)` | URL interpolada diretamente | Simplificação; perde a proteção contra `slug` vazio |
| Modelo de Órgão | **inexistente** | **inexistente** | — |
| `contents.orgaos_id` | presente | presente | — |
| `modulos.status` de `orgaos` | — | `false` | Default da migration (`'status' => 0`) nunca alterado |

## 7. Portabilidade — o que não pode mudar na reconstrução

1. A **URL pública** `/a-instituicao/orgaos/{slug}` (linkada por menus, por material impresso e por
   terceiros). URLs antigas `/a-prefeitura/orgaos/{slug}` devem responder por redirecionamento 301.
2. A **grade do CMS** precisa continuar exibindo "Acessar órgão" e "Galeria" com as mesmas permissões
   (`orgaos.edit`, `orgaos.delete`).
3. A **exclusão** deve continuar removendo os itens de menu vinculados.
4. A **API** precisa continuar devolvendo `titulo`, `descricao`, `interacoes`, `slug`, `arquivos` no
   envelope `data`.
5. A **contagem de acessos** (`clicks`) alimenta a API e a ordenação — deve ser mantida.
6. O **vínculo notícia↔órgão** e **evento↔órgão** precisam continuar existindo, ainda que o módulo
   esteja desligado por padrão (`modulos.status = false`).
