# Requerimento de Diárias — Regras e Portabilidade

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

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | O número do requerimento (`codigo_diarias`) é **inteiro** e único na prática (não há constraint UNIQUE no banco) | `2023_01_30_093017_alter_codigo_diarias_to_requerimento_diarias.php` |
| R2 | O CMS sugere o próximo código como `max(codigo_diarias) + 1` — se o maior for deletado (soft delete), o código é reusado | `RequerimentoDiariasController@create` |
| R3 | As datas são gravadas em `Y-m-d`; a entrada do formulário é `dd/mm/aaaa` (máscara `br-data`) e convertida com `str_replace('/', '-')` + `strtotime` | `RequerimentoRepository@structureUpInsert` |
| R4 | `fundamentos_legais` é pré-preenchido com a "lei padrão" da configuração (linha 1) quando o cadastro é novo | `form.blade.php` (`$lei_padrao`) |
| R5 | A configuração é tratada como **singleton na linha `id = 1`** em todos os pontos de leitura (`RequerimentoDiariasConfig::where('id', 1)`) | Controller e Portal |
| R6 | O arquivo da lei padrão é gravado em `storage/app/public/leis_de_requerimentos/` (nome original do arquivo) | `RequerimentoDiariasController@store/@update_configuration` |
| R7 | Marcar "Usar Arquivo de Lei Padrão" **não copia** o arquivo: cria um `archives` apontando para o mesmo `archive` da configuração | `RequerimentoDiariasController@store` |
| R8 | A listagem do CMS lê a **view** `req_diarias` (com `JOIN servidor` e filtro `deleted_at IS NULL`); o Portal lê a **tabela** via Eloquent | `DatatableController@get_requerimentos_diarias` vs `BuscaDiariasController@index` |
| R9 | O Portal pagina em 10 registros e ordena por `created_at DESC` | `BuscaDiariasController@index` |
| R10 | O PDF do Portal mascara **apenas** CPF e identidade; banco, agência, conta, matrícula e cargo ficam expostos | `pdf/index.blade.php:56,61` |
| R11 | O "download_link" da view `req_diarias` aponta para `/cms/requerimento-diarias/{id}` — rota do CMS, que exige login | migration `2023_04_10_100557_create_view_to_requerimentos_diarias.php` |
| R12 | A exclusão é **soft delete** (`$this->table->destroy($id)`) | `RequerimentoDiariasController@destroy` |
| R13 | `destroy_archive` zera `archive`/`path` em **todas** as linhas de `requerimento_diarias_configs` (`::query()->update()`), não apenas na id 1 | `RequerimentoDiariasController@destroy_archive` |
| R14 | O `codigo_diarias` exibido no PDF é `codigo_diarias/ano de created_at` | `pdf/index.blade.php:33` |

## 2. Filtros do Portal — comportamento exato

Fonte: `v6/app/Http/Controllers/Portal/BuscaDiariasController.php:33-44`.

| Filtro | Implementação | Observação |
|---|---|---|
| `req_name` | `Servidor::whereRaw("translate(lower(name), 'àáâãäéèëêíìïîóòõöôúùüûçÇ', 'aaaaaeeeeiiiiooooouuuucc') ilike '%" . $request->req_name . "%'")->pluck('id')` e depois `whereIn('id_servidor', $ids)` | Busca por **substring**, com normalização de acentos |
| `cpf` | Mesmo padrão sobre `cpf` do servidor, mas com o conjunto de caracteres **diferente** (começa com um espaço: `' àáâãä...'`) | A diferença de translate altera o mapeamento das letras acentuadas |
| `num_diaria` | `$diarias->whereRaw("CAST(codigo_diarias AS TEXT) ilike '%" . $request->num_diaria . "%'")` | `CAST` permite busca parcial mesmo com coluna `int` |
| `data_saida` | `$diarias->where("data_saida", ">=", $request->data_saida)` | |
| `data_retorno` | `$diarias->where("data_saida", "<=", $request->data_retorno)` | **Compara contra `data_saida`**, não `data_entrada` |

Observações:

