# VIAP — Regras de Negócio e Portabilidade

> Regras não óbvias, acoplamentos, armadilhas de código e decisões que a
> reconstrução em Laravel 11/12 precisa preservar.

---

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

| # | Regra | Onde está | Consequência prática |
|---|---|---|---|
| R-01 | O portal exibe apenas lançamentos com `status = true` (`scopeActive`) | `v6/app/Http/Model/Prefecture/Viap.php:78-81` | `publication_date` **não** controla a publicação; a flag `status` é a chave — mas **não há tela para alterá-la** |
| R-02 | `link_type` decide o vínculo: `gabinete` (parlamentar) ou `outro_orgao` (órgão) | `v6/app/Http/Requests/ViapRequest.php:25-34` | Ao trocar o tipo, o formulário mostra/esconde o seletor correspondente; o valor do outro vínculo **não é limpo** (fica gravado junto) |
| R-03 | O slug público é `{mês}-{ano}[-gabinete-{parlamentar}|-orgao-{órgão}]` | `v6/app/Repositories/Eloquent/Prefecture/Viap/ViapRepositoryDefaultImpl.php:39-67` | O slug é **regerado a cada update** → o link público muda quando o registro é editado (Gap G-13) |
| R-04 | `councilman_id` / `organ_id` com valor `0` são convertidos para `null` | `ViapRepositoryDefaultImpl::structureUpInsert()` (linhas 25-31) | O seletor "Selecione o Parlamentar/Órgão" com value 0 não viola a FK |
| R-05 | A data é gravada em formato americano (`YYYY-MM-DD`) | `v6/app/Http/Traits/AppTrait.php:264-275` (`toUSDataFormat`) | O input do CMS é `d/m/Y`; o banco é `timestamp` |
| R-06 | Cada anexo tem sua própria descrição e fica na tabela `archives`, ligada por `viap_has_archives` | `v6/app/Http/Controllers/Prefecture/ViapController.php:134-171` | Arquivos são servidos de `storage/{path}`; o path gerado é `viap/{id}/arquivos/{nome}` |
| R-07 | A exclusão é **soft delete** (`SoftDeletes`) | `v6/app/Http/Model/Prefecture/Viap.php:16` | Registros excluídos somem da view `view_viap` e do portal |
| R-08 | O módulo só entra no menu com `modulos.status = true` **e** `has_access('viap.index')` | `v6/app/Providers/MenuServiceProvider.php:45-46` | Desativar o módulo no CMS remove o link do menu, mas **não** bloqueia as rotas |
| R-09 | A listagem do CMS lê a **view** `view_viap`, não o model | `v6/app/Http/Controllers/Prefecture/DatatableController.php:482` | Mudanças no model (ex.: novo campo) **não** aparecem na listagem até a view ser migrada |
| R-10 | O portal pagina 15 registros por página | `v6/app/Http/Controllers/Portal/ViapPortalController.php:62-70` | Só há paginação **quando há filtro** (Gap G-01) |
| R-11 | Os anos disponíveis no filtro do portal são o atual e os 5 anteriores | `v6/resources/views/portal/layouts/modelo1/viap/index.blade.php` | Constraint de UI (não de negócio) |
| R-12 | No CMS o ano de referência vai do atual até 10 anos atrás | `v6/resources/views/prefecture/viap/form.blade.php` | Constraint de UI |

## 2. Acoplamentos

### 2.1 Dependências do VIAP

| Módulo/tabela | Tipo de acoplamento | Ponto de contato |
|---|---|---|
| **Vereadores** (`councilmen`) | FK + consulta | `Viap::councilman()`; `CityCouncilor::where('status', true)` |
| **Publicações / Conteúdos** (`contents`) | FK + consulta | `Viap::organ()`; órgãos = conteúdos da categoria `slug = 'orgaos'` |
| **Categorias de conteúdo** (`category_contents`) | Consulta por slug | `CategoryContent::where('slug', 'orgaos')` |
| **Arquivos** (`archives`) | Pivot `viap_has_archives` | `Viap::archives()`, `IArchiveRepository` |
| **Módulos** (`modulos`, `resources`, `menu_orders`) | Registro administrative | `ViapSeeder`, `MenuServiceProvider` |
| **Papéis** (`roles`) | Permissões `viap.*` | `ViapSeeder` passo 4 |
| **Páginas estáticas** (`static_pages`) | Texto institucional opcional | `StaticPage::where('slug','viap')` — **nenhuma** cadastrada hoje |

### 2.2 Dependências *do* VIAP

**Nenhuma.** O módulo não é consumido por outros módulos: não há API, não há
hooks, não há views de outros módulos que o referenciem. Ele é uma folha da
arquitetura.

