# Concursos e PSS — Gaps, Dívidas e Regressão v5 → v6

> Este módulo **existe apenas na v5**. A seção "Regressão v5 -> v6" é a parte
> obrigatória deste dossiê.

---

## 1. Regressão v5 -> v6

### 1.1 Veredito

| Aspecto | v5 | v6 | Situação |
|---|---|---|---|
| Controller CMS | `v5/app/Http/Controllers/Prefecture/ConcursoPssController.php` | **ausente** | **Regressão** |
| Controller Portal | `v5/app/Http/Controllers/Portal/ConcursosPssController.php` | **ausente** | **Regressão** |
| Model | `v5/app/Http/Model/Prefecture/ConcursoPss.php` | **ausente** | **Regressão** |
| Form Request | `v5/app/Http/Requests/ConcursoPssRequest.php` | **ausente** | **Regressão** |
| Export (Excel) | `v5/app/Exports/ConcursosPssExport.php` | **ausente** | **Regressão** |
| View de exportação | `v5/resources/views/exports/concursos-pss.blade.php` | **ausente** | **Regressão** |
| Tabela `concursos_pss` | migration `2026_05_27_000000` | **ausente** | **Regressão** |
| View SQL `concursos_pss_view` | migration `2026_05_27_000001` | **ausente** | **Regressão** |
| Rotas CMS | `v5/routes/basepro.php:356-362` + `:423` | **ausentes** | **Regressão** |
| Rotas Portal | `v5/routes/web.php:162-163` | **ausentes** | **Regressão** |
| Breadcrumbs | `v5/routes/breadcrumbs.php:190-198` | **ausentes** | **Regressão** |
| Views CMS | `v5/resources/views/prefecture/concursos-pss/*` (4 arquivos) | **ausentes** | **Regressão** |
| Views Portal | `v5/resources/views/portal/layouts/modelo{1..6}/concursos-pss/*` (18) + `components/concursos-pss/table.blade.php` | **ausentes** | **Regressão** |
| JS do DataTable | `v5/public/assets/js/custom/config/concursos-pss/datatable.js` | **ausente** | **Regressão** |
| **Resource de permissão** | `resources.id = 16`, `slug = 'concurso-pss'`, name "Concursos e PSS" | **não existe** (nem `resources` com `modulo_id` para concursos) | **Regressão** |
| **Menu (`menu_orders`)** | `menu_orders.id = 36`, `slug = 'concurso-pss'`, `clicks = 0` | **não existe** | **Regressão** |
| Entrada no `modulos` | não existe na v5 (tabela `modulos` só na v6) | **não existe** — nenhum dos 56 módulos é concursos/pss | **Lacuna estrutural** |

### 1.2 Detalhamento

**Resource `concurso-pss` (id 16).** Criada por
`v5/database/migrations/2026_05_27_000002_insert_concurso_pss_menu_and_resource.php`:

```php
if (!DB::table('resources')->where('slug', 'concurso-pss')->exists()) {
    DB::table('resources')->insert([
        'slug'        => 'concurso-pss',
        'name'        => 'Concursos e PSS',
        'description' => 'Módulo de Concursos Públicos e Processo Seletivo Simplificado',
        'created_at'  => now(),
        'updated_at'  => now(),
    ]);
}
```

Estado verificado na base v5 (`SELECT id, slug, name FROM resources WHERE slug ILIKE '%concurso%'`):

```
 id |     slug     |       name       | status_create
----+---------------+------------------+---------------
 16 | concurso-pss | Concursos e PSS  |
```

Sequência completa na v5: 13 `obras`, 14 `terceirizados`, 15 `estagiario`,
**16 `concurso-pss`**, 17 `fila-espera-sus`, 18 `usuario`, 19 `perfil`,
20 `permissao`. **Nenhuma dessas resources existe no banco da v6** — a v6
não tem `obras`, `terceirizados`, `estagiario`, `concurso-pss` nem
`fila-espera-sus`.

**Menu inserido pela migration `2026_05_27_000002`.** A mesma migration cria
a linha de contador:

```
 id |     slug     | clicks |     created_at      |     updated_at      | deleted_at
----+---------------+--------+---------------------+---------------------+------------
 36 | concurso-pss |      0 | 2026-10-21 08:59:07 | 2026-10-21 08:59:07 |
```

- A v6 **não** tem `menu_orders.slug = 'concurso-pss'`. Na v6 o par
  `menu_orders`/`resources` passou a referenciar `modulos`
  (`v6/database/migrations/2024_11_04_094157_add_modulo_id_to_menu_order_table.php`
  e `v6/database/migrations/2024_11_05_104026_add_modulo_id_to_resources_table.php`),
  e **nenhum** registro foi criado para o módulo de concursos.