- Os `whereRaw` são aplicados sobre `$diarias` (Builder) — nos dois primeiros
  casos o resultado é reatribuído (`$diarias->whereIn(...)`), nos demais a
  chamada é encadeada e o retorno descartado (`$diarias->where(...)` sem
  atribuição). Funciona porque `where()` muta o builder, mas é frágil.
- A ordem importa: `orderBy('created_at','desc')` é aplicado **antes** do
  `whereIn`, o que é aceito pelo PostgreSQL.

## 3. Armadilhas (risco concreto)

### 3.1 SQL montado por concatenação de string

```php
Servidor::whereRaw("translate(lower(name), ...) ilike '%" . $request->req_name . "%'")
```

Os valores vêm diretamente do request HTTP, sem binding. Um valor contendo
`'` permite fechar a string e injetar SQL — **injeção de SQL**. O mesmo ocorre em
`cpf` e `num_diaria`. A validação `DiariasRequest` cobre apenas
`cpf` (`nullable|min:14`), e `min:14` **não** impede aspas.

**Mitigação na v7:** `whereRaw('... ilike ?', ['%'.$termo.'%'])` com bindings,
ou `whereRaw` + `addBinding`.

### 3.2 Coluna inexistente `padrao_arquivo_config`

- `RequerimentoDiariasController@delete_archives` executa
  `$requerimento->update(['padrao_arquivo_config' => false])`.
- A coluna **não existe** em `requerimento_diarias` (verificado com `\d`) e
  **não está** no `$fillable` do model.

Consequência: o `update()` é silenciosamente ignorado (o atributo é filtrado
pelo mass assignment) e o estado "usa arquivo padrão" **nunca é persistido** —
o checkbox do formulário é sempre reexibido sem estado.

### 3.3 `archive` no `$fillable` sem coluna correspondente

`v6/app/Http/Model/Prefecture/RequerimentoDiarias.php` declara `'archive'` no
`$fillable`, mas a tabela não tem essa coluna. Se o campo chegasse ao
`updateOrCreate`, o PostgreSQL acusaria coluna inexistente.

### 3.4 `update()` exige arquivo sempre

```php
// v6/app/Http/Controllers/Prefecture/RequerimentoDiariasController.php:149-156
public function update(Request $request, $id)
{
    try {
        if ($this->repository->upInsert($request, $id)) {
            $data['title'] = $request->archive->getClientOriginalName();
            $archive = $request->file('archive')->storeAs('leis_de_requerimentos', $data['title']);
```

Se o operador editar um requerimento **sem** anexar novo arquivo,
`$request->archive` é `null` e a chamada gera `Error: Call to a member function
getClientOriginalName() on null`. A exceção é capturada pelo `try/catch` do
controller, que exibe "Ocorreu um erro ao tentar cadastrar o registro." — a
edição **falha silenciosamente** quando não há novo upload.

O formulário só exibe o `<input type="file">` quando **não** existe arquivo
anterior (`@if (isset($arquivo[0]['title']))` … `@else`), o que agrava o caso:
um requerimento com arquivo anexado não tem como ser reeditado.

### 3.5 Caminhos de arquivo inconsistentes

| Contexto | Caminho gravado |
|---|---|
| `store`/`update` (upload) | `storage/app/public/leis_de_requerimento/` + nome (singular) |
| `store`/`update`/`update_configuration` (disco) | `leis_de_requerimentos` (plural) |
| Portal (link) | `url('storage/leis_de_requerimentos', $title)` (plural) |

O `path` gravado no banco aponta para um diretório que **não existe** (singular),
enquanto o link do Portal usa o plural (que existe). O campo `path` é, portanto,
inutilizável para reconstruir a URL.

### 3.6 Identificação do requerimento recém-criado

```php
$requerimento_id = RequerimentoDiarias::latest()->first()->id;
```

Em `store()` e `update()`, o id do requerimento é obtido por
`latest()->first()` (ordena por `created_at`) em vez do retorno do
`updateOrCreate`. Em concorrência, o id pode ser de outro registro.

### 3.7 `Repository@upInsert` faz `dd($th)`

