# VIAP — Gaps, Riscos e Dúvidas

> Módulo **novo na v6**. Este documento registra o que foi verificado, o que
> está quebrado, o que é risco e o que ainda precisa de decisão do negócio.

---

## 1. Mapa v5 → v6

### 1.1 O módulo **não existe na v5**

Busca negativa executada sobre **todo** o repositório `v5/` por `viap` e
`VIAP` (case-insensitive), incluindo PHP, Blade, JS, SQL e demais arquivos.

| Tipo de arquivo | Resultado |
|---|---|
| `v5/app/**` (controllers, models, repositories) | **0 ocorrências** |
| `v5/database/migrations/**` | **0 ocorrências** |
| `v5/database/seeds/**` | **0 ocorrências** |
| `v5/routes/**` | **0 ocorrências** |
| `v5/resources/views/**` | **0 ocorrências** |
| `v5/public/assets/**` | apenas falsos positivos em arquivos minificados de terceiros: `v5/public/assets/plugins/DataTables/Buttons/js/pdfmake.min.js`, `v5/public/assets/landing-covid/js/pdfmake.min.js`, `v5/public/assets/landing-covid/js/toastr/pdfmake.min.js` |
| `v5/especificacoes/**` | falso positivo em arquivo binário do LibreOffice: `v5/especificacoes/modelos/PREFEITURAS0604.mwb` |

**Conclusão:** o VIAP é uma funcionalidade **inteiramente nova na v6**. Não há
código, dado ou contrato a portar da v5 — a especificação funcional precisa
ser levantada com o negócio (ver seção 5).

### 1.2 Cronologia na v6

