# Solicitações LAI (e-SIC) — Gaps, Riscos e Dividas

---

## 1. Bugs e erros conhecidos

### 1.1 Segurança e privacidade (prioridade alta)

| # | Problema | Evidência | Impacto |
|---|---|---|---|
| S1 | `getInformationByProtocol` devolve o model Eloquent **completo** | `v6/app/Http/Controllers/Portal/InformationController.php:29-40`; verificado em execução (HTTP 200 com o JSON do model) | Qualquer visitante que descubra o protocolo obtém o **texto original do pedido**, o IP, o User-Agent e os dados do titular (CPF, RG, e-mail, telefone, endereço). O módulo já tem `mask_request` justamente para evitar isso — e não o usa nesse endpoint. |
| S2 | `getAnswerByInformation` não verifica se o pedido existe | `v6/app/Http/Controllers/Portal/InformationController.php:44`; verificado: erro 500 com `Trying to get property 'id' of non-object` | Erro 500 (e stack trace) para protocolo inexistente. |
| S3 | Stack trace exposto ao cliente | `v6/app/Exceptions/Handler.php:50` concatena a exceção na mensagem JSON; também em `InformationController@storeRequestInformacaoAjax` (`'...'.$e`) | Vazamento de caminho absoluto, linhas de código e estrutura interna. |
| S4 | Protocolo é um carimbo de data/hora | `date('YmdHis')` no Blade (`solicitacao-form.blade.php:16`) | Protocolos de janelas temporais conhecidas são enumeráveis; combinado com S1, permite varredura de pedidos e dados pessoais. |
| S5 | Alteração de status por `GET` | `v6/routes/basepro.php:233` → `InformationsRequestController@statusUpdate` | Sem token CSRF; um link (ou pré-carregamento) muda o status de uma solicitação. |
| S6 | `info-request-status-update` está em `openRoutes` | `v6/app/Http/Middleware/CheckRoutePermission.php:52-59` | Qualquer usuário autenticado no CMS (inclusive com papel mínimo) altera o status de qualquer solicitação. |
| S7 | `status` não validado na alteração | `InformationsRequestController@statusUpdate` grava o valor recebido | É possível gravar `status_id` inválido (ex.: 99) ou o status de outro módulo (5, 6, 7). |
| S8 | ReCAPTCHA desativado na via ajax | `InformationController@storeRequestInformacaoAjax` (regra comentada) | A rota `requested-information-ajax` aceita submissões automatizadas sem desafio. |
| S9 | Anexos com URL pública e previsível | `AnswerController@store`: `path = url("storage/answer/{id}/{nome}")` | Os arquivos de resposta ficam sob `public/storage` com caminho adivinhável (`id` sequencial). |
| S10 | Exceções de validação do ajax com `email_address` opcional | via ajax `email_address` é `nullable`, mas as mensagens dizem "required" | Inconsistência entre as duas vias de entrada. |

### 1.2 Funcionalidade quebrada