- Na v5, essa linha de `menu_orders` é **funcionalmente inerte** para este
  módulo: `menuClicks('concurso-pss')` nunca é chamado (ver G3).

**Menus do portal.** A v5 tem o item de menu estático "Concurso Público"
(`menu_items` id 35 em `menus_id = 1` e id 59 em `menus_id = 2`), apontando para
`category_contents_id = 18` — uma **página de conteúdo**, não o módulo. Ele
continua existindo na v6 como conteúdo estático. É a "página de Concurso Público"
que o módulo pretendia substituir.

**Link morto na v6.** O rodapé "mais procurados" da v6 mantém o texto
"Concurso público" apontando para `#`:
`v6/resources/views/portal/layouts/modelo6/layout/footer/mais-populares.blade.php:26`.

### 1.3 Consequências

| Consequência | Detalhe |
|---|---|
| **Cidadão perdeu a funcionalidade** | Não há mais listagem pública de concursos nem download de editais. Na prática, o município voltou a depender de páginas estáticas. |
| **Operador perdeu o cadastro** | Não há tela para inserir certames; os PDFs de edital passam a ser publicados como conteúdo genérico. |
| **Permissões órfãs** | As chaves `concurso-pss.*` não existem em nenhum papel da v5 (verificado com `LIKE '%concurso-pss%'` em `roles.permissions`). Recriar o módulo na v7 exige recriar `modulos` + `resources` **e** revogar as permissões aos papéis. |
| **Dados preservados** | A tabela v5 tem **0 linhas** (`SELECT count(*) FROM concursos_pss`), portanto não há migração de dados a fazer para este ambiente — mas o procedimento deve existir para outros ambientes. |
| **View SQL órfã** | `concursos_pss_view` existe apenas na v5 e depende da tabela; na v7 deve ser criada junto com a tabela ou eliminada em favor de paginação server-side. |

### 1.4 Mapa v5 → v6 → v7

| Artefato v5 | Destino na v6 | Destino na v7 |
|---|---|---|
| `concursos_pss` (tabela) | — | `concursos_pss` (schema novo, `arquivo` como `json`) |
| `concursos_pss_view` | — | opcional; preferir paginação server-side |
| `resources` id 16 | — | `modulos` (slug `concursos-pss`) + `resources` (slug `concurso-pss`, `modulo_id`) |
| `menu_orders` id 36 | — | `menu_orders` com `modulo_id` (ou item de menu real em `menu_items`) |
| `ConcursoPssController` (CMS) | — | `App\Http\Controllers\Prefecture\ConcursoPssController` + Policy |
| `ConcursosPssController` (Portal) | — | `App\Http\Controllers\Portal\ConcursoPssController` |
| `ConcursoPss` (model) | — | `App\Models\ConcursoPss` |
| `ConcursoPssRequest` | — | `Store`/`UpdateConcursoPssRequest` |
| `ConcursosPssExport` | — | `App\Exports\ConcursoPssExport` (`FromView` + `ShouldAutoSize`) |
| `exports/concursos-pss.blade.php` | — | template único com cabeçalho sempre presente |
| `prefecture/concursos-pss/*` (4 views) | — | componentes Blade `<x-concurso-pss.*>` |
| `portal/layouts/modelo{1..6}/concursos-pss/*` (18 views) | — | 3 views únicas + layout ativo |
| `portal/components/concursos-pss/table.blade.php` | — | `x-concurso-pss.table` |
| `datatable.js` do módulo | — | DataTable server-side ou Inertia |
| Rotas `concurso-pss.*` / `concursos-pss.*` | — | mesma URL e mesmos nomes |
| `menu_items` id 35/59 (página estática) | mantido | **decidir**: remover em favor do módulo, ou manter como atalho |

## 2. Bugs e erros conhecidos

