# Rotas, controladores e validação — Licitações

## 1. Convenção de rotas

`V2/Modules/Publicacoes/Licitacoes/Routes/cms.php` é carregado sob o grupo `/cms` pelo Core, com o prefixo
`cms/v2/licitacoes`; `V2/Modules/Publicacoes/Licitacoes/Routes/web.php` é carregado no portal com as URIs
`/licitacoes` e `/licitacao/{slug}`. O módulo não possui rotas de API.

```php
Route::group([
    'prefix' => 'cms/v2/licitacoes',
    'middleware' => ['v2.authenticate', 'v2.permission:licitacoes,read'],
], function () {
```

(`V2/Modules/Publicacoes/Licitacoes/Routes/cms.php:6-10`)

O prefixo do módulo no menu é declarado em `ModuleProvider::getConfig()`:
`'active_pattern' => 'cms/v2/licitacoes*'` e `'route' => 'v2.licitacoes.index'`.
O middleware `V2PermissionMiddleware` **ignora os parâmetros** `licitacoes,read`
(`V2/Core/Middleware/V2PermissionMiddleware.php:22` não recebe argumentos
extras); ele identifica o módulo pelo `active_pattern` e libera/bloqueia por
método HTTP e nível (`none`/`read`/`write`). Os parâmetros existem apenas como
documentação e continuam presentes nas rotas.

## 2. Rotas do CMS

Todas exigem autenticação. “+write” indica a presença adicional de
`middleware('v2.permission:licitacoes,write')`.

| Método | URI | Nome | Controller@action | Middleware extra |
|---|---|---|---|---|
| GET | `/cms/v2/licitacoes/orgaos` | `v2.licitacoes.orgaos.index` | `Admin\LicitacaoOrgaoController@index` | — |
| POST | `/cms/v2/licitacoes/orgaos` | `v2.licitacoes.orgaos.store` | `Admin\LicitacaoOrgaoController@store` | +write |
| POST | `/cms/v2/licitacoes/orgaos/create-inline` | `v2.licitacoes.orgaos.create-inline` | `Admin\LicitacaoOrgaoController@createInline` | +write |
| POST | `/cms/v2/licitacoes/orgaos/{id}/update` | `v2.licitacoes.orgaos.update` | `Admin\LicitacaoOrgaoController@update` | +write |
| POST | `/cms/v2/licitacoes/orgaos/{id}/delete` | `v2.licitacoes.orgaos.delete` | `Admin\LicitacaoOrgaoController@destroy` | +write |
| POST | `/cms/v2/licitacoes/orgaos/{id}/restore` | `v2.licitacoes.orgaos.restore` | `Admin\LicitacaoOrgaoController@restore` | +write |
| POST | `/cms/v2/licitacoes/orgaos/{id}/toggle` | `v2.licitacoes.orgaos.toggle` | `Admin\LicitacaoOrgaoController@toggle` | +write |
| GET | `/cms/v2/licitacoes` | `v2.licitacoes.index` | `Admin\LicitacaoCmsController@index` | — |
| GET | `/cms/v2/licitacoes/arquivadas` | `v2.licitacoes.archived` | `Admin\LicitacaoCmsController@archived` | — |
| GET | `/cms/v2/licitacoes/create` | `v2.licitacoes.create` | `Admin\LicitacaoCmsController@create` | +write |
| POST | `/cms/v2/licitacoes` | `v2.licitacoes.store` | `Admin\LicitacaoCmsController@store` | +write |
| GET | `/cms/v2/licitacoes/{id}/edit` | `v2.licitacoes.edit` | `Admin\LicitacaoCmsController@edit` | +write |
| POST | `/cms/v2/licitacoes/{id}` | `v2.licitacoes.update` | `Admin\LicitacaoCmsController@update` | +write |
| POST | `/cms/v2/licitacoes/{id}/archive` | `v2.licitacoes.archive` | `Admin\LicitacaoCmsController@destroy` | +write |
| POST | `/cms/v2/licitacoes/{id}/restore` | `v2.licitacoes.restore` | `Admin\LicitacaoCmsController@restore` | +write |
| POST | `/cms/v2/licitacoes/{id}/force-delete` | `v2.licitacoes.force-delete` | `Admin\LicitacaoCmsController@forceDelete` | +write |
| POST | `/cms/v2/licitacoes/{id}/toggle-status` | `v2.licitacoes.toggle-status` | `Admin\LicitacaoCmsController@toggleStatus` | +write |
| POST | `/cms/v2/licitacoes/{licitacaoId}/attachments` | `v2.licitacoes.attachments.upload` | `Admin\LicitacaoCmsController@uploadAttachment` | +write |
| PUT | `/cms/v2/licitacoes/{licitacaoId}/attachments/{anexoId}` | `v2.licitacoes.attachments.update` | `Admin\LicitacaoCmsController@updateAttachment` | +write |
| DELETE | `/cms/v2/licitacoes/{licitacaoId}/attachments/{anexoId}` | `v2.licitacoes.attachments.delete` | `Admin\LicitacaoCmsController@deleteAttachment` | +write |
| POST | `/cms/v2/licitacoes/{licitacaoId}/attachments/reorder` | `v2.licitacoes.attachments.reorder` | `Admin\LicitacaoCmsController@reorderAttachments` | +write |