| # | Problema | Evidência | Impacto |
|---|---|---|---|
| B1 | `InformationsRequestController@index` renderiza a view sem `$status` | `v6/app/Http/Controllers/Prefecture/InformationsRequestController.php:33-38`; verificado: `Undefined variable: status` | `GET /cms/respostas-lai` (rota `respostas-lai.index`) **quebra**. O caminho em uso é `listByStatus`. |
| B2 | `InformationsRequestController@create` aponta para view inexistente | `view()->exists('prefecture.informations-requests.create')` → `no` | `GET /cms/respostas-lai/create` **quebra**. |
| B3 | `InformationsRequestController@store` redireciona para view inexistente | `redirect()->view("portal.layouts.default.pages.requested-information")`; `view()->exists()` → `no` | `POST /cms/respostas-lai` **quebra**. |
| B4 | O mesmo `store()` redireciona para uma rota inexistente no `catch` | `redirect()->route("solicitar-informacao")`; `route:list` não tem essa rota | O tratamento de erro lançaria `RouteNotFoundException`. |
| B5 | `InformationsRequestController@store` grava `person_id` (chave inexistente) | `v6/app/Http/Controllers/Prefecture/InformationsRequestController.php:163,185`; o `$fillable` do model tem `persons_id` | O titular não é vinculado (`persons_id` fica nulo) e a solicitação não aparece nas listagens (as vistas SQL usam `inner join persons`). |
| B6 | `InformationsRequestController@store` não define `status_id` | idem | A solicitação fica sem status e não aparece em nenhuma listagem do CMS. |
| B7 | `AnswerController@createByRequest` aponta para view inexistente | `view()->exists('prefecture.answers.create')` → `no` | `GET /cms/answer/create/{id}` **quebra**. |
| B8 | `edit`, `update`, `destroy` do CMS estão vazios | `v6/app/Http/Controllers/Prefecture/InformationsRequestController.php` | Rotas do resource existem e não fazem nada (retornam null). |
| B9 | `view()->exists('portal.layouts.default.transparencia-fiscal.informacao-solicitada')` → `no` | o controller monta `portal.layouts.$template.transparencia-fiscal.informacao-solicitada` | Com o template `default` ativo, o `store()` do portal **quebra** após gravar. O template ativo no banco é `modelo1`, onde o caminho existe. |
| B10 | Script inexistente referenciado no portal | `v6/resources/views/portal/layouts/newsbit/informacoes-instituicao/acesso-rapido/solicitar-informacao.blade.php:96` → `assets/js/custom/portal/lai/state.js` (arquivo ausente) | 404 de asset no console. |
| B11 | `return 'entrei ';` antes do tratamento de erro | `v6/app/Http/Controllers/Portal/InformationController.php` (`catch (\PDOException)`) | Código morto: a tela de erro nunca é exibida nesse caminho. |
| B12 | `answers()` do model aponta para coluna inexistente | `v6/app/Http/Model/Prefecture/InformationsRequest.php:81-84` (`hasMany(Answer::class,'id','answers_id')`); verificado: binding `null` e coluna `answers_id` inexistente | `with('answers')` **nunca** retorna respostas — afeta `InformationsRequestController@index` e `GetInformationsPrefectureTrait`. |
| B13 | `Answer::statuses()` aponta para `answers.statuses_id`, coluna inexistente | `v6/app/Http/Model/Prefecture/Answer.php` | Relação quebrada (não usada pelo fluxo atual, mas presente no `$fillable`). |
| B14 | `Person::addresses()/emails()/phones()` são `belongsTo` | `v6/app/Http/Model/Admin/Person.php:41-60` | Devolvem um único registro; titulares com mais de um endereço mostram só o primeiro. |
| B15 | `scopeGetByBetweenDate` usa `groupBy('status_id')` sobre `select *` | `v6/app/Http/Model/Prefecture/InformationsRequest.php:112-118`; verificado no PostgreSQL: `column "informations_requests.id" must appear in the GROUP BY clause` | A consulta só funciona encapsulada em `->count()`; chamada diretamente, quebra. |
| B16 | `status()` e `person()` como `hasOne` com argumentos invertidos | `v6/app/Http/Model/Prefecture/InformationsRequest.php:46-58` | Funciona por coincidência (os ids coincidem), mas é frágil e enganoso. |
| B17 | O template `default` do portal mostra protocolo e nome **sem máscara** na listagem do relatório | `v6/resources/views/portal/layouts/default/pages/statics/transparencia-fiscal/relatorio-estatistico.blade.php` (usa `protocol` e `person->name`), enquanto `modelo1..6` usam `masked_protocol`/`masked_name` | Inconsistência de LGPD entre templates. |
| B18 | Flash message e log de atividade fixos na prorrogação | `InformationsRequestController@statusUpdate` | Mensagem "Solicitação prorrogada com sucesso." mesmo para outros status; log sempre `Status:prorrogada`. |
| B19 | `AnswerController` da v6 perdeu o fallback de usuário | `v5` usava `$user ? $user->name : 'Sistema'`; a v6 usa `Sentinel::getUser()->name` direto | Sem usuário autenticado, o acesso a `->name` gera erro. |
| B20 | `AnswerController` da v6 perdeu o `Log::error` nos `catch` | diff entre v5 e v6 | Falhas de banco deixa de ser registradas no log da aplicação. |
| B21 | Duplicação de titulares | `InformationController@store` cria `persons`/`addresses`/`emails`/`phones` novos a cada pedido | Crescimento da base sem reaproveitamento por CPF/CNPJ. |

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| `respostas-lai.create` / `.store` / `.edit` / `.update` / `.destroy` | Rotas vivas, código vazio ou quebrado (B2, B3, B4, B8). Devem ser removidas. |
| `answer-request` (`GET cms/answer/create/{id}`) | Aponta para view inexistente (B7). |
| `InformationController@storeRequestInformacaoAjax` | Endpoint completo, mas **nenhuma view ou JS da v6 o consome** (verificado por busca em `resources/` e `public/`). O `Form::open` de todos os formulários aponta para `requested-information`. |
| `InformationController::RequestMaskWithRegex` | Marcado `@deprecated`; só é usado pelo seeder de backfill. |
| `getDisplayRelatoriosLai()` | `v6/app/Helpers/functions.php:62-71` lê a chave `relatorio_completolai`; **não é chamada** em nenhum lugar, e a chave não existe na tabela `configurations` do banco (o `value` lido seria erro de índice). A chave efetivamente usada é `full_list_requests_disabled`. |
| `InformationsRequest::getMaskedNameAttribute()` | Definido, mas as listagens do CMS (vistas SQL) usam o nome em claro — o accessor só é usado no relatório público. |
| `InformationController@getAnswerByInformation` | O próprio código traz o comentário *"VERIFICAR ESSE MÉTODO AQUI A VIEW AINDA NAO EXISTE NO SNOVOS TEMPLATES"*. |
| `Archive` do model `Answer` (`answers.archives_id`) | Coluna praticamente sempre nula; o upload usa o pivot `answer_archives`. |
| `Configuration` `relatorio_completolai` | Criada pelo seeder `UpdateConfigurationOrderSeederRelatorioLai` mas nunca lida pelo código. |
| `response_time` ("Prazo de resposta em dias") | Configurada e exibida no CMS, mas **não afeta a LAI** — só a Ouvidoria. |