| # | Bug | Evidência | Severidade |
|---|---|---|---|
| B1 | **Duplo flash** no `store`/`update`: o `catch` mostra a mensagem da exceção e a execução segue para o `flash()` genérico, que a **sobrescreve**. A causa real nunca chega ao operador. | `v5/app/Http/Controllers/Prefecture/ConcursoPssController.php:44-49` e `:91-96` | Média |
| B2 | **Erro engolido**: `catch (\Exception $e)` sem `report()` — nada é registrado em log. | idem | Média |
| B3 | **Arquivo órfão no disco**: `destroyArquivo()` remove a entrada do JSON mas não apaga o PDF (`FileTrait::deleteFile()` nunca é chamado). | `ConcursoPssController.php:122-140` | Média |
| B4 | **`<tbody>` por linha** na view de exportação — HTML inválido; o Excel/PDF pode renderizar de forma inconsistente. | `v5/resources/views/exports/concursos-pss.blade.php` (bloco `@foreach` dentro de `<tbody>`) | Média |
| B5 | **`colspan="5"`** na mensagem "Nenhum registro encontrado" — a tabela tem 8 colunas. | idem | Baixa |
| B6 | **XLSX/CSV sem cabeçalho** — o bloco com título e data só é renderizado quando `$pdf` está definido. | idem (`@if(isset($pdf))`) | Média |
| B7 | **Content-Type inválido** `plain/txt` (deveria ser `text/plain`). | `v5/app/Http/Controllers/Portal/ConcursosPssController.php:106` | Baixa |
| B8 | **Paginação em memória** — `->get()` sem limite; o `total()` e o `slice()` são feitos em PHP. | `ConcursosPssController.php:45,114-127` | Alta (escala) |
| B9 | **`%`/`_` não escapados** no `LIKE` da busca. | `ConcursosPssController.php:30-34` | Média |
| B10 | **Função SQL sobre a coluna** (`translate(lower(descricao))`) impede índice; busca é varredura sequencial. | idem | Média (escala) |
| B11 | **Normalização assimétrica**: `clean_slug()` remove `ñ`→`n`, mas o `translate()` do SQL não cobre `ñ`. | `v5/app/Helpers/functions.php:780` vs `ConcursosPssController.php:31` | Baixa |
| B12 | **Sem escape de HTML no JS do DataTable**: `${data}` interpolado direto em template literal. | `v5/public/assets/js/custom/config/concursos-pss/datatable.js` | Média (XSS armazenado) |
| B13 | **Slug recalculado a cada edição** — links públicos publicados quebram silenciosamente. | `v5/app/Http/Model/Prefecture/ConcursoPss.php:36-38` | Alta |
| B14 | **Lista de tipos hardcoded** — certames com tipo fora dos 3 valores não podem ser filtrados. | `v5/app/Http/Controllers/Portal/ConcursosPssController.php:39-43` | Média |
| B15 | **`destroyArquivo` por índice posicional** — remover o 1º arquivo renumera os demais e invalida botões já renderizados. | `ConcursoPssController.php:122-140` | Média |
| B16 | **`$isUpdate` não usado** no Form Request. | `v5/app/Http/Requests/ConcursoPssRequest.php:16` | Baixa |
| B17 | **`prepareData()` sem type hint** de `Request`. | `ConcursoPssController.php:111` | Baixa |
| B18 | **Rota `destroy-arquivo` exige permissão inexistente**: o `CheckRoutePermission` resolve o nome `concurso-pss.destroy-arquivo`, que **não** existe entre as permissões geradas (`index`, `create`, `edit`, `delete`, `inactive`, `inactivate`, `restore`, `show`). Somente `super-admin` consegue remover anexos. | `v5/app/Http/Middleware/CheckRoutePermission.php:41-88` + `v5/app/Http/Controllers/Prefecture/DatatableController.php:508` | Alta |
| B19 | **Slug duplicado** não é barrado: a coluna não tem índice único. | migration `2026_05_27_000000` | Baixa |
| B20 | **`validade` gravada com `date('Y-m-d', strtotime(...))`** sem verificar o retorno de `strtotime` (se `false`, `date()` gravaria `1970-01-01`). Na prática a validação `date_format:d/m/Y` já rejeita datas inexistentes (ex.: `31/02/2026`), então o risco é teórico — mas a conversão continua frágil. | `ConcursoPssController.php:111-117` | Baixa |

## 3. Funcionalidades incompletas ou mortas

