# Páginas Estáticas — Regras de Negócio e Portabilidade

## 1. Regras não óbvias

| # | Regra | Onde | Consequência observável |
|---|---|---|---|
| S1 | A página é servida na **raiz do domínio** (`/{slug}`) | `v6/routes/basepro.php:548` e o "Link Permanente" do CMS (`url('/') . '/' . $slug`) | O menu do portal e os links internos apontam para `/{slug}`, não `/pagina/{slug}` |
| S2 | `status_create = 1` marca **página de sistema** e bloqueia a exclusão | `StaticPageController::destroy` (`:361-398`) e `deleteByLote` (`:42-66`) | "Contato", "Ouvidoria" etc. não podem ser removidos pelo CMS |
| S3 | A exclusão em lote só considera páginas com `status_create` falso ou nulo | `deleteByLote` | Páginas de sistema são ignoradas silenciosamente |
| S4 | A exclusão de uma página remove os `menu_items` vinculados | `destroy` | O item de menu some do portal |
| S5 | `is_external = true` faz o portal redirecionar para `links.link` | `PortalController::staticShow` (`:289-292`) | A página nunca é renderizada; o clique **é** contabilizado |
| S6 | Slugs reservados são interceptados por um `switch` | `PortalController::staticShow` (`:294-300`) | `contato`, `ouvidoria`, `manual-de-navegacao`, `solicitar-informacao`, `acompanhe-sua-solicitacao`, `relatorio-estatistico` nunca exibem o conteúdo da página estática |
| S7 | Uma página inexistente devolve **200** com "PÁGINA NÃO ENCONTRADA" | `staticShow` (`:307-309`) + `portal/statics/show.blade.php` | Não há 404; monitoração e SEO ficam mais difíceis |
| S8 | `layout` decide largura da coluna e presença da barra lateral | `portal/statics/show.blade.php` | `layout = 1` -> coluna estreita + sidebar; `layout = 2` -> página cheia |
| S9 | O upload da galeria gera nome `img_{YmdHi}{4 aleatórios}.{ext}` e **não** guarda título nem descrição | `StaticPageController::ajaxUploadFiles` (`:202-232`) | A galeria não tem metadados editáveis |
| S10 | A galeria é gravada em `storage/statics/{slug}/galeria/` | `ajaxUploadFiles` | Caminho diferente do padrão `storage/content/...` usado pelos conteúdos |
| S11 | A exclusão de arquivo da galeria remove o arquivo do disco **e** o registro | `ajaxDeleteFilesOfGallery` (`:182-200`) | Sem "lixeira" |
| S12 | O slug é desambiguado com `-<dmyHis>` quando colide com outra página ou com uma `category_contents.slug` | `StaticPageRepository::structureUpInsert` | A URL exibida no formulário pode mudar após o salvamento |
| S13 | Slugs `diario-oficial`, `requerimento-de-diarias`, `portal-da-transparencia` e `licitacoes` são recusados pela validação | `ConditionalRequiredSlug::passes` | Erro "URL já existente" |
| S14 | `is_external` exige `link` preenchido | `ConditionalRequiredLink` | Validação condicional implementada por `Rule` própria |
| S15 | O contador de cliques é incrementado em **toda** visita, inclusive nas que terminam em redirecionamento | `staticShow` (`:286`) | Métrica conta redirecionamentos e slugs reservados |
| S16 | A página **não** possui data de publicação nem status de rascunho | Modelo `static_pages` | Uma página é publicada assim que é salva |

## 2. Acoplamentos

### Dependências

| Depende de | Tipo | Onde |
|---|---|---|
| `links` | dados | Link externo e vídeo |
| `archives` | dados | Anexos e galeria |
| `menu_items` | dados | Entrada de menu do portal |
| `configurations` | configuração | `layout` (template do portal) |
| `resources` / `modulos` | permissões | `pagina-estatica.create/edit/delete` |
| `ArchiveController` (rotas `galeria.*`) | código | Exclusão de arquivo chamada pelo JS da galeria |
| `PortalController` | código | Renderização pública e slugs reservados |
| `ContentController` | **nada** | Nenhuma dependência (módulo independente) |

### Dependentes

| Depende de | Onde |
|---|---|
| Menu do portal | `menu_items.static_pages_id` |
| Busca global do portal | `v6/public/assets/js/custom/utilities/helpers/search.js` |
| Dashboard | `v6/tests/Feature/DashboardConteudosVisitadosWidgetTest.php` (contadores de conteúdo) |
| Rotas de contato/ouvidoria/acesso-rapido | slugs reservados |

