# Solicitações LAI (e-SIC) — Rotas e Controllers

> Rotas verificadas com `php artisan route:list` executado dentro do container
> `v6_6f22e1bd-app-1` (imagem da v6) e por leitura de `v6/routes/web.php` e
> `v6/routes/basepro.php`.

---

## 1. Rotas do Portal (público)

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| POST | `/informacao-solicitada` | `requested-information` | `Portal\InformationController@store` | `web`, `check.site.manutention` |
| POST | `/informacao-solicitada-ajax` | `requested-information-ajax` | `Portal\InformationController@storeRequestInformacaoAjax` | `web`, `check.site.manutention` |
| POST | `/cms/ajax/get/information/by/protocol` | `ajax.get.information.by.protocol` | `Portal\InformationController@getInformationByProtocol` | `web` (sem `check.logged`) |
| POST | `/cms/ajax/get/answer/by/information` | `ajax.get.answer.by.information` | `Portal\InformationController@getAnswerByInformation` | `web` (sem `check.logged`) |
| POST | `/AjaxGetanswers` | `get.answers` | `Portal\TransparencyController@getAnswers` | `web`, `check.site.manutention` |
| ANY | `/relatorio-estatistico` | `relatorio-estatistico` | `Portal\TransparencyController@relStatitics` | `web`, `check.site.manutention` |
| ANY | `/pagina/{slug}` | `static.show` | `Portal\PortalController@staticShow` | `web`, `check.site.manutention` |
| ANY | `/acesso-rapido/{slug}` | `acesso-rapido.index` | `Portal\QuickAccessController@index` | `web`, `check.site.manutention` |
| ANY | `/acessos-rapido/{slug}` | `acessos-rapido.index` | `Portal\QuickAccessController@getIndexStatic` | `web`, `check.site.manutention` |

Definições em `v6/routes/web.php:16-17` (e-SIC), `v6/routes/web.php:70`
(`AjaxGetanswers`) e `v6/routes/basepro.php:117` e `v6/routes/basepro.php:119`
(consulta por protocolo, dentro do grupo `prefix => 'ajax'`).

Rotas de página do e-SIC (resolvidas por conteúdo estático, sem controller
próprio):

| Slug (`static_pages.slug`) | Destino | Controller que resolve |
|---|---|---|
| `solicitar-informacao` | `acesso-rapido.index` com slug `solicitar-informacao` | `Portal\PortalController@staticShow` (redireciona) → `Portal\QuickAccessController@getIndexStatic` |
| `acompanhe-sua-solicitacao` | `acesso-rapido.index` com slug `acompanhe-sua-solicitacao` | idem |
| `relatorio-estatistico` | `relatorio-estatistico` | `Portal\PortalController@staticShow` (redireciona) |

> Não existe rota nomeada `solicitar-informacao` no `route:list` (verificado:
> `grep -c 'solicitar-informacao'` retorna 0). O `catch` do
> `Prefecture\InformationsRequestController@store` faz
> `redirect()->route("solicitar-informacao")` — essa rota **não existe** e o
> redirect lançaria `RouteNotFoundException`.