## 3. Itens existentes na v5 e removidos na v6 (e o contrário)

| Item | v5 | v6 |
|---|---|---|
| `modulos` (registry de módulos) | Tabela **inexistente** | Tabela com 58 slugs, incluindo `solicitacoes-lai` (id 33) |
| `resources.modulo_id` | Coluna **inexistente** | Coluna presente; `lai` (id 37) e `respostas-lai` (id 38) apontam para o módulo 33 |
| Guarda `hasColumn` na migration de `mask_request` | **Presente** | **Ausente** — a migration v6 falha se a coluna já existir |
| `Log::error` no `AnswerController` | Presente | Removido |
| Fallback de usuário no log do `AnswerController` | Presente (`'Sistema'`) | Removido |
| `fieldBetween` para status 1/2/3 | `updated_at` | `created_at` (correção) |
| `limpaCPF_CNPJ` do controller do portal | Sem `strtoupper` | Com `strtoupper` |
| Helper global `limpaCPF_CNPJ` | `preg_replace('/[^A-Za-z0-9]/','',$valor)` | `str_replace` de `.`, `,`, `-`, `/` — **não** remove outros caracteres especiais |
| View de exportação | `exports/information-requests.blade.php` | `exports/informations-requests.blade.php` |
| Rota de menu do CMS | idem | idem (só mudou o número de linha) |
| Demais controllers/views do módulo | idem | idem (`diff` sem diferenças) |

## 4. Mapa v5 -> v6

### 4.1 Estrutura de arquivos

| Conceito | v5 | v6 |
|---|---|---|
| Controller do portal | `v5/app/Http/Controllers/Portal/InformationController.php` (400 linhas) | `v6/app/Http/Controllers/Portal/InformationController.php` (407 linhas) |
| Controller do CMS | `v5/app/Http/Controllers/Prefecture/InformationsRequestController.php` | `v6/app/Http/Controllers/Prefecture/InformationsRequestController.php` (idêntico) |
| Model | `v5/app/Http/Model/Prefecture/InformationsRequest.php` | `v6/app/Http/Model/Prefecture/InformationsRequest.php` |
| Trait de mascaramento | `v5/app/Http/Traits/DataMaskTrait.php` | `v6/app/Http/Traits/DataMaskTrait.php` (mesmas funções: `maskTextDeterministic`, `maskSensitiveDictionary`, `maskTextWithAi`, `maskProtocol`, `maskName`) |
| Migration da tabela | `v5/database/migrations/2018_04_13_192114_create_informations_requests_table.php` | idem na v6 |
| Migration de `mask_request` | `v5/database/migrations/2025_10_13_161153_add_mask_request_to_informations_request_table.php` (**13/10/2025**) | `v6/database/migrations/2025_10_14_154644_add_mask_request_to_informations_request_table.php` (**14/10/2025**) |
| Seeder de dados de teste | `v5/database/seeds/LaiCaturiteSeeder.php` | idem na v6 |
| Seeder de configuração | `v5/database/seeds/UpdateConfigurationOrderSeederRelatorioLai.php` | idem na v6 |
| Seeder de backfill | `v5/database/seeds/UpdateRequestToMaskRequestInInformationRequest.php` | idem na v6 |
| Form Request | `v6/app/Http/Requests/InformationsRequestRequest.php` | idem |
| JS das DataTables | `v5/public/assets/js/custom/contents/solicitacao-lai/**` | idem (diff sem diferenças) |
| Views do CMS | `v5/resources/views/prefecture/informations-requests/**` | idem |
| Views do portal | `v5/resources/views/portal/layouts/**` (LAI) | idem |

