# Transparência Fiscal — Gaps, Bugs e Dívidas

> Achados verificados por leitura de código, inspeção do banco (read-only) e busca de
> arquivos no repositório `v6` (com `v5` como referência).

---

## 1. Bugs e erros conhecidos

| # | Severidade | Achado | Evidência | Efeito |
|---|---|---|---|---|
| B1 | **Alta** | `TransparencyController@show` acessa `$transparencia->title` sem proteger `$transparencia` | `v6/app/Http/Controllers/Portal/TransparencyController.php:102` (`$transparencia->title ?? 'Transparência Fiscal'`) | Com slug inexistente, `Content::first()` retorna `null` → erro 500 "Trying to get property of non-object" (o `??` não protege o acesso a propriedade de `null`) |
| B2 | **Alta** | Relação `InformationsRequest::answers()` aponta para uma coluna inexistente | `v6/app/Http/Model/Prefecture/InformationsRequest.php` (`hasMany(Answer::class,'id','answers_id')`) | A tabela `answers` tem `informations_request_id`, **não** `answers_id`. O `with('answers')` usado em `getAnswers` não retorna as respostas — o controller contorna com uma segunda consulta |
| B3 | **Alta** | 9 rotas com **3 nomes duplicados** | `v6/routes/web.php:165-176` | `transparencia.index` e `transparencia.show` registrados 4 e 5 vezes. O `route()` sempre resolve para a última URI; a navegação interna nunca usa 8 das 9 URIs. Risco de SEO (conteúdo duplicado) e de conflito de rotas no Laravel moderno (a última definição sobrescreve as anteriores) |
| B4 | **Alta** | `subcategorias.blade.php` existe apenas no `modelo5` | `v6/resources/views/portal/layouts/modelo5/transparencia-fiscal/subcategorias.blade.php` (único arquivo com esse nome) | A rota `/transparencia/categoria/{slug}` responde **404 View not found** em qualquer template diferente de `modelo5` |
| B5 | **Média** | `getTemplateActived()` grava no banco durante um GET | `v6/app/Http/Controllers/Admin/Controller.php:39-47` (`DB::table('configurations')->where('key','pagina_em_manutencao')->update(['value' => 1])`) | Se a view do template não existir, o portal **entra em manutenção globalmente** só por causa de uma navegação |
| B6 | **Média** | `Configuration::where('key','layout')->first()->value` sem guarda de nulo | `v6/app/Http/Controllers/Admin/Controller.php:41` | Se a chave `layout` não existir, todas as páginas do portal quebram com 500 |
| B7 | **Média** | Resposta do e-SIC injetada no HTML sem escape | `v6/resources/views/portal/layouts/modelo1/transparencia-fiscal/relatorio-estatistico/js.blade.php` (`${value.answer}` em template string) | **XSS armazenado**: o texto da resposta do e-SIC é renderizado como HTML no modal |
| B8 | **Média** | `groupBy('status_id')` no meio de uma contagem | `v6/app/Http/Model/Prefecture/InformationsRequest.php` (`scopeGetByBetweenDate`) | `->count()` sobre query agrupada devolve o número de grupos; os contadores do gráfico podem não refletir o número de solicitações |
| B9 | **Média** | Carregamento integral da view SQL no DataTable | `v6/app/Http/Controllers/Prefecture/DatatableController.php:266` (`DB::table("transparencia_fiscal")->get()`) | Sem paginação server-side; payload completo ao navegador |
| B10 | **Baixa** | Id literal `25` replicado em 9 pontos | `TransparencyController` (3×), `CategoryContentController` (3×), `ContentController` (3×) | Se a raiz mudar de id, o sistema exibe a categoria errada **sem erro** |
| B11 | **Baixa** | `whereRaw` com SQL concatenado | `InformationsRequest::scopeGetByBetweenDate*` | Não há injeção (os fragmentos são internos), mas o padrão é frágil e não usa bindings |
| B12 | **Baixa** | `order` pode ser NULL | `contents.order` é nullable | `ORDER BY c."order"` coloca NULLs por último (PostgreSQL), quebrando a ordem esperada |
| B13 | **Baixa** | `isBlank == 1` (comparação numérica em booleano) | `v6/resources/views/portal/components/transparencia-fiscal/item-box.blade.php` | Funciona em PostgreSQL; quebra em MySQL |
| B14 | **Baixa** | `$datas` (anos) carrega todas as solicitações em memória | `relStatitics` (`InformationsRequest::orderBy('created_at','desc')->get()->pluck(...)`) | Em volume alto, consumo desnecessário |
| B15 | **Baixa** | Tabela `contents` não tem coluna `isBlank`, mas o `store` tenta gravar | `v6/app/Http/Controllers/Prefecture/ContentController.php:653` (`'isBlank' => $request->get('isBlank')`) | O valor é ignorado pelo mass assignment (não está no `$fillable`); o `links.isBlank` é a fonte da verdade |