## 2. Rotas do CMS (`v6/routes/basepro.php`)

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/cms/respostas-lai` | `respostas-lai.index` | `Prefecture\InformationsRequestController@index` | `web`, `check.route.permission` |
| GET | `/cms/respostas-lai/create` | `respostas-lai.create` | `Prefecture\InformationsRequestController@create` | `web`, `check.route.permission` |
| POST | `/cms/respostas-lai` | `respostas-lai.store` | `Prefecture\InformationsRequestController@store` | `web`, `check.route.permission` |
| GET | `/cms/respostas-lai/{respostas_lai}` | `respostas-lai.show` | `Prefecture\InformationsRequestController@show` | `web`, `check.route.permission` |
| GET | `/cms/respostas-lai/{respostas_lai}/edit` | `respostas-lai.edit` | `Prefecture\InformationsRequestController@edit` | `web`, `check.route.permission` |
| PUT/PATCH | `/cms/respostas-lai/{respostas_lai}` | `respostas-lai.update` | `Prefecture\InformationsRequestController@update` | `web`, `check.route.permission` |
| DELETE | `/cms/respostas-lai/{respostas_lai}` | `respostas-lai.destroy` | `Prefecture\InformationsRequestController@destroy` | `web`, `check.route.permission` |
| GET | `/cms/solicitacoes/lai/{status}` | `info-request-by-status` | `Prefecture\InformationsRequestController@listByStatus` | `web`, `check.route.permission` |
| GET | `/cms/solicitacoes/{id}/lai/{status}` | `info-request-status-update` | `Prefecture\InformationsRequestController@statusUpdate` | `web`, `check.route.permission` |
| POST | `/cms/answer` | `answer.store` | `Prefecture\AnswerController@store` | `web`, `check.route.permission` |
| GET | `/cms/answer/create/{id}` | `answer-request` | `Prefecture\AnswerController@createByRequest` | `web`, `check.route.permission` |
| GET | `/cms/datatable/em-tramitacao` | `datatable.respostas-lai.indexem-tramitacao` | `Prefecture\DatatableController@get_lai` | `web`, `check.route.permission` |
| GET | `/cms/datatable/atendidos` | `datatable.respostas-lai.indexatendidos` | `Prefecture\DatatableController@get_lai_atendidos` | `web`, `check.route.permission` |
| GET | `/cms/datatable/prorrogados` | `datatable.respostas-lai.indexprorrogados` | `Prefecture\DatatableController@get_lai_prorrogados` | `web`, `check.route.permission` |
| GET | `/cms/datatable/indeferidos` | `datatable.respostas-lai.indexindeferidos` | `Prefecture\DatatableController@get_lai_indeferidos` | `web`, `check.route.permission` |

Referências: `v6/routes/basepro.php:177` (resource `respostas-lai`),
`v6/routes/basepro.php:178` (resource `answer`),
`v6/routes/basepro.php:231` (`answer-request`),
`v6/routes/basepro.php:232` (`info-request-by-status`),
`v6/routes/basepro.php:233` (`info-request-status-update`),
`v6/routes/basepro.php:421-424` (DataTables de LAI).

> Anomalia de nomenclatura: as quatro rotas de DataTable usam
> `->name('respostas-lai.index')->name('em-tramitacao')`. Como
> `Route::name()` **concatena** (`vendor/laravel/framework/src/Illuminate/Routing/Route.php:638-643`),
> o nome final é `datatable.respostas-lai.indexem-tramitacao` — confirmado no
> `route:list`. O nome pretendido provavelmente seria `datatable.em-tramitacao`.

## 3. Permissões exigidas

O `check.route.permission` (`v6/app/Http/Middleware/CheckRoutePermission.php`)
resolve o nome da rota para uma permissão do Sentinel:

| Rota | Permissão esperada |
|---|---|
| `respostas-lai.*` | `respostas-lai.index` / `.create` / `.store` / `.show` / `.edit` / `.update` / `.destroy` |
| `info-request-by-status` | `lai.index` (via `hasAccessSegment()`, que usa o 3º segmento da URL — `lai` em `cms/solicitacoes/lai/{status}`) |
| `info-request-status-update` | **Liberada para qualquer usuário logado** — o nome está na lista `openRoutes` do middleware |
| `datatable.*` | liberada (`datatable` está em `openRoutes`) |
| `answer.*` | `answer.index`, `answer.store`, etc. |
| `ajax.get.information.by.protocol` / `ajax.get.answer.by.information` | nenhuma — são controllers `Portal`, que não aplicam `check.route.permission` |

Permissões cadastradas: `v6/database/seeds/ResourcesTableSeeder.php:315` cria a
resource de slug `lai` e `:324` a de slug `respostas-lai`, ambas com
`modulo_id = 33` (módulo `solicitacoes-lai`).
`v6/database/seeds/RolesTableSeeder.php:88-90` distribui as chaves
`lai.index`, `lai.create`, `lai.edit`, `lai.delete`, `lai.inactive`,
`lai.inactivate`, `lai.restore`, `lai.show` e as equivalentes
`respostas-lai.*`.

O menu superior do CMS (`v6/resources/views/vendor/adminlte/page.blade.php:161`)
usa `has_access(['lai.index'])`, helper definido em
`v6/app/Library/Helper/appHelpers.php:22`.

## 4. Form Requests e validação

### 4.1 `App\Http\Requests\InformationsRequestRequest`

`v6/app/Http/Requests/InformationsRequestRequest.php` — usado **apenas** por
`Portal\InformationController@store` (rota `requested-information`).

| Campo | Regra | Mensagem (customizada) |
|---|---|---|
| `request` | `required\|min:20\|max:4000` | "Digite a sua solicitação." / "Descreva melhor sua solicitação." |
| `email_address` | `required\|email\|max:191` | "Por favor, informe seu email." |
| `protocol` | `unique:informations_requests\|numeric` | "Sua solicitação já foi recebida. Consulte o seu protocolo." |
| `street_name` | `max:191` | |
| `street_number` | `max:10` | |
| `zip_code` | `max:45` | |
| `neighborhood` | `max:191` | |
| `g-recaptcha-response` | `required\|recaptcha` | "Por favor, marque a opção não sou um robô" |
| `person-type` | `required\|boolean` | "Não foi possível recuperar o tipo de pessoa" |
| `cpf` (PF) | `cpf` (regra de `laravellegends/pt-br-validator`) | "Por favor, informe um CPF válido." |
| `rg` (PF) | `max:20` | |
| `name` (PF) | `required\|max:191` | "Por favor, informe seu nome." |
| `cnpj` (PJ) | `cnpj` | "Por favor, informe um CNPJ válido." |
| `social_name` (PJ) | `required` | |

Regras extras presentes apenas na **variante ajax**
(`Portal\InformationController@storeRequestInformacaoAjax`, validação inline
com `validator()`):

- Pessoa Física: `email_address` é `nullable|email` (não `required`);
  `phone_number` é `max:45` (não obrigatório); o reCAPTCHA está **comentado**
  (`//'g-recaptcha-response' => 'required|recaptcha', //DESCOMENTAR QUANDO PUBLICAR`).