> **Atenção — datas diferentes da migration:** o mesmo efeito (coluna
> `mask_request`) está em **13/10/2025** na v5 e em **14/10/2025** na v6, com
> nomes de arquivo e de classe idênticos (`AddMaskRequestToInformationsRequestTable`).
> Consequências: (a) quem executou a v5 e depois a v6 no mesmo banco recebe
> erro de migration "column already exists" (a v5 tem guarda `hasColumn`, a v6
> não); (b) o seeder de backfill das duas versões usa o corte
> `created_at < '2025-10-14 00:00:00'`, alinhado à data da v6 e **um dia
> adiantado** em relação à migration da v5.

### 4.2 Rotas (mesmas URIs e nomes nas duas versões)

| Conceito | v5 (linhas em `v5/routes/`) | v6 (linhas em `v6/routes/`) |
|---|---|---|
| `POST /informacao-solicitada` | `web.php:24` | `web.php:16` |
| `POST /informacao-solicitada-ajax` | `web.php:25` | `web.php:17` |
| `POST cms/ajax/get/information/by/protocol` | `basepro.php:107` | `basepro.php:117` |
| `POST cms/ajax/get/answer/by/information` | `basepro.php:109` | `basepro.php:119` |
| `Route::resource('respostas-lai')` | `basepro.php:163` | `basepro.php:177` |
| `GET cms/solicitacoes/lai/{status}` | `basepro.php:204` | `basepro.php:232` |
| `GET cms/solicitacoes/{id}/lai/{status}` | `basepro.php:205` | `basepro.php:233` |
| DataTables de LAI | `basepro.php:432-435` | `basepro.php:421-424` |

### 4.3 Banco de dados

| Objeto | v5 | v6 |
|---|---|---|
| `informations_requests` | mesmas colunas, mesmas FKs (`persons`, `statuses`), `mask_request` presente | idêntico (verificado com `\d` nos dois bancos) |
| `answers`, `answer_archives`, `persons`, `addresses`, `emails`, `phones`, `statuses`, `status_categories` | idênticos | idênticos |
| Vistas `em_tramitacao`, `atendidos`, `indeferidos`, `prorrogados` | presentes, mesma definição | presentes, mesma definição |
| `modulos` | **não existe** | 58 slugs; `solicitacoes-lai` = id 33 |
| `resources.modulo_id` | **não existe** | existe; `lai` (37) e `respostas-lai` (38) → módulo 33 |
| `menu_orders.modulo_id` | **não existe** | existe; `lai` → módulo 33 |

### 4.4 Comportamento observável

| Comportamento | v5 | v6 |
|---|---|---|
| Prazo legal (20 dias) | idem | idem |
| Estados 1..4 | idem | idem |
| Mascaramento do texto do pedido | `mask_request` gravado na criação (IA + determinístico) | idem |
| Backfill do mascaramento | mesmo seeder, mesmo corte `2025-10-14` | idem |
| Relatório estatístico | idem | idem |
| Exportações xls/csv/pdf/txt | idem | idem |
| Filtro de ano do relatório (status 1/2/3) | por `updated_at` (bug) | por `created_at` (corrigido) |
| Limpeza de CPF/CNPJ no controller do portal | preserva o caso | converte para maiúsculas |
| Helper global `limpaCPF_CNPJ` | remove todo caractere não alfanumérico | remove apenas `.`, `,`, `-`, `/` |
| Falhas de banco no `AnswerController` | registradas em `Log::error` | não registradas |
| Log de atividade sem usuário | usa "Sistema" | usa `Sentinel::getUser()->name` (erro se ausente) |

**Conclusão do mapa:** a v6 é essencialmente a v5 com o registro de módulos
(`modulos`/`resources.modulo_id`/`menu_orders.modulo_id`) e algumas correções
pontuais. O módulo **não** ganhou functionality nova — a parte mais relevante
(views, controllers, rotas, JS e estrutura de banco) é idêntica. A diferença
de data na migration de `mask_request` (13/10 na v5, 14/10 na v6) é o item que
exige atenção em qualquer ambiente que migre de uma para a outra.

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