`v6/app/Repositories/Repository.php:67` — no `catch (\Throwable)` o método
executa `dd($th)`, despejando a exceção na tela. Em produção vaza stack trace
e interrompe o fluxo de tratamento de erro do controller.

### 3.8 `destroyAll` — retorno sempre "verdadeiro"

`Repository@destroyAll` devolve `response()->json(true)` ou
`response()->json(false)` — **objetos JsonResponse**, sempre truthy em PHP.
O controller faz:

```php
if ($this->repository->destroyAll($request->toDelete)) { ...sucesso... } else { ...erro... }
```

O ramo de erro é **inalcançável**: mesmo sem deletar nada, o operador recebe
"Requerimentos de diária foram deletados com sucesso."

### 3.9 Controller do Portal estende `Admin\Controller`

`v6/app/Http/Controllers/Portal/BuscaDiariasController.php:13` estende
`App\Http\Controllers\Admin\Controller`, herdando `getTemplateActived()` de um
controller administrativo. Não quebra, mas mistura responsabilidades.

### 3.10 `hasOne` onde deveria ser `belongsTo`

`servidor()` e `estado()` em `RequerimentoDiarias` são `hasOne` com FK local —
o modelo correto seria `belongsTo`. Funciona, mas induz erro em consultas
futuras (`hasOne` gera `WHERE servidor.id = requerimento_diarias.id_servidor`
com LIMIT 1, o que dá o mesmo resultado aqui por acaso).

### 3.11 `Archive` "many-to-one" modelado como `hasOne`

`RequerimentoDiarias::archives()` é `hasOne`, apesar de a tabela `archives`
permitir vários arquivos por requerimento (o `ON DELETE CASCADE` da FK
confirma a relação 1:N). O `with('archives')` do Portal carrega apenas um
arquivo por requerimento.

### 3.12 Rotas destrutivas em GET

`/deletar-arquivo-padrao` e `/deletar-arquivo/{id}` são **GET** com efeito
destrutivo e ficam fora do grupo `/cms` (sem prefixo de instalação). Além do
problema de cache/prefetch, o `check.route.permission` deriva a permissão do
nome da rota (`deletar-arquivo-padrao`, `deletar-arquivo`), slugs que não
existem em `resources` — logo, apenas `super-admin` consegue executá-las
(pelo atalho `Sentinel::inRole('super-admin')` em
`v6/app/Http/Middleware/CheckRoutePermission.php:24`).

### 3.13 `ORDER BY` dentro de view SQL

`req_diarias` termina com `order by rd.codigo_diarias desc`. O PostgreSQL
ignora o `ORDER BY` de uma view quando a consulta não tem `LIMIT`/`OFFSET`; a
ordenação final é a do DataTable.

### 3.14 `RequerimentosConfigsRequest` órfã

`v6/app/Http/Requests/RequerimentosConfigsRequest.php` define regras
(`lei_padrao` required, `archive` required/mimes) que **nunca são aplicadas**,
porque `update_configuration()` recebe `Illuminate\Http\Request` cru.

## 4. Acoplamentos

### Dependências (este módulo → outros)

| Depende de | Tipo | Ponto de acoplamento |
|---|---|---|
| `servidor` | dados | `id_servidor` + `with('servidor')`; `Servidor::whereRaw` no filtro por nome/CPF |
| `states` | dados | `estado_destino` + `with('estado')` |
| `archives` | dados | Anexo da lei (`requerimento_id`) |
| `configurations` | configuração | `favicon`, `estado`, `titulo_site`, `cidade` (PDF) e `layout` (tema do Portal) |
| `modulos` / `resources` / `menu_orders` | metadados | Registro do módulo (36) e permissões |
| Módulo Publicações/Requérimentos (41) | **nomes** | A view `requerimentos` (migrations de 2024-10-08 em diante) não pertence a este módulo, mas compartilha o radical "requerimento" |
| `Portal\RequerimentoController` | **nomes** | Rotas `/requerimentos*` — módulo distinto |

### Dependências (outros → este módulo)