Linhas: `V2/Modules/Publicacoes/Licitacoes/Routes/cms.php:11-101`.

### 2.1 Ordem de declaração e colisões

- `/arquivadas` e `/create` são declaradas antes de `/{id}/edit`; não há
  captura indevida.
- `POST /{id}` (update) é declarado antes de `POST /{id}/archive`, mas as URIs
  têm número diferente de segmentos, então não colidem.
- `POST /{licitacaoId}/attachments/reorder` é declarada depois de
  `PUT /{licitacaoId}/attachments/{anexoId}`; métodos diferentes, sem colisão.
- Não existe rota `POST /{id}/delete`: o arquivamento usa `/archive`. A view
  `V2/Modules/Publicacoes/Licitacoes/Views/cms/archived.blade.php` usa as rotas corretas.

## 3. Rotas do portal

| Método | URI | Nome | Action | Middleware |
|---|---|---|---|---|
| GET | `/licitacoes` | `v2.licitacoes.portal.index` | `Portal\LicitacaoPortalController@index` | nenhum específico |
| GET | `/licitacao/{slug}` | `v2.licitacoes.portal.show` | `Portal\LicitacaoPortalController@show` | nenhum específico |

Código: `V2/Modules/Publicacoes/Licitacoes/Routes/web.php:5-9`. Não há `throttle`, cache explícito, API JSON,
sitemap ou feed. A rota de detalhe usa `{slug}` sem expressão regular; slugs
com caracteres inesperados caem no `firstOrFail()` e devolvem 404.

## 4. Controlador principal — ações e efeitos

`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php`.

| Action | Linha | Lê | Escreve | Resposta |
|---|---|---|---|---|
| `index` | 40 | `search`, `sort`, `dir` | — | view `cms.index`, paginador com `appends` |
| `archived` | 72 | `search`, `sort`, `dir` | — | view `cms.archived`, `onlyTrashed()->paginate(20)` **sem** `appends` |
| `create` | 97 | — | — | view `cms.create` com `new Licitacao()` e órgãos ativos |
| `store` | 112 | FormRequest validado | `Licitacao::create`, `orgaos()->sync`, auditoria, log | redirect `v2.licitacoes.index` |
| `edit` | 139 | `id` | — | view `cms.edit` com órgãos e anexos |
| `update` | 157 | FormRequest validado + `id` | `update`, `sync`, auditoria, log | redirect índice |
| `destroy` | 180 | `id` | `delete()` + auditoria | redirect índice |
| `restore` | 201 | `id` | `restore()` + auditoria | redirect `archived` |
| `forceDelete` | 220 | `id` | apaga arquivos dos anexos visíveis, `forceDelete()`, auditoria | redirect `archived` |
| `toggleStatus` | 246 | `id` | alterna `is_active`, auditoria, log | redirect `back()` |
| `uploadAttachment` | 269 | `file`, `title` | delega ao serviço | redirect `back()` com sucesso/erro |
| `updateAttachment` | 308 | `title` | delega `updateTitle` | redirect `back()` |
| `deleteAttachment` | 330 | ids | delega `delete` | redirect `back()` |
| `reorderAttachments` | 347 | `order[]` | delega `reorder` | JSON `{success:true}` |