- Pessoa Jurídica: `cnpj` `nullable|cnpj|min:14`; `social_name` `required`;
  `phone_number` e `cities_id` comentados.
- O bloco de mensagens define `email_address.required` e `phone_number.required`
  que **nunca** são disparados na variante ajax (as regras são `nullable`).

### 4.2 `App\Http\Requests\AnswerRequest`

`v6/app/Http/Requests/AnswerRequest.php`:

| `type` | Regras |
|---|---|
| `1` (resposta) | `information_request_id` `required`, `type` `required\|boolean`, `answer` `required` |
| `0` (indeferimento) | `information_request_id` `required`, `type` `required\|boolean`, `rejected` `required` |
| qualquer outro | `information_request_id` `required`, `type` `required\|boolean` |

Mensagens: "O campo resposta é obrigatório." / "O campo justificativa é obrigatório."

## 5. Controllers — contratos observados

### 5.1 `Portal\InformationController`

`v6/app/Http/Controllers/Portal/InformationController.php`

| Método | Entrada | Saída | Observações |
|---|---|---|---|
| `store(InformationsRequestRequest $request)` | Form Request | View `portal.layouts.$template.transparencia-fiscal.informacao-solicitada` com `success`/`error` | Grava pessoa/endereço/e-mail/telefone + solicitação dentro de `DB::transaction`; envia 2 e-mails. No `catch (\PDOException)` há um `return 'entrei ';` **antes** do tratamento de erro — código morto. |
| `storeRequestInformacaoAjax(Request $request)` | `Request` cru | JSON `{success, message}` | Duplica o bloco de gravação do `store()`. Em caso de exceção, concatena a exceção na mensagem (`'...'.$e`), expondo stack trace ao cliente. |
| `getInformationByProtocol(Request $request)` | `protocol` | JSON do model (200) ou JSON vazio (404) | `InformationsRequest::with('person')->where('protocol', '=', $request->get('protocol'))->first()`. Não aplica máscara. Verificado: protocolo inexistente → HTTP 404, corpo `{}`. |
| `getAnswerByInformation(Request $request)` | `protocol` | JSON com `answer` + `view` renderizada | Acessa `$informationRequest->id` sem verificar se o registro existe → erro 500 em protocolo inexistente (verificado: `ErrorException: Trying to get property 'id' of non-object` em `InformationController.php:44`). |
| `MaskRequestWithGeminiAi($requestText)` | texto | texto mascarado | Delega a `DataMaskTrait::maskTextWithAi`. |
| `RequestMaskWithRegex($text)` | texto | texto mascarado | Marcado como `@deprecated`; delega a `maskTextDeterministic`. Mantido por compatibilidade com o seeder de backfill. |
| `limpaCPF_CNPJ($valor)` | string | string | Remove `.`, `,`, `-`, `/` e devolve em maiúsculas. |

### 5.2 `Prefecture\InformationsRequestController`

`v6/app/Http/Controllers/Prefecture/InformationsRequestController.php`
(o cabeçalho do arquivo declara *"Este controller esta em off"*).