---

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| Breadcrumb `transparencia-fiscal-show` | Registrado em `v6/routes/breadcrumbs.php:30` mas **não usado** por nenhuma view (as views usam `acesso-rapido` e `static-transparencia`) |
| Views do template `default` | `v6/resources/views/portal/layouts/default/pages/contents/transparencia-fiscal/index.blade.php` e `v6/resources/views/portal/layouts/default/pages/statics/transparencia-fiscal/*.blade.php` — **órfãs**: nenhum controller atual as renderiza |
| Template `newsbit` | Só possui `transparencia-fiscal/index.blade.php`; as demais rotas do módulo falham |
| `layouts.key = 'transparencia-fiscal-grid'` | Existe (id 34) com `status = false` no banco; controlado pelo `LayoutController` |
| `v6/app/Services/PortalTransparenciaService.php` | **Não é usado** por `TransparencyController`; serve apenas à busca global do portal (`Portal\PortalController@searchContents`) |
| `CategoryContent.display_type` | Vale apenas para a home (`PortalController@index`); a página de transparência ignora o campo |
| `Content.linha_fina`, `Content.acronym`, `Content.source`, `Content.author` | Campos genéricos não utilized por este módulo |
| `AddTerceirizadosInTransparenciaFiscalSeeder` | Só produz efeito quando `configurations.estado = 'PB'`; nos demais estados é um no-op silencioso |
| Rota `relatorio-estatistico/exportacao` | Existia na v5, **removida** na v6 (a exportação passou a usar `?type=`) |
| `configurations.portal_transparencia_*` | Criadas pelo seeder da v6; sem o `codigo_ente` preenchido a integração com o portal externo fica desligada |

---

## 3. Itens que existem na v5 e foram removidos na v6 (e vice-versa)

| Item | v5 | v6 |
|---|---|---|
| Rota `ANY /relatorio-estatistico/exportacao` (`relatorio-estatistico.exportacao`) | **Presente** (`v5/routes/web.php:29`) | **Removida** — a exportação passou a ser `?type=` em `relatorio-estatistico` |
| Método `exportRelStatitics(Request)` | Presente | **Removido** — substituído por `export($informationRequests, $type)` |
| Método `exportInformationRequestsTxt` (protected) | Presente | **Removido** — substituído por `exportTxt` (private) |
| View do PDF do relatório | `exports.information-requests` (singular) | `exports.informations-requests` (plural) |
| Nome dos arquivos exportados | `lai-relatorio-estatistico.*` | `relatorio-estatistico.*` |
| Cabeçalho do TXT | Inclui `config('app.name')` e o ano | Cabeçalho fixo "LAI - Relatório Estatístico" + data (o **ano foi perdido**) |
| `PortalTransparenciaService` + seeder de configurações do portal externo | **Ausente** | **Novo na v6** |
| Templates do portal | `modelo1..modelo5` | `modelo1..modelo6` + `newsbit` |
| Rotas de transparência (9) | Idênticas | Idênticas |
| 4 migrations da view SQL | Idênticas (mesmas datas) | Idênticas |