| # | Item | Situação |
|---|---|---|
| M1 | `menu_orders.slug = 'concurso-pss'` (id 36) | **Morto**: `menuClicks()` nunca é chamado para este slug; `clicks` fica em `0` para sempre. |
| M2 | Coluna `slug` |indexada e usada como chave de URL, mas **sem índice único** — consultas por slug fazem varredura. |
| M3 | `active` / `inactivate` injetados por `struct_views()` | **Sem efeito**: o módulo não tem controle de ativação/desativação; os flags são gerados e ignorados. |
| M4 | Colunas `vagas_ofertadas`, `vagas_preenchidas`, `validade`, `arquivo` | **Ausentes** no DataTable do CMS (a view as traz, o JS não as renderiza). |
| M5 | `deleteAll` do `runDatatables` | Não habilitado — exclusão em lote indisponível. |
| M6 | Filtro por `situacao` e por `numero` (edital) | **Não existem** no portal, apesar de serem campos indexáveis e de a busca ser restrita à descrição. |
| M7 | `activity()` / `activity_log` | **Ausente**: nenhuma ação do módulo é auditada. |
| M8 | Modo de manutenção (`active.landing`) | Registrado no kernel, não aplicado ao módulo. |
| M9 | Seed / dados de exemplo | Nenhum; a tabela está vazia na base v5. |
| M10 | Testes automatizados | Nenhum (`v5/tests/Feature/` só tem `ExampleTest.php`). |

## 4. Riscos de dados (migração de schema)

| Risco | Impacto | Mitigação |
|---|---|---|
| `arquivo` é `text` com JSON em 3 formatos legados | Alto | Script de saneamento antes do `CAST(arquivo AS json)`. |
| `slug` pode estar `NULL` (linha inserida antes do hook) | Médio | Recalcular com `Str::slug("{tipo}-{numero}-{ano}-{id}")` e criar índice único. |
| `timestamp(0)` sem timezone | Médio | Converter para `timestampTz` assumindo `America/Sao_Paulo`. |
| `vagas_preenchidas` pode ser maior que `vagas_ofertadas` | Baixo | Saneamento antes de criar o `check`. |
| Sem backup dos PDFs | Alto | Os arquivos vivem em `storage/app/public/docs`, **fora** do banco; a migração de schema não os move. Planejar a cópia para o disco privado da v7. |
| `deleted_at` preenchido em registros | Baixo | Preservar o soft delete na migração. |

## 5. Perguntas em aberto para o negócio

| # | Pergunta | Impacto na v7 |
|---|---|---|
| Q1 | O **slug** pode mudar quando o certame é editado, ou deve ser congelado na publicação? | Define se há redirecionamento de URL antigo. |
| Q2 | Quais são os **tipos** válidos? Os 3 hardcoded cobrem a operação real? | Define o enum e se `tipo` vira FK. |
| Q3 | Quais são as **situações** válidas e há uma lista fechada? | Define o enum de `situacao` e a necessidade de migração de normalização. |
| Q4 | A **busca** deve incluir `numero` (edital) e `situacao`, ou continuar restrita à descrição? | Define a API de filtro. |
| Q5 | Os PDFs podem ser públicos (link direto) ou exigir autenticação? | Define disco privado + rota de download. |
| Q6 | Deve existir **filtro por situação** no portal público? | Requisito novo (não é regressão). |
| Q7 | Qual o **volume** esperado de certames e de PDFs? | Define se a exportação precisa ir para fila. |
| Q8 | A exclusão deve ser definitiva (com remoção dos arquivos)? | Muda a semântica do soft delete. |
| Q9 | A página estática "Concurso Público" (`menu_items` 35/59) deve ser **removida** em favor do módulo? | Evita conteúdo duplicado/divergente. |
| Q10 | O módulo precisa de **auditoria** (quem cadastrou/alterou o certame)? | Define `LogsActivity` e o escopo dos logs. |

## 6. Mapa v5 → v6 (resumo operacional)

| Item | v5 | v6 | Ação na v7 |
|---|---|---|---|
| Listagem pública | `/concursos-pss` | **404** | Recriar com a mesma URL. |
| Detalhe público | `/concurso-pss/{slug}` | **404** | Recriar com a mesma URL. |
| Exportação pública | `/concursos-pss?type=xls\|pdf\|csv\|txt` | **404** | Recriar + rota dedicada. |
| CMS | `/cms/concursos-pss` | **404** | Recriar com os mesmos nomes de rota. |
| DataTable | `/cms/datatable/concurso-pss` | **404** | Recriar. |
| Permission resource | `concurso-pss` (id 16) | **inexistente** | Recriar em `modulos` + `resources`. |
| Menu / contador | `menu_orders` id 36 | **inexistente** | Recriar com `modulo_id`. |
| Tabela | `concursos_pss` (0 linhas) | **inexistente** | Criar (migração trivial neste ambiente). |
| View SQL | `concursos_pss_view` | **inexistente** | Opcional na v7. |
| Página estática | `menu_items` 35/59 → conteúdo 18 | mantida | Decidir (Q9). |
| Link "Concurso público" no rodapé | — | `href="#"` (morto) | Apontar para `/concursos-pss`. |