| Risco | Impacto | Mitigação |
|---|---|---|
| Coluna `mask_request` já existente ao rodar a migration da v6 | migration falha (`column "mask_request" already exists`) | Recriar a migration com `Schema::hasColumn()` (como a v5) ou marcar a v6 como já executada |
| `protocol` sem índice único | duplicidade de protocolos sob concorrência (o `unique` é só da aplicação) | Índice único em `protocol` na v7 |
| `persons` duplicado por pedido | crescimento e inconsistência de CPF/CNPJ | Estratégia de deduplicação definida com o negócio antes de migrar |
| `answers.archives_id` x `answer_archives` | duas fontes de vínculo de arquivo | Consolidar no pivot; remover a coluna em migration de dados |
| `statuses` compartilhada entre LAI (1..4) e Licitações (5..7) | colisão de ids ao recriar seeds | Criar tabela própria ou fixar os ids por seed idempotente |
| Ausência de `due_at`/`answered_at` | impossível recalcular o histórico de prazo após a migração | Calcular `due_at` a partir de `created_at + 20 dias` e `answered_at` a partir do `min(created_at)` das respostas na migration de backfill |
| `timestamp without time zone` | ambiguidade de fuso nos relatórios (`now() - created_at`) | Migrar para `timestamptz` com conversão explicita |
| `mask_request` nulo em registros antigos | o relatório público pode exibir texto não mascarado | Backfill determinístico (o seeder existente) + backfill da IA a partir de `mask_request IS NULL` |

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

1. **Prazo de prorrogação:** a Lei 12.527/2011 (art. 19, §3º) permite prorrogar
   por mais 10 dias com justificativa. O sistema não tem campo de nova data
   nem de justificativa — a prorrogação reinicia a contagem a partir de
   `updated_at` com os mesmos 20 dias. O prazo correto deve ser 20 + 10?
2. **Sigilo e anonymity:** o formulário do portal **não** oferece a opção
   "sigiloso" nem "anônimo", embora `maskName()` já trate essas modalidades.
   O e-SIC oficial permite pedido anônimo. Deve ser incluído?
3. **Prazo configurável:** a chave `response_time` existe e é usada pela
   Ouvidoria. Deve ser compartilhada com a LAI?
4. **Prazo de resposta de segunda via (e-mail):** a mensagem diz
   "retornaremos seu contato" — o portal promete resposta, e-mail ou
   ambos?
5. **Exposição do texto original:** o acompanhamento por protocolo mostra o
   texto **sem máscara** e o nome completo do titular. Deve mostrar apenas a
   versão mascarada, mesmo para quem digitou o protocolo?
6. **Notificação ao solicitante:** só o e-mail cadastrado é notificado. Deve
   haver notificação por SMS/WhatsApp ou por push no portal?
7. **Reuso de titular:** duplicar a `persons` a cada pedido é intencional
   (histórico) ou um defeito a corrigir?
8. **Estatística pública:** o Relatório Estatístico pode ser desligado
   (`full_list_requests_disabled`), mas o **gráfico** de prazo não tem
   interrupção. Mantém-se assim?
9. **Anexos de resposta:** devem ficar em área pública (`public/storage`) ou
   restritos a quem tem o protocolo?
10. **Prazo de retenção:** existe alguma política de expurgo de
    `informations_requests`, `answers` e anexos?
11. **Migração v5 -> v6 da coluna `mask_request`:** em ambientes que já rodaram
    a migration da v5, a da v6 deve ser marcada como executada — há
    procedimiento de deploy documentado?
12. **Widget do dashboard:** `LaiWidget` mostra a distribuição por status. Deve
    mostrar também o tempo médio de resposta?

## 7. Itens que não foram verificados

- Não há dados de produção disponíveis: as tabelas `informations_requests`,
  `answers`, `persons` (1 linha) e `statuses` do banco de desenvolvimento
  praticamente não têm dados, o que limita verificações de volume e de
  desempenho.
- As **views SQL** foram lidas de `pg_views` e das migrations; não foi
  executado `EXPLAIN ANALYZE` sobre elas com volume real.
- O comportamento de `spatie/laravel-activitylog` (tabela `activity_log`) não
  foi inspecionado no banco.
- A configuração de envio de e-mail em produção (SMTP, remetente, SPF/DKIM)
  não foi verificada; o ambiente local usa MailHog.
- A regra `cpf`/`cnpj` vem do pacote `laravellegends/pt-br-validator`; não
  foi auditado o comportamento dela para valores limpos.
- Não foi verificado se há Other CMS (outros tenants/instalações) consumindo as
  vistas SQL `em_tramitacao`, `atendidos`, `indeferidos` e `prorrogados`
  diretamente.