### 2.3 Acoplamento ao framework legado

- **Autorização por nome de rota** (`check.route.permission`): o módulo só
  funciona se as rotas mantiverem exatamente os nomes `viap.index`,
  `viap.create`, `viap.edit`, `viap.show`, `viap.delete-por-lote`,
  `viap.ajax.delete.files` — as permissões do papel são casadas com esses
  nomes.
- **Repository pattern com `updateOrCreate`**: o contrato
  `IViapRepository` é vazio; toda a lógica está em
  `ViapRepositoryDefaultImpl` + `Repository::upInsert()`.
- **View SQL** como contrato entre o banco e a listagem do CMS.

## 3. Armadilhas

### 3.1 `dd($th)` no repository base — o `try/catch` do controller nunca roda

```php
// v6/app/Repositories/Repository.php:59-70
public function upInsert($request, int $id = null)
{
    try {
        $data = DB::transaction(function () use ($request, $id) {
            return $this->structureUpInsert($this->dataTrait($request));
        });
        return $this->getModel()->updateOrCreate(['id' => $id], $data);
    } catch (\Throwable $th) {
        dd($th);            // ← despeja a stack trace na tela
    }
    return false;
}
```

O `ViapController::store()`/`update()` involves `try { … } catch (Exception $e)
{ flash('Ocorreu um erro ao cadastrar o VIAP.')->error(); }`, mas essa
exceção **nunca chega ao controller**: o `dd()` mata a requisição. O operador
vê um dump de exceção em vez da mensagem amigável. **Armadilha herdada, mas
crítica para o módulo** porque um erro de validação de FK (por exemplo,
`councilman_id` inexistente) derrubaria a tela.

### 3.2 Mass assignment silencioso — `description` e `amount` nunca persistem

```php
// v6/app/Http/Model/Prefecture/Viap.php:21-30
protected $fillable = [
    'publication_date', 'reference_month', 'reference_year',
    'link_type', 'councilman_id', 'organ_id', 'status', 'slug',
];
```

- `description` é enviada pelo formulário (Summernote) e **descartada**;
- `amount` não tem campo no formulário **e** não é `fillable`;
- o `Repository::upInsert` passa o array inteiro da request ao `updateOrCreate`
  — o Eloquent ignora silenciosamente o que não está no `$fillable`.

### 3.3 N+1 dentro da transação de escrita

`ViapRepositoryDefaultImpl::generateSlug()` executa uma consulta por registro
gravado:

```php
$slug .= '-gabinete-' . $this->slugify($councilman->name);   // CityCouncilor::find(...)
$slug .= '-orgao-' . $this->slugify($organ->title);           // Content::find(...)
```

São 1 consulta extra por gravação — aceitável, mas o padrão "consulta dentro
do repository para montar o slug" deve ser trocado por relação eager-loaded na
v7.

### 3.4 `slugify` dependente de locale

```php
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
```

`//TRANSLIT` depende da locale do sistema: em containers com locale C/POSIX,
nomes acentuados podem virar string vazia, gerando slugs como
`janeiro-2026-gabinete-`. Com `trim($text, '-')` o resultado pode ser vazio.

### 3.5 Consultas duplicadas no portal

`ViapPortalController::index()` executa a **mesma** query duas vezes quando há
filtro:

```php
$result = $query->orderBy('publication_date', 'desc')->get();   // materializa TUDO
$viaps  = $result;

if (request()->route()->getName() == 'viap.export') {
    return $this->export($result, $request->type);
}

if (!$hasFilters) {
    $viaps = new \Illuminate\Pagination\LengthAwarePaginator([], 0, 15);
} else {
    $viaps = $query->orderBy('publication_date', 'desc')->paginate(15);  // 2ª query
}
```

A primeira materialização é necessária só para a exportação, mas ela acontece
**também na página HTML** → o volume inteiro de registros é carregado em
memória a cada visita, mesmo com paginação.

### 3.6 Carregamento integral da view no DataTable do CMS

`DB::table('view_viap')->get()` traz **todas** as linhas (sem `limit`) e o
`struct_views()` ainda faz um laço PHP por linha para injetar flags de
permissão. O DataTables pagina no cliente.

### 3.7 `getRelatedData()` sem cache

Cada render de `create`/`edit` dispara 3 consultas (`category_contents`,
`councilmen`, `contents`).

### 3.8 Sem `->whereRaw()` / `DB::raw()` / `$_SESSION` / `Route::` em controller

Verificado por busca no módulo: **não há** `whereRaw`, `DB::raw`, `selectRaw`,
`$_SESSION` nem chamadas a `Route::` dentro dos controllers do VIAP. A view SQL
`view_viap` é o único SQL "hardcoded" do módulo (e está encapsulada em uma
migration).