---

## 4. Mapa v5 → v6

| Camada | v5 | v6 | Observação |
|---|---|---|---|
| Controller do portal | `v5/app/Http/Controllers/Portal/TransparencyController.php` (250 linhas) | `v6/app/Http/Controllers/Portal/TransparencyController.php` (239 linhas) | Mesmo nome e mesmas ações; `index`, `subcategoria`, `show` e `getAnswers` praticamente idênticos |
| Controller do CMS | **Não existe** `Prefecture\TransparencyController` | **Não existe** idem | A administração é genérica (`ContentController`/`CategoryContentController`) nas duas versões |
| Rotas do portal | `v5/routes/web.php:131-142` | `v6/routes/web.php:165-176` | Mesmas 9 URIs, mesmas ações, mesmos nomes de rota |
| Rota do relatório | `v5/routes/web.php:28` + `:29` (export) | `v6/routes/web.php:20` (export via `?type=`) | **Única mudança estrutural de rota** |
| Rota Ajax | `v5/routes/web.php:63` | `v6/routes/web.php:70` | Idêntica |
| Rota CMS (DataTable) | `v5/routes/basepro.php:424` | `v6/routes/basepro.php:413` | `Prefecture\DatatableController@get_transparencia_fiscal` nas duas |
| View SQL | 4 migrations de 2023-04-11 a 2023-05-04 | Idênticas | `pg_get_viewdef` da v6 = SQL da migration de 2023-05-04 |
| Service do portal externo | Ausente | `v6/app/Services/PortalTransparenciaService.php` | Novo na v6 |
| Seeder `AddTerceirizadosInTransparenciaFiscalSeeder` | Presente | Presente (mesma lógica, condicionada a `PB`) | — |
| Seeder `AddPortalTransparenciaConfigurationsSeeder` | Ausente | Presente | Novo na v6 |
| Models | `App\Prefecture\*` | `App\Prefecture\*` | Mesmos nomes; `InformationsRequest` ganhou `DataMaskTrait` na v6 (mascaramento LGPD) |
| Views do portal | `modelo1..modelo5` | `modelo1..modelo6`, `newsbit` | views do módulo replicadas por template |

**Conclusão do comparativo:** o módulo é **funcionalmente equivalente** entre v5 e v6,
exceto por (a) incorporação da exportação ao relatório, (b) renomeação dos arquivos
exportados e da view de PDF, (c) simplificação do TXT e (d) o **acréscimo** da integração
com o portal de transparência externo. Nenhuma funcionalidade de transparência fiscal
 foi removida.

---

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

| Risco | Detalhe | Mitigação na v7 |
|---|---|---|
| View SQL como contrato de leitura | `transparencia_fiscal` alimenta o CMS; renomear colunas quebra controller + JS | Recriar as colunas equivalentes em um Resource/Query |
| `contents.order` com lacunas/duplicatas/NULLs | A ordenação do portal e da view depende dele | Normalizar na migração (`position` NOT NULL, índice único por categoria) |
| `links` opcional | Itens sem URL caem no `route('transparencia.show')` | Tornar `url` obrigatório ou definir o comportamento de itens sem link |
| `category_contents_id` da raiz | O id 25 está "hardcoded" em 9 pontos | Resolver pelo slug; falhar explicitamente se a raiz não existir |
| `statuses` com ids fixos 1-4 | Scopes e JS usam os ids literais | Migrar para enum |
| `static_pages.slug = 'relatorio-estatistico'` | O relatório depende desse registro; sem ele, `$content->text` quebra | Criar o registro no seeder e tratar o ausente |
| `informations_requests.protocol` | Protocolos únicos por prefeitura; o mascaramento depende do formato | Preservar o formato e o algoritmo de máscara (`DataMaskTrait`) |
| `answer_archives` | Pivot N:N entre respostas e arquivos; some uma resposta e seus arquivos somem | Manter integridade com `cascadeOnDelete` no novo schema |
| `person_id` nullable | Pedidos antigos sem pessoa | Manter nullable; `masked_name` degrada para null sem quebrar |
| Colunas de `contents` compartilhadas | `contents` é usada por ~25 módulos; migrá-la é um projeto global | Criar tabelas próprias (`transparency_items`, `transparency_categories`) e migrar por script idempotente |