### 4.1 Allowlist de ordenação

```php
$search = $r->get('search', '');
$sort = $r->get('sort', 'created_at');
$dir = $r->get('dir', 'desc');
$allowed = ['number', 'title', 'created_at', 'status_id', 'is_active'];
$sort = in_array($sort, $allowed) ? $sort : 'created_at';
$dir = in_array($dir, ['asc', 'desc']) ? $dir : 'desc';
```

(`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:46-51`)

A allowlist impede SQL injection pelo nome da coluna, mas **não** existe
validação equivalente em `archived` para o parâmetro, embora o código seja
copiado (`:78-83`). `appends($r->query())` preserva a query string na
paginação do índice, mas falta em `archived`.

### 4.2 Store e update

```php
$data = $r->validated();
$data['id'] = \v2_uuid();

$licitacao = Licitacao::create($data);

// Sync orgaos
if ($r->has('orgao_ids')) {
    $licitacao->orgaos()->sync($r->input('orgao_ids', []));
}
```

(`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:117-123`)

No `store`, `orgao_ids` **não** é removido de `$data` antes do `create`, mas
não está em `$fillable`, então é descartado silenciosamente. No `update`, o
`sync` usa `$r->input('orgao_ids', [])` **sem** condicional: se o campo não for
enviado, todas as associações são removidas. O formulário sempre envia os
checkboxes existentes, mas um POST parcial (API/teste) apaga vínculos.

### 4.3 Ciclo arquivar/restaurar/excluir

- `destroy` executa soft delete; os anexos **permanecem** acessíveis pela
  relação (a FK cascade só vale para hard delete).
- `restore` usa `withTrashed()->findOrFail($id)` e chama `restore()`.
- `forceDelete` usa `withTrashed()`, percorre `$licitacao->anexos` (apenas não
  deletados), apaga cada `file_path` do disco `public` e chama
  `forceDelete()`. A FK cascade remove registros de anexos e o pivô.
- `toggleStatus` não valida se a licitação está arquivada; pode alterar
  `is_active` de uma linha em soft delete.

### 4.4 Anexos

`uploadAttachment` valida `file` (`required|file|max:51200` — 50 MB) e `title`
(`nullable|string|max:255`). O `order` enviado ao serviço é
`$licitacao->anexos()->count()`, ou seja, a posição é o total atual. Como o
plural não inclui soft-deleted, reenviar após exclusões pode repetir a ordem.

`updateAttachment` busca o anexo com
`LicitacaoAnexo::where('licitacao_id', $licitacaoId)->findOrFail($anexoId)`,
o que impede editar anexo de outra licitação. O mesmo padrão é usado no
`deleteAttachment`. O `reorderAttachments` valida
`order` (`required|array`) e `order.*` (`string|exists:v2_licitacao_anexos,id`),
mas **não** valida que cada id pertence à licitação; o serviço filtra por
`licitacao_id` no update, então ids de outra licitação são ignorados em
silêncio.

## 5. Controlador de órgãos

`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoOrgaoController.php`.

| Action | Linha | Comportamento |
|---|---|---|
| `index` | 15 | `withTrashed()->orderBy('name')->get()`, view `cms.orgaos.index` |
| `store` | 29 | valida `name` único, cria com `is_active=true`, redirect índice |
| `update` | 52 | `withTrashed()->findOrFail`, valida `name` único ignorando o id, atualiza, redirect |
| `destroy` | 74 | `findOrFail` (não acha já deletado), `delete()` (soft) |
| `restore` | 91 | `withTrashed()->findOrFail`, `restore()` |
| `toggle` | 108 | `withTrashed()->findOrFail`, alterna `is_active` |
| `createInline` | 124 | valida `name` único, cria, responde `{success:true, orgao:{...}}` |