| Método | Comportamento | Problemas conhecidos |
|---|---|---|
| `index()` | Carrega todas as solicitações com `with('status')->with('answers')->with('person')` e renderiza `prefecture.informations-requests.index` | A view exige a variável `$status` (usada no `id` da tabela e no caminho do JS) e `index()` não a passa → **erro de view** (verificado em execução: `Undefined variable: status`). A relação `answers` também é quebrada. |
| `listByStatus($status)` | Converte 1→`em-tramitacao`, 2→`atendidos`, 3→`indeferidos`, 4→`prorrogados`; chama `menuClicks('lai')`; registra log de atividade; renderiza a mesma view com `status` | Rota que realmente é usada pelo menu. |
| `statusUpdate(int $id, $status)` | Grava `status_id = $status`; se `status == 4` envia o e-mail de prorrogação; redireciona para `info-request-by-status` | **GET** altera estado (não há POST/CSRF); a mensagem de flash é sempre "Solicitação prorrogada com sucesso." mesmo para outros status; o log registra sempre `Status:prorrogada`; o `status` não é validado contra {1,2,3,4}. |
| `show(int $id)` | Carrega a solicitação com `person` e `Answer::with('archive')->where('informations_request_id', $id)`; renderiza `prefecture.informations-requests.show` | A view referencia `$informationRequest->person->addresses->city->name` e `->phones->phone_number` — relações `belongsTo` que podem devolver `null` (erro em view se o titular não tiver endereço/telefone). |
| `create()` | Registra log e renderiza `prefecture.informations-requests.create` | **A view não existe** (verificado com `view()->exists()`: `no`). |
| `store(Request $request)` | Recria pessoa/endereço e grava a solicitação com `protocol = date('YmdHis')`; redireciona para `redirect()->view("portal.layouts.default.pages.requested-information")` | A view de destino **não existe**; usa a chave `person_id` (que não está no `$fillable` do model, cujo campo é `persons_id`); não define `status_id`; o `catch` redireciona para a rota inexistente `solicitar-informacao`. |
| `edit($id)`, `update($request, $id)`, `destroy($id)` | Vazios | Rotas do resource existem mas não fazem nada. |

### 5.3 `Prefecture\AnswerController`

`v6/app/Http/Controllers/Prefecture/AnswerController.php`

| Método | Comportamento |
|---|---|
| `createByRequest(int $idRequest)` | Renderiza `prefecture.answers.create` — **view inexistente** (verificado). |
| `store(AnswerRequest $request)` | `type == 1` → status 2 (Atendido); `type == 0` → status 3 (Indeferido). Grava `Answer`, sanitiza o nome do arquivo (remove acentos, minúsculo, só `[a-zA-Z0-9_ -]`), cria `Archive` com `path = url("storage/answer/{id}/{nome}")`, faz `storeAs("answer/{id}/", $nome)`, vincula via `$answer->archives()->attach(...)`, atualiza `informations_requests.status_id`, registra log de atividade e envia e-mail (`email_answer_attach`, `email_answer_no_attach` ou `email_rejected`), anexando os arquivos ao e-mail. |

### 5.4 `Prefecture\DatatableController` (LAI)

`v6/app/Http/Controllers/Prefecture/DatatableController.php:300-324`

```php
public function get_lai()
{
    $informationRequests =  DB::table("em_tramitacao")->get();
    return DataTables::of($this->struct_views($informationRequests, 'lai'))->make(true);
}
```

As quatro rotinas são idênticas, trocando apenas a view. `struct_views()`
(linha 504) anexa a cada linha as flags `edit`, `delete`, `active` e
`inactivate` conforme `has_access("lai.edit")` etc.

### 5.5 `Portal\TransparencyController` (relatório)

`v6/app/Http/Controllers/Portal/TransparencyController.php`

| Método | Comportamento |
|---|---|
| `relStatitics(Request $request)` | Lê a página estática, incrementa `clicks`, calcula os 8 indicadores de prazo com os scopes do model, monta a lista de anos disponíveis e renderiza `portal.layouts.$template.transparencia-fiscal.relatorio-estatistico.index` com `fullListRequestsDisabled` lido de `full_list_requests_disabled`. Se `type` vier na query, delega a `export()`. |
| `getAnswers(Request $request)` | Devolve `{request: {protocol: masked, mask_request, status_id}, answers: [...]}` — **é o único endpoint que expõe `mask_request`**. |
| `export($informationRequests, $type)` | `xls`/`csv` via `InformationsRequestsExport` (Maatwebsite Excel a partir da view `exports.informations-requests`), `pdf` via `PDF::loadView('exports.informations-requests')` (dompdf) e demais tipos via `exportTxt()`. |
| `exportTxt(...)` | Gera texto com `status->name`, `masked_protocol` e `masked_name` — isto é, **mascarado**. |

## 6. API Resources / presenters

Não há API Resource para a LAI. As respostas JSON são models Eloquent
serializados diretamente (`response()->json($informationRequest)`), o que expõe
todos os atributos do model, incluindo `request` (texto sem máscara), `ip`,
`browser` e a relação `person` (com `cpf`, `rg`, `email`, `telefone`).

O model declara `protected $presenter = ContentPresenter::class;`
(`v6/app/Http/Model/Prefecture/InformationsRequest.php:36`), herdado de um
padrão antigo do framework; não há `getPresenter()` no model, logo o presenter
não é aplicado na serialização.

O model `App\Prefecture\Answer` também não tem `$hidden`: a resposta JSON
inclui `archives_id` e `informations_request_id`.