---

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

1. **Escopo real do módulo**: o nome "Transparência Fiscal" sugere despesas, orçamento e
   licitações. Hoje o módulo é apenas um catálogo de links. A prefeitura pretende
   internalizar os dados fiscais (com upload de arquivos) ou manter a integração externa?
2. **Integridade da view SQL**: a tabela `despesas` não existe no banco. A view foi
   desenhada para listar *links* de transparência, não dados orçamentários. Existe algum
   requisito (e.g., metas de receita/despesa previstas na LRF) que exigiria dados próprios?
3. **URLs canônicas**: qual das 9 URIs deve ser a oficial? Há material já publicado
   (impresso, redes sociais, links em portarias) apontando para alguma delas?
4. **`subcategoria`**: a rota `/transparencia/categoria/{slug}` só funciona no `modelo5`.
   Ela é usada de verdade? Se sim, as views devem ser replicadas ou o template ativo
   deve ser fixado.
5. **Prazo legal**: os 20 dias vêm da LAI (e-SIC). O relatório também cobre a Ouvidoria
   (prazo de 20 dias) — a mesma regra vale para todos os portes?
6. **`full_list_requests_disabled`**: quem pode desligar a listagem completa e por quê?
   A configuração está em `category_configurations_id = 1` (categoria geral).
7. **Portal externo (Elmar)**: o catálogo de 33 páginas é fixo no código. O contrato com o
   fornecedor prevê outros portais/entes? Vale abstrair como provider desde já?
8. **Cache de 600 s** das configurações do portal externo: aceitável para a operação ou
   precisa de invalidação imediata?
9. **Relatório estatístico**: há exigência de publicação periodicada (mensal) do arquivo
   gerado? Hoje o download é sob demanda, sem registro.
10. **Ordem (`order`)**: a reordenação é por item dentro da categoria. Ao mover um item
    entre categorias, a posição é recalculada?

---

## 7. Dívidas técnicas prioritárias

| Prioridade | Dívida | Esforço estimado na v7 |
|---|---|---|
| P0 | Corrigir B1 (erro 500 em slug inexistente) | Baixo — `find()` + `abort(404)` |
| P0 | Corrigir B2 (relação `answers` com FK errada) | Baixo — `hasMany(Answer::class, 'informations_request_id', 'id')` |
| P0 | Consolidar as 9 URIs em 1 + 1 com 301 | Médio — tabela de redirecionamentos + testes |
| P0 | Corrigir B4 (views ausentes por template) | Médio — componente único + replicação ou fallback |
| P1 | Corrigir B7 (XSS no modal do relatório) | Baixo — `textContent`/`createElement` no JS |
| P1 | Corrigir B5/B6 (efeito colateral e NPE em `getTemplateActived`) | Baixo — cache da configuração e guarda de nulo |
| P1 | Eliminar a view SQL | Médio — query Eloquent paginada |
| P2 | Corrigir B8 (contadores com `groupBy`) | Médio — reescrever as agregações com `SUM(CASE WHEN ...)` |
| P2 | Eliminar B10 (id literal 25) | Baixo — resolver por slug com exceção explícita |
| P2 | Paginação server-side no DataTable do CMS (B9) | Baixo |
| P3 | Remover views órfãs do template `default` | Baixo |
| P3 | Extrair o relatório estatístico para um controller próprio | Médio |