## 3. Armadilhas

### 3.1 Rota duplicada com o mesmo nome

`v6/routes/web.php:227` e `v6/routes/basepro.php:548` registram `static.show`.
A segunda (mapeada depois, em `RouteServiceProvider::mapBaseproRoutes()`)
sobrescreve o nome, e `route('static.show', 'x')` gera `http://localhost/x`.

Consequências:

- o breadcrumb (`v6/routes/breadcrumbs.php:189`) e o botão "Acessar" do CMS
  geram a URL da **raiz**, e não `/pagina/x`;
- qualquer consumidor que espere `/pagina/{slug}` quebra;
- a rota da raiz **não** tem o middleware `check.site.manutention` — páginas
  estáticas continuam acessíveis com o portal em manutenção.

### 3.2 `catch-all` na raiz compete com as demais rotas do portal

`Route::get('/{slug}', ...)` é registrado **após** as rotas de
`v6/routes/web.php` (o `RouteServiceProvider` mapeia `web.php` antes de
`basepro.php`), então as rotas específicas vencem. Mas qualquer URL não
mapped — inclusive erros de digitação — cai em `staticShow` e devolve a
página "PÁGINA NÃO ENCONTRADA" com HTTP 200, mascarando 404 reais.

### 3.3 `$_SERVER['REQUEST_URI']` dentro do repository e da regra

`StaticPageRepository::structureUpInsert` e
`ConditionalRequiredSlug::passes` descobrem o `id` da página pelo **último
segmento da URI**:

```php
$caminho = $_SERVER['REQUEST_URI'];
$partesDaUrl = explode("/", $caminho);
$id =  end($partesDaUrl);
```

Na rota de criação (`POST /cms/pagina-estatica`) o último segmento é
`pagina-estatica`, não um id — a comparação com `$static_page['id']` fica
sempre verdadeira e a slug é sempre desambiguada com timestamp. Em
`ConditionalRequiredSlug`, a mesma lógica impede reutilizar a própria slug
na edição via `/cms/pagina-estatica/editar/{id}` (o id é o último segmento
nessa URL, o que funciona) mas falha em rotas com sufixo.

### 3.4 Mass assignment do Request inteiro

`StaticPageRepository::upInsert` faz:

```php
$data = $this->structureUpInsert($this->dataTrait($request), $id);
return $this->getModel()->updateOrCreate(['id' => $id], $data);
```

`structureUpInsert` devolve o **Request** (que implementa `ArrayAccess`).
O `fill()` do Eloquent respeita o `$fillable` do modelo:

```php
protected $fillable = [
    'key', 'title', 'text', 'access', 'slug', 'status_create',
    'galery', 'layout', 'is_external'
];
```

Consequências:

- `is_external` **é** gravado na criação só porque está no `$fillable` e o
  controller de `store` não faz `merge` explícito (ao contrário de
  `update`) — dependência implícita e frágil;
- qualquer campo futuro enviado no formulário que passe a estar no
  `$fillable` é gravado automaticamente;
- `clicks` e `id` **não** estão no `$fillable` (o id é atribuído pela
  sequência do banco).

### 3.5 `updateOrCreate(['id' => null])` na criação

`upInsert($request, $id = null)` chama `updateOrCreate(['id' => null], $data)`.
Como `id` não é preenchível, o atributo é descartado e o Postgres usa a
sequência — funciona, mas é fragile (qualquer mudança no `$fillable` que
inclua `id` quebraria a criação).

### 3.6 `edit()` ignora o parâmetro do route model binding

```php
public function edit($staticPage)
{
    $content = StaticPage::find($staticPage);
```

O controller não usa `Route::modelBinding`; a busca é manual e o
parâmetro é posicional.

### 3.7 Rota `pagina-estatica.show` aponta para um método inexistente

`Route::resource('pagina-estatica', ...)` gera
`GET /cms/pagina-estatica/{pagina_estatica}` -> `StaticPageController@show`,
mas **não existe** o método `show` no controller -> erro 500.

### 3.8 Slugs duplicados no banco

Consulta ao banco `prefeituras_v5`:

```
 id |         title           |           slug            | status_create | clicks
----+------------------------+--------------------------+---------------+--------
  6 | Manual de Navegação     | manual-de-navegacao       | t             |
  8 | Manual de Navegação     | manual-de-navegacao       | f             |     17
```

Há **duas** páginas com a mesma slug. `staticShow` usa `->first()` e
devolve sempre a id 6 (a de sistema) — a página id 8 (17 cliques) é
**inalcançável** pela URL. A unicidade da slug é garantida apenas pela
validação, que usa o mesmo `$_SERVER['REQUEST_URI']`.

### 3.9 View SQL sem a coluna `galery`

Detalhado em `02-MODELAGEM.md`: a coluna foi removida da view em
`2024_10_10_120559_update_view_static_pages.php`, mas o JS do CMS ainda
consome `data.galery` -> o botão de galeria fica sempre desabilitado.

### 3.10 Endpoint de exclusão de arquivo divergente

O JS `archives-pages.js` chama `POST /cms/galeria/delete/{id}`
(`ArchiveController@deleteArchives`), enquanto a rota do
`StaticPageController` (`ajax.delete.static.files.gallery`) espera
`archive_id` e `content` no corpo e **não é usada** por esse JS.

### 3.11 Sem `SoftDeletes` na leitura via `DB::table`

`get_paginas_estaticas` usa `DB::table('paginas_estaticas')` — mas a view já
filtra `deleted_at IS NULL`, então o resultado está correto.

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

| Trecho | v5 | v6 | Observação |
|---|---|---|---|
| `StaticPageController` | `v5/app/Http/Controllers/Prefecture/StaticPageController.php` | `v6/app/Http/Controllers/Prefecture/StaticPageController.php` | Diferença de **40 linhas**: a v6 acrescenta `sanitizeLink()` e aplica-o em `store` e `update` |
| `StaticPage` (model) | `v5/app/Http/Model/Prefecture/StaticPage.php` | `v6/app/Http/Model/Prefecture/StaticPage.php` | **Idêntico** (0 linhas de diferença) |
| `StaticPageRequest` | `v5/app/Http/Requests/StaticPageRequest.php` | `v6/app/Http/Requests/StaticPageRequest.php` | Idêntico |
| `StaticPageRepository` | `v5/app/Repositories/Eloquent/Prefecture/StaticPage/StaticPageRepository.php` | idem | Diferença de 3 linhas: a v5 só considerava conflito de slug com categorias **raiz** (`whereNull('category_contents_id')`); a v6 considera **qualquer** `category_contents` |
| `PortalController::staticShow` | `v5/app/Http/Controllers/Portal/PortalController.php` | `v6/app/Http/Controllers/Portal/PortalController.php` | O arquivo inteiro difere (~280 linhas), mas `staticShow` é equivalente |

## 5. Onde a v6 divergiu da v5

| Tema | v5 | v6 | Impacto |
|---|---|---|---|
| Link externo | gravado cru | `sanitizeLink()` (remove `https://` de rotas internas) | Evita `https:///rota` |
| `is_external` | Já existia (mesma migration e mesma coluna no banco) | Idem | **Sem mudança** — o `is_external` **não** foi introduzido na v6 |
| Demais métodos | — | — | Sem alteração funcional relevante |

O módulo Páginas Estáticas é, entre os três documentados, o **mais estável**
entre v5 e v6.

## 6. Recomendações de portabilidade

1. Consolidar as duas rotas `static.show` em uma só (`/paginas/{slug}`) e
   manter `/{slug}` como **redirect 301** explícito, com nome de rota
   único.
2. Devolver **404** quando a página não existir (em vez de 200 com
   "PÁGINA NÃO ENCONTRADA").
3. Passar o id da página explicitamente ao repository/regra, em vez de
   deduzir de `$_SERVER['REQUEST_URI']`.
4. Listar campos explícitos no `structureUpInsert` em vez de passar o
   Request inteiro.
5. Adicionar índice único em `static_pages.slug` (parcial, soft delete) e
   corrigir os duplicados existentes (ids 6 e 8).
6. Remover o método `show` do resource ou implementá-lo.
7. Repor a coluna `galery` na view `paginas_estaticas` (ou passar o dado
   por outro canal) para o botão de galeria funcionar.
8. Unificar a rota de exclusão de arquivo (`galeria.delete` vs
   `ajax.static.delete.files.gallery`).
9. Usar `Storage::disk()` e nome de arquivo determinístico para a galeria.