| Quem consome | Como |
|---|---|
| `Prefecture\DatatableController` | Lê a view `req_diarias` |
| `Prefecture\Controller` (base) | Aplica `check.route.permission` a todos os controllers do CMS |
| `AppTrait` | `filter_request` / `traitRequest` usados pelo repositório |
| Layout do Portal | `portal.layouts.<modelo>.layout.layout` + `breadcrumb` |

## 5. Código duplicado / copiado entre v5 e v6

Verificado por `diff` arquivo a arquivo: **todos** os arquivos do módulo são
**byte a byte idênticos** entre `v5/` e `v6/`:

| Arquivo | v5 | v6 | Idêntico |
|---|---|---|---|
| `app/Http/Controllers/Prefecture/RequerimentoDiariasController.php` | sim | sim | sim |
| `app/Http/Controllers/Portal/BuscaDiariasController.php` | sim | sim | sim |
| `app/Http/Model/Prefecture/RequerimentoDiarias.php` | sim | sim | sim |
| `app/Repositories/Eloquent/Prefecture/RequerimentoDiarias/RequerimentoRepository.php` | sim | sim | sim |
| `app/Http/Requests/RequerimentosRequest.php` | sim | sim | sim |
| `app/Http/Requests/DiariasRequest.php` | sim | sim | sim |
| `resources/views/prefecture/contents/requerimento-diarias/**` | sim | sim | sim |
| `resources/views/portal/layouts/busca-diarias/show.blade.php` | sim | sim | sim |
| `public/assets/js/custom/contents/requerimentos_diarias/datatable.js` | sim | sim | sim |

**Duplicação interna:** o bloco de upload de arquivo do controller aparece
quase idêntico em `store()`, `update()` e `update_configuration()`
(`getClientOriginalName` + `storeAs('leis_de_requerimentos', ...)` +
montagem de `path`), sem extração para um trait/serviço.

## 6. Onde a v6 divergiu da v5

| Aspecto | v5 | v6 |
|---|---|---|
| Registro do módulo | `v5/database/seeds/RequerimentoDiariasSeeder.php` grava `menu_orders` e `resources` **sem** `modulo_id` | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` cria `modulos` (id 36); `ResourcesTableSeeder` e `MenuOrderSeeder` gravam com `modulo_id = 36`; o seeder dedicado foi **removido** |
| Tabela `modulos` | **não existe** na v5 | existe na v6 (58 slugs no banco) |
| View `requerimentos` | migrations **inexistentes** na v5 | 4 migrations na v6 (2024-10-08, 2025-08-12, 2026-06-09, 2026-06-12) — módulo Publicações/Requérimentos |
| Rotas | mesmas URIs, outras linhas (`v5/routes/basepro.php:186,220,445,564-568`; `v5/routes/web.php:188-190`) | `v6/routes/basepro.php:205,248,434,526-530`; `v6/routes/web.php:210-212` |
| Código de aplicação | idêntico | idêntico |

**Motivo provável da divergência:** a v6 introduziu o conceito de *módulos*
(`modulos` + `modulo_id`) para organizar o menu e as permissões do CMS; o
módulo de Diárias foi absorvido por esse esquema sem alteração de comportamento.

## 7. Checklist de portabilidade para a v7

- [ ] Eliminar os 3 `whereRaw` com concatenação.
- [ ] Criar a coluna `padrao_arquivo_config` (boolean, default false) ou
      remover o uso.
- [ ] Remover `archive` do `$fillable` ou criar a coluna.
- [ ] Tornar `update()` tolerante à ausência de upload (`hasFile('archive')`).
- [ ] Corrigir o filtro "data retorno" para `data_entrada`.
- [ ] Unificar o diretório de arquivos (`leis_de_requerimentos`) e gravar o
      `path` real.
- [ ] Trocar as rotas destrutivas em GET por `DELETE`/`POST` com CSRF.
- [ ] Substituir a view `req_diarias` por uma query Eloquent.
- [ ] Mascarar banco/agência/conta/matrícula/cargo no PDF do Portal.
- [ ] Corrigir `Repository@upInsert` (`dd` → log/exception) e `destroyAll`
      (retornar bool).