### 3.9 Outras armadilhas

| Armadilha | Onde | Efeito |
|---|---|---|
| `route('portal.viap.show', $viap->slug ?? $viap->id)` nos modelos 1/2/3/6 | `v6/resources/views/portal/layouts/modelo1/viap/index.blade.php:162` | Se o slug for `null`, a rota aponta para `/viap/{id}` e o `show()` (que busca por `slug`) devolve **404** |
| `route('portal.viap.show', $viap->id)` na view órfã | `v6/resources/views/portal/viap/index.blade.php:118` | Sempre 404 (o `show` busca por slug) |
| `description` é HTML do Summernote | `v6/resources/views/prefecture/viap/form.blade.php` | Quando o bug 3.2 for corrigido, o HTML precisa ser sanitizado antes de ir para o portal |
| `extractedToArchive` engole exceções por arquivo | `v6/app/Http/Controllers/Prefecture/ViapController.php:160-167` | Falha parcial de upload é registrada no log, mas o operador recebe "sucesso" |
| `deleteByLote` retorna `response()->json(false)` do repository | `v6/app/Repositories/Repository.php:99-113` | O JS espera `response.success`; com `false` cai no texto padrão "Registro(s) deletados com sucesso!" mesmo em caso de falha |
| `viap.show` do CMS aponta para método inexistente | `v6/routes/basepro.php:192` (`Route::resource`) | HTTP 500 (Gap G-04) |
| `status` é `fillable` mas não é enviado pelo formulário | `v6/app/Http/Model/Prefecture/Viap.php:21-30` | O `updateOrCreate` **não** sobrescreve o valor existente → o registro nunca é desativado |
| Permissões `viap.inactive` / `viap.inactivate` / `viap.restore` / `viap.show` sem rota | `v6/database/seeds/ViapSeeder.php:61-70` | Permissões mortas (Gap G-08) |

## 4. Código duplicado

| Duplicação | Detalhe |
|---|---|
| Views do portal | `modelo1..6` × `viap/index.blade.php` e `viap/show.blade.php` — 12 arquivos quase idênticos (diferem em `@extends` e no breadcrumb) |
| View órfã | `v6/resources/views/portal/viap/index.blade.php` e `show.blade.php` (cópias do modelo5, não referenciadas pelo controller) |
| Consultas de órgãos/parlamentares | Repetidas no `ViapController::getRelatedData()` e no `ViapPortalController::index()` (mesma consulta de `CategoryContent::where('slug','orgaos')`) |
| Tabela de meses | `Viap::referenceMonths()` (model) e um array PHP equivalente no `form.blade.php` |

## 5. Onde a v6 divergiu da v5

**Não há divergência: o módulo não existe na v5.** Verificação negativa
executada sobre todo o repositório `v5/` (busca textual por `viap`/`VIAP`):
os únicos achados são falsos positivos dentro de arquivos minificados de
terceiros (`v5/public/assets/plugins/DataTables/Buttons/js/pdfmake.min.js` e
`v5/public/assets/landing-covid/js/pdfmake.min.js`) e de um arquivo de
modelo binário do LibreOffice (`v5/especificacoes/modelos/PREFEITURAS0604.mwb`).

Não há, na v5: migration, seed, model, controller, repository, view ou rota
referente ao VIAP.

## 6. Decisões de equivalência funcional (não podem mudar na v7)

1. **A publicabilidade é controlada por `status`, não por data.** Se a v7
   passar a filtrar por `publication_date <= now()`, o comportamento muda.
2. **O slug é a chave pública.** O formato `{mês}-{ano}-{tipo}-{nome}` é
   referenciado em URLs já divulgadas.
3. **O valor e a descrição existem no schema.** Decidir se a v7 passa a
   preenchê-los (exigindo migration + formulário) ou se os campos devem ser
   removidos — é uma **decisão de negócio** (pergunta aberta Q-01 em
   `08-GAPS-E-DIVIDAS.md`).
4. **A exclusão em lote é por array de ids** (`toDelete`) e responde JSON.
5. **As exportações mantêm os mesmos nomes de arquivo** (`viap.xlsx`,
   `viap.pdf`, `viap.csv`, `viap.txt`) e as mesmas 3 colunas.
6. **O texto institucional padrão** (Resolução nº 1885/2020 e alterações)
   deve continuar aparecendo quando não houver *static page* com `slug='viap'`.
7. **O módulo deve continuar público** (sem login) no portal, e **restrito ao
   CMS** — com a correção da granularidade de permissões descrita em
   `08-GAPS-E-DIVIDAS.md` (Gap G-05).