Todas as ações checam `v2_can('licitacoes','write')`, exceto `index`, que checa
`read`. Nenhuma audita. `destroy` não remove o vínculo com licitações; o pivô
continua apontando para o órgão soft-deleted, e a relação `orgaos` do model
**não** filtra `deleted_at`, então uma licitação continua exibindo um órgão
removido.

## 6. Form Request

`V2/Modules/Publicacoes/Licitacoes/Requests/LicitacaoRequest.php`.

### 6.1 Autorização

```php
public function authorize()
{
    return \v2_can('licitacoes', 'write');
}
```

(`V2/Modules/Publicacoes/Licitacoes/Requests/LicitacaoRequest.php:14-17`)

### 6.2 Regras

| Campo | Regras | Efeito |
|---|---|---|
| `number` | `required|string|max:50` | obrigatório, sem unicidade |
| `title` | `required|string|max:255` | obrigatório |
| `slug` | `nullable|string|max:255|unique:v2_licitacoes,slug,{id},id` | único, ignorando o id da rota |
| `object` | `required|string` | obrigatório |
| `process_number` | `nullable|string|max:100` | |
| `value` | `nullable|numeric|min:0` | aceita `null`; mutator converte em 0 |
| `show_value` | `boolean` | checkbox com hidden `0` |
| `publication_edital_date` | `nullable|date` | aceita qualquer formato de data |
| `competition_date` | `nullable|date` | |
| `homologation_date` | `nullable|date` | |
| `is_active` | `boolean` | checkbox com hidden `0` |
| `reason` | `nullable|string` | sem `max` |
| `status_id` | `nullable|string|exists:v2_licitacao_status,id` | |
| `local` | `nullable|string|max:500` | |
| `management_unit` | `nullable|string|max:191` | |
| `orgao_ids` | `nullable|array` | |
| `orgao_ids.*` | `string|exists:v2_licitacao_orgaos,id` | ids existentes, inclusive soft-deleted |

O `slug` é validado como `nullable`, mas o formulário exibe o campo com o valor
atual; deixar vazio faz o model gerar na criação. Em edição, limpar o campo
mantém o slug antigo? Não: `validated()` inclui `slug => null` e o
`updating` só regenera se `number`/`title` estiverem sujos e `slug` não estiver
sujo. Se o slug for enviado como `null`, ele entra em `isDirty('slug')`, então
o model **não** regenera e grava `null`, quebrando a URL pública
(`public_url`). É um caso de borda não testado.

### 6.3 Mensagens

Somente quatro mensagens customizadas (`number.required`, `title.required`,
`object.required`, `slug.unique`); o restante usa as mensagens padrão do
Laravel 5.5.

## 7. API Resources e presenters

Não existem. As respostas são:

- HTML (views Blade);
- `RedirectResponse` com flash `success`/`error`;
- JSON ad hoc em dois pontos: `createInline` devolve o model inteiro
  (`{success, orgao}`) e `reorderAttachments` devolve `{success:true}`.

Não há presenter, transformer, Resource, serialização de datas ou contrato de
API. Isso é relevante para o V7: qualquer consumo externo precisará de um
endpoint novo.

## 8. Permissões exigidas — resumo

| Operação | Nível exigido | Onde é checado |
|---|---|---|
| ver listagem/arquivadas/órgãos | `read` | middleware + `v2_can('licitacoes','read')` |
| criar/editar/arquivar/restaurar/excluir | `write` | middleware + `v2_can('licitacoes','write')` + `LicitacaoRequest::authorize` |
| anexos (upload/update/delete/reorder) | `write` | middleware + `v2_can` |
| órgãos (store/update/delete/restore/toggle/inline) | `write` | middleware + `v2_can` |

O portal não exige autenticação; a visibilidade é controlada por `is_active`
e soft delete. Não há escopo por unidade gestora, órgão ou regional; um perfil
com `write` administra todas as licitações.