| Item | Data / valor |
|---|---|
| Migration `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | cria a tabela `modulos` com **56** módulos |
| Linhas 57 e 58 de `modulos` | `carta-de-servicos` (id 57) e **`viap` (id 58)** — criados **depois** da migration, por seed |
| Migration `v6/database/migrations/2026_05_25_112000_create_viap_table.php` | 2026-05-25 11:20:00 — `viap` + `viap_has_archives` |
| Migration `v6/database/migrations/2026_05_25_112100_create_view_to_viap.php` | 2026-05-25 11:21:00 — view `view_viap` |
| `v6/database/seeds/ViapSeeder.php` | cria `modulos` (id 58), `resources` (id 79), `menu_orders` (id 45) e as permissões `viap.*` no papel `administrador` |

## 2. Gap G-10 — O módulo é registrado por **seed**, não por migration

**Gravidade: alta (operacional).**

O seed `v6/database/seeds/ViapSeeder.php` é o **único** responsável por
inscrever o módulo nas tabelas administrativas. Consequências:

1. Um ambiente novo que rode apenas `php artisan migrate` **não** terá:
   - a linha `modulos` id 58 (`slug = 'viap'`);
   - o `resources` id 79 (permissão);
   - o `menu_orders` id 45 (entrada de menu);
   - as permissões `viap.*` no papel `administrador`.
2. Sem a linha em `modulos`, o menu lateral **não mostra** o VIAP
   (`v6/app/Providers/MenuServiceProvider.php:45-46` exige
   `$menu->modulo->status`).
3. Sem as permissões, os operadores com papel `administrador` recebem
   "Você não tem autorização para acessar esta funcionalidade!" ao tentar
   acessar `/cms/viap`.
4. As **tabelas** (`viap`, `viap_has_archives`) e a **view** (`view_viap`) são
   criadas por migration — ou seja, o schema existe, mas o módulo é
   invisível. Situação desconcertante para quem só rodar `migrate`.

**Mitigação:** tornar `db:seed` (ou o `ViapSeeder`) parte obrigatória do
pipeline de deploy, ou migrar o registro para uma migration. Esta é a
**pergunta Q-03** (seção 5).

## 3. Segurança

### 3.1 O que foi **verificado** (e não é problema)

A hipótese "controllers do CMS sobrescrevem `__construct()` sem
`parent::__construct()`, removendo o middleware `check.route.permission`"
foi verificada no código **e em runtime**:

```php
// v6/app/Http/Controllers/Prefecture/ViapController.php:24-29
public function __construct(IViapRepository $viapRepository, IArchiveRepository $archiveRepository)
{
    $this->middleware('check.route.permission');   // <- registrado explicitamente
    $this->viapRepository = $viapRepository;
    $this->archiveRepository = $archiveRepository;
}
```

A classe base `App\Http\Controllers\Prefecture\Controller` também registra
`check.route.permission` no seu construtor, mas o `ViapController` **o
re-registra por conta própria**. Resultado medido em runtime:

| Requisição (sem sessão) | Resposta |
|---|---|
| `GET /cms/viap` | `302 → /login` |
| `GET /cms/viap/1` | `302 → /login` |
| `GET /cms/viap/1/edit` | `302 → /login` |
| `GET /cms/datatable/viap` | `302 → /login` |

**Conclusão: as rotas de escrita do VIAP NÃO estão expostas a anônimos.**
O risco crítico temático do panorama
(`dossie_modules/_00_panorama/16-AUTORIZACAO-ROTA-SEM-AUTENTICACAO.md`) **não
se materializa neste módulo**.

### 3.2 Gap G-05 — As rotas de escrita são abertas a **qualquer usuário logado**

**Gravidade: alta.**

Ainda dentro de `CheckRoutePermission::hasAccess()`, os segmentos `store`,
`update` e `destroy` estão nos **valores** de `$resourceRoutes`
(`['edit' => 'update', 'create' => 'store', 'delete' => 'destroy']`), e o
código faz `in_array($value, $resourceRoutes)` — que compara com os **valores**.
Consequência: as rotas `viap.store`, `viap.update` e `viap.destroy` são
consideradas **abertas**, sem checar permissão.

Matriz verificada em runtime com o papel `administrador`:

| Rota | Resultado |
|---|---|
| `viap.store` | **ABERTO** a qualquer logado |
| `viap.update` | **ABERTO** a qualquer logado |
| `viap.destroy` | **ABERTO** a qualquer logado |
| `viap.ajax.delete.files` | **ABERTO** (segmento `ajax` está em `$openRoutes`) |
| `datatable.viap` | **ABERTO** (segmento `datatable` está em `$openRoutes`) |

Ou seja: **qualquer usuário autenticado no CMS** — inclusive com o papel
`basico`, que não tem nenhuma permissão `viap.*` — consegue **criar, editar e
excluir** lançamentos de VIAP.

### 3.3 Gap G-06b — `ajaxDeleteFiles` apaga QUALQUER arquivo do sistema

**Gravidade: crítica (cross-module).**

```php
// v6/app/Http/Controllers/Prefecture/ViapController.php:112-121
public function ajaxDeleteFiles(Request $request)
{
    $archiveId = $request->get('archive_id');
    if ($this->archiveRepository->destroy($archiveId)) {
        return response()->json(['success' => 'Arquivo deletado com sucesso.'], 200);
    }
    return response()->json(['erro' => 'Erro ao deletar arquivo.'], 500);
}
```

Não há **nenhuma** verificação de que o `archive_id` pertence a um VIAP (nem
a um conteúdo, nem a uma página estática). Combinado com o fato de a rota ser
**aberta** (Gap G-05, segmento `ajax`), qualquer usuário logado no CMS pode
**apagar (soft delete) qualquer arquivo do portal** — de notícias, publicações,
diário oficial, editais etc. — informando o `archive_id`.

**Mitigação obrigatória na v7:** escopar a operação ao VIAP
(`Viap::findOrFail($id)->archives()->where('archives.id', $archiveId)->firstOrFail()`)
e proteger a rota com `can:delete` / Policy.

### 3.4 Demais observações de segurança

| Item | Situação |
|---|---|
| CSRF | As rotas do CMS estão no grupo `web`, que inclui `VerifyCsrfToken` (`v6/app/Http/Kernel.php:35-44`). O `'before' => 'csrf'` do grupo em `v6/routes/basepro.php:35` é **sintaxe do Laravel 5.2**, ignorada na 5.5 — mas o middleware `web` já cobre o CSRF. **Verificado, não é problema.** |
| Sessão/cookie | `EncryptCookies` + `StartSession` vêm do grupo `web`. |
| XSS | A descrição (HTML do Summernote) **não é persistida** hoje (Gap G-03); ao corrigir, precisa de sanitização antes de ir para o portal. |
| SQL injection | Não há `whereRaw`/`DB::raw` no módulo. A view `view_viap` é SQL estático. |
| Exportação pública | `/viap/export` é público e aceita qualquer método (`Route::any`); exporta apenas registros `status = true`. **Sem PII.** |
| Rate limiting | Não há `throttle` nas rotas públicas do portal. |

## 4. Bugs e funcionalidades incompletas

| ID | Descrição | Gravidade | Onde |
|---|---|---|---|
| **G-01** | **A listagem pública fica vazia sem filtro.** Sem nenhum parâmetro, `$hasFilters` é `false` e o controller substitui o resultado por um paginador vazio: `$viaps = new LengthAwarePaginator([], 0, 15)`. O cidadão chega em `/viap` e vê "Nenhum registro encontrado.", mesmo havendo registros. Os botões de exportação (que só aparecem com `$viaps->count() > 0`) também somem. | **Alta** | `v6/app/Http/Controllers/Portal/ViapPortalController.php:62-70` |
| **G-02** | **`amount` nunca é preenchido.** A coluna existe, o portal mostra a coluna "Valor" — e exibe sempre `R$ 0,00`. Não há campo no formulário e o atributo não está no `$fillable`. | **Alta** | `v6/app/Http/Model/Prefecture/Viap.php:21-30`, `v6/resources/views/prefecture/viap/form.blade.php` |
| **G-03** | **`description` nunca é persistida.** O campo Summernote é enviado e gravado… não: o `Repository::upInsert` faz `updateOrCreate` e o Eloquent ignora o atributo por não estar no `$fillable`. O dado é silenciosamente perdido. | **Alta** | `v6/app/Http/Model/Prefecture/Viap.php:21-30` |
| **G-04** | **Rota `viap.show` do CMS aponta para método inexistente.** `Route::resource` gera `GET /cms/viap/{viap}` → `ViapController@show`, mas o controller **não tem** método `show()`. Resultado: **HTTP 500**. | Média | `v6/routes/basepro.php:192` vs `v6/app/Http/Controllers/Prefecture/ViapController.php` |
| **G-05** | Escrita liberada a qualquer usuário logado (ver 3.2). | **Alta** | `v6/app/Http/Middleware/CheckRoutePermission.php:44-95` |
| **G-06b** | `ajaxDeleteFiles` apaga arquivos de qualquer módulo (ver 3.3). | **Crítica** | `v6/app/Http/Controllers/Prefecture/ViapController.php:112-121` |
| **G-06** | **`slug` não é único** (sem índice) e é **regerado a cada edição** — o link público muda quando o registro é atualizado, quebrando links já publicados (Gap G-13). | Média | `v6/app/Repositories/Eloquent/Prefecture/Viap/ViapRepositoryDefaultImpl.php:39-67` |
| **G-07** | **A exclusão em lote é negada para o papel `administrador`.** O botão aparece (`has_access(['viap.delete'])` = true) mas o POST é recusado: a rota exige `viap.delete-por-lote` ou `delete-por-lote.index`, permissões que o seed **não** grava. O operador vê o botão e recebe "Você não tem autorização…". | Média | `v6/database/seeds/ViapSeeder.php:61-70` + `CheckRoutePermission::hasAccessSegment()` |
| **G-08** | **Permissões mortas e sem função:** `viap.inactive`, `viap.inactivate`, `viap.restore` e `viap.show` são gravadas pelo seed, mas **não existe rota** para inativar/restaurar. Consequência: a coluna `status` **nunca muda** — o `scopeActive` do portal filtra por um campo que está sempre `true`. | Média | `v6/database/seeds/ViapSeeder.php:61-70` |
| **G-09** | **O tratamento de erro do controller é inoperante.** `Repository::upInsert()` captura `\Throwable` e chama `dd($th)`, matando a requisição antes que o `catch (Exception $e)` do `ViapController` possa exibir a mensagem amigável. | Média | `v6/app/Repositories/Repository.php:59-70` |
| **G-10** | Módulo registrado só por seed (ver seção 2). | Alta | `v6/database/seeds/ViapSeeder.php` |
| **G-11** | **12 views duplicadas** do portal (`modelo1..6` × `index`/`show`) + **2 views órfãs** (`v6/resources/views/portal/viap/index.blade.php` e `show.blade.php`) que nenhuma rota usa. | Baixa | `v6/resources/views/portal/layouts/modelo*/viap/` |
| **G-12** | **O botão "pdf" do portal está quebrado.** `gerar-pdf-viap.js` depende da global `viaps`, que nunca é definida (o controller passa `$viaps` para a view Blade). O clique sempre mostra o alert "Não há dados para exportar". | Baixa | `v6/public/assets/js/modulos/jspdf/gerar-pdf-viap.js` + `v6/resources/views/portal/layouts/modelo1/viap/index.blade.php:215-217` |
| **G-13** | **Slug regerado a cada edição** → o link público muda e links publicados quebram. | Média | `ViapRepositoryDefaultImpl::structureUpInsert()` |
| **G-14** | **`link_type` não limpa o vínculo oposto.** Ao trocar de "Gabinete" para "Outros Órgãos", o `councilman_id` anterior permanece gravado junto com o `organ_id`. A view SQL prioriza o `CASE` por `link_type`, então o erro fica oculto — mas o dado está inconsistente. | Baixa | `v6/resources/views/prefecture/viap/form.blade.php` |
| **G-15** | **A rota de detalhe do portal falha quando o slug é `null`:** `route('portal.viap.show', $viap->slug ?? $viap->id)` gera `/viap/{id}`, e o `show()` busca **por slug** → 404. A view órfã `v6/resources/views/portal/viap/index.blade.php:118` usa o `id` direto → **sempre 404**. | Baixa | `v6/app/Http/Controllers/Portal/ViapPortalController.php:75-86` |
| **G-16** | **A consulta do portal é executada duas vezes** e materializa **todos** os registros em memória mesmo com paginação (`->get()` seguido de `->paginate(15)`). Em produção, com muitos lançamentos, é um risco de memória/tempo. | Média | `v6/app/Http/Controllers/Portal/ViapPortalController.php:52-70` |
| **G-17** | **O DataTable do CMS carrega a view inteira** (`DB::table('view_viap')->get()`), sem paginação no servidor, e ainda faz um laço PHP por linha. | Baixa | `v6/app/Http/Controllers/Prefecture/DatatableController.php:480-489` |
| **G-18** | **`deleteByLote` e `ajaxDeleteFiles` não validam a entrada** (nem `toDelete` como array, nem `archive_id` como inteiro existente). | Baixa | `v6/app/Http/Controllers/Prefecture/ViapController.php:102-121` |
| **G-19** | **Upload parcial silencioso:** `extractedToArchive` captura a exceção de cada arquivo e só grava no log; o operador recebe "VIAP cadastrado com sucesso" mesmo com arquivos falhados. | Baixa | `v6/app/Http/Controllers/Prefecture/ViapController.php:160-167` |

### 4.1 Funcionalidades mortas / não implementadas

| Item | Situação |
|---|---|
| Botão/ação de **ativar/desativar** lançamento | Não existe (nada altera `status`) — permissões `viap.inactive`/`viap.inactivate`/`viap.restore` são órfãs |
| Detalhe do VIAP no CMS (`viap.show`) | Rota existe, método não |
| Exportação com **valor** e **descrição** | As exportações usam apenas 3 colunas (Data, Referência, Vínculo) |
| PDF client-side | Quebrado (G-12) |
| *Static page* `slug = 'viap'` | Suportada pelo controller (`$viap_static_page`), mas **nenhuma** cadastrada no banco — o texto padrão fixo é sempre o exibido |

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

| ID | Pergunta | Impacto |
|---|---|---|
| **Q-01** | **O valor da verba (`amount`) e a descrição devem ser preenchidos?** Hoje as colunas existem, o portal exibe "Valor" (sempre `R$ 0,00`) e o formulário tem um editor de descrição que é perdido. Respostas possíveis: (a) implementar valor + descrição (exige migration, formulário, API/CSV de importação e sanitização de HTML); (b) remover as colunas e o campo do formulário; (c) manter como estão. | Alto — define se a v7 terá campos de importação de valores |
| **Q-02** | **O slug deve ser estável?** Hoje é regerado a cada edição. Se os links forem publicados (redes sociais, QR Code, relatórios), a mudança quebra o acesso. Alternativas: slug imutável, `slug` + `uuid`, ou redirecionamento do slug antigo. | Médio |
| **Q-03** | **O registro do módulo deve continuar em seed?** Se sim, o `db:seed` precisa ser obrigatório no deploy (com risco de esquecimento). Alternativa: migration que cria `modulos`/`resources`/`menu_orders`. | Alto — operacional |
| **Q-04** | **A listagem pública deve mostrar todos os registros sem filtro?** A implementação atual só mostra registros filtrados (G-01). A intenção era esconder o volume total, ou é bug? | Alto — define a correção |
| **Q-05** | **Como o município importa os lançamentos de VIAP?** Não há planilha de importação, API nem integração com o sistema de contas do legislativo. Hoje a carga é manual, um a um, no CMS. | Alto — pode exigir CSV de importação em lote na v7 |
| **Q-06** | **A exclusão deve ser definitiva?** Hoje é soft delete e o portal some com o registro, mas o dado permanece no banco e nos backups. Há exigência legal de retenção? | Baixa |
| **Q-07** | **Qual o vínculo legal do registro?** A tabela não guarda a Resolution que regulamenta a verba (mencionada no texto do portal) nem o número do empenho/ordem de pagamento. É necessário? | Médio |
| **Q-08** | **Quem pode inativar um lançamento?** As permissões existem mas não há ação. Sem isso, "publicar" e "despublicar" não são controláveis. | Médio |

## 6. Riscos de dados (migração de schema v6 → v7)

| Risco | Detalhe | Mitigação |
|---|---|---|
| Slug duplicado ao criar índice `unique` | Sem restrição hoje, podem existir slugs repetidos | Antes de criar o índice, desambiguar (`-2`, `-3`) |
| Tabelas vazias na origem | `viap` e `viap_has_archives` têm **0 registros** no banco de análise | A migração de dados é trivial neste ambiente; em ambientes com dados, migrar em transação e com `ON CONFLICT DO NOTHING` |
| `view_viap` não portada | A listagem do CMS dependia da view | Substituir por `with(['councilman','organ'])` + accessor `linked_name` (já previsto em `06-MIGRACAO-MODERNA.md`) |
| FK `ON DELETE CASCADE` em `councilman_id`/`organ_id` | Apagar um parlamentar/órgão apaga os lançamentos de VIAP | Confirmar com o negócio antes de mudar para `nullOnDelete` |
| Coluna `description` hoje vazia | Se o campo passar a ser preenchido, o histórico fica incompleto | Registrar que os registros antigos não têm descrição |
| `amount` NULL em todos os registros | Planilha de valores histórica inexistente | Registrar a lacuna |
| Permissões `viap.*` só no papel `administrador` | Papéis `basico`, `licitacoes`, `Lai`, `covid-19` **não** têm nenhuma permissão VIAP | Ao ativar a Policy na v7, redistribuir as permissões |

## 7. Resumo executivo dos riscos

| Severidade | Itens |
|---|---|
| **Crítica** | G-06b — `ajaxDeleteFiles` apaga arquivos de qualquer módulo |
| **Alta** | G-01 (listagem pública vazia), G-02 (`amount` morto), G-03 (`description` perdida), G-05 (escrita aberta a qualquer logado), G-10 (módulo só por seed) |
| **Média** | G-04 (rota 500), G-07 (exclusão em lote negada), G-08 (permissões mortas / `status` inalterável), G-09 (`dd()` no repository), G-13 (slug instável), G-16 (dupla query) |
| **Baixa** | G-11 (views duplicadas), G-12 (PDF client-side quebrado), G-14 (vínculo não limpo), G-15 (404 por `id`), G-17 (DataTable sem paginação server), G-18 (sem validação), G-19 (upload parcial silencioso) |

**Veredito sobre a hipótese original do risco crítico:** a exposição de rotas
de escrita **sem autenticação** foi testada e **não ocorre** no VIAP — o
controller re-registra `check.route.permission` e todas as rotas do CMS
redirecionam para `/login` sem sessão. O risco real é de **granularidade**
(qualquer logado escreve) e de **escopo** (`ajaxDeleteFiles` alcança arquivos
de outros módulos).
