# Legislação — Rotas e Controllers

> **Slug:** `Legislacao` · Rotas carregadas por `V2ServiceProvider` a partir de
> `Routes/web.php` e `Routes/cms.php`.
>
> Caminhos relativos a `lotep/`.

---

## 1. Superfícies

| Superfície | Arquivo | Prefixo | Autenticação |
|---|---|---|---|
| CMS | `V2/Modules/Publicacoes/Legislacao/Routes/cms.php` | `cms/v2/legislacao` | `v2.authenticate` + `v2.permission:Legislacao,read` |
| Portal | `V2/Modules/Publicacoes/Legislacao/Routes/web.php` | `/legislacao` | pública |
| API | — | — | não existe no módulo |

O `ModuleProvider` aponta o item do sidebar para `v2.legislacao.items.index` e
marca `active_pattern = 'cms/v2/legislacao*'`
(`V2/Modules/Publicacoes/Legislacao/ModuleProvider.php:15-24`).

## 2. Rotas do CMS

| Método | URI | Nome | Controller@action | Permissão extra |
|---|---|---|---|---|
| GET | `cms/v2/legislacao` | `v2.legislacao.items.index` | `Admin\LegislationCmsController@index` | somente `read` |
| GET | `cms/v2/legislacao/categorias` | `v2.legislacao.categories.index` | `categories` | somente `read` |
| POST | `cms/v2/legislacao/categorias` | `v2.legislacao.categories.store` | `categoriesStore` | `write` |
| POST | `cms/v2/legislacao/categorias/{id}` | `v2.legislacao.categories.update` | `categoriesUpdate` | `write` |
| POST | `cms/v2/legislacao/categorias/{id}/delete` | `v2.legislacao.categories.destroy` | `categoriesDestroy` | `write` |
| GET | `cms/v2/legislacao/import/template` | `v2.legislacao.items.import.template` | `downloadImportTemplate` | `write` |
| POST | `cms/v2/legislacao/import/preview` | `v2.legislacao.items.import.preview` | `importPreview` | `write` |
| POST | `cms/v2/legislacao/import/process` | `v2.legislacao.items.import.process` | `processImportExcel` | `write` |
| GET | `cms/v2/legislacao/create` | `v2.legislacao.items.create` | `create` | `write` |
| POST | `cms/v2/legislacao` | `v2.legislacao.items.store` | `store` | `write` |
| GET | `cms/v2/legislacao/{id}/edit` | `v2.legislacao.items.edit` | `edit` | `write` |
| POST | `cms/v2/legislacao/{id}` | `v2.legislacao.items.update` | `update` | `write` |
| POST | `cms/v2/legislacao/{id}/delete` | `v2.legislacao.items.destroy` | `destroy` | `write` |
| POST | `cms/v2/legislacao/{id}/files` | `v2.legislacao.files.upload` | `uploadFile` | `write` |
| POST | `cms/v2/legislacao/{id}/files/{fileId}` | `v2.legislacao.files.update` | `updateFile` | `write` |
| POST | `cms/v2/legislacao/{id}/files/{fileId}/delete` | `v2.legislacao.files.delete` | `deleteFile` | `write` |
| POST | `cms/v2/legislacao/{id}/files-reorder` | `v2.legislacao.files.reorder` | `reorderFiles` | `write` |

Observações de roteamento:

- Todas as escritas usam **POST** (não há `PUT/PATCH/DELETE`); exclusões são
  `POST /{id}/delete` com CSRF. O V7 deve normalizar para verbos REST.
- Categorias e importação são declaradas **antes** de `{id}` para não serem
  capturadas como identificador (`Routes/cms.php:6-15`).
- `{id}` é string UUID; `findOrFail` devolve 404 para id inexistente ou
  soft-deleted.
- `files-reorder` responde JSON; as demais escritas redirecionam com flash.

## 3. Rotas do portal

| Método | URI | Nome | Controller@action | Observação |
|---|---|---|---|---|
| GET | `/legislacao` | `v2.legislacao.portal.index` | `Portal\LegislationPortalController@index` | Lista paginada (10). |
| GET | `/legislacao/anexos/{file}/download` | `v2.legislacao.portal.download` | `downloadFile` | Declarada antes de `{slug}`. |
| GET | `/legislacao/{slug}` | `v2.legislacao.portal.show` | `show` | Detalhe por slug. |

A rota de download usa o **id do anexo**, não o slug nem o caminho, o que evita
path traversal na URL — a validação do caminho físico é feita no controller.

## 4. Ações do CMS em detalhe

### 4.1 `index` (`LegislationCmsController.php:44-68`)

- Exige `v2_can('Legislacao','read')`.
- Lê `search`, `category`, `sort`, `dir`.
- `sort` é validado contra allowlist `['title','number','year','publication_date','created_at']`;
  `dir` contra `['asc','desc']`. Qualquer outro valor cai em
  `publication_date desc`.
- `V2Legislation::search($search)->when($catId)->orderBy($sort,$dir)->paginate(20)`.
- Carrega todas as categorias (`ordered()`) e registra telemetria com total,
  busca, categoria e ordenação.

### 4.2 `store` / `update` (`:82-125`)

- `LegislationRequest` valida; `authorize()` também exige `write`.
- `normalizePublicationDate()` converte o valor de `datetime-local` em
  `Y-m-d H:i:s` via `strtotime`; valor inválido vira `NULL`.
- `store` injeta `id = v2_uuid()` explicitamente (além do hook do model).
- `update` usa `findOrFail` e `$item->update($dados)`.
- Após persistir, `uploadFormFiles()` processa `files[i][file]` e
  `files[i][title|view_pdf]`; falhas de tipo são convertidas em flash `error`
  sem abortar a gravação.
- Auditoria: `activity($item, 'created'|'updated', ['title','files'])`.
- Redireciona para a listagem com mensagem de sucesso.

### 4.3 `destroy` (`:130-145`)

- Percorre `$item->files()->get()` e chama `LegislationFileService::delete()`
  (remove o físico quando o prefixo é conhecido + soft delete).
- Soft-deleta a norma e audita `deleted`.
- Não há restauração na UI.

### 4.4 Categorias (`:153-241`)

| Ação | Regras |
|---|---|
| `categories` | Lista com `withCount('items')`, ordenada por `order` e `name`. |
| `categoriesStore` | Valida nome (obrigatório, ≤100), descrição (≤500), ativo, ordem (0–9999); rejeita slug duplicado considerando soft-deleted. |
| `categoriesUpdate` | Mesmas regras, ignorando o próprio id na checagem de slug. |
| `categoriesDestroy` | Renomeia slug para sufixo `-deleted-` mais 6 caracteres, desvincula itens (`update(['v2_legislation_category_id' => null])`), soft-deleta e audita. |

O slug da categoria é sempre recalculado de `name` no controller
(`Str::slug($data['name'])`), inclusive no update.

### 4.5 Importação (`:248-367`)

| Ação | Comportamento |
|---|---|
| `downloadImportTemplate` | Gera XLSX a partir de `LegislationImportSchema` e devolve como anexo. |
| `importPreview` | Valida extensão `xlsx|xls|csv` e tamanho ≤ 20 MB; delega ao `ImportExcelService::previewImportExcelData`; erros de validação retornam 422 JSON com mensagem amigável. |
| `processImportExcel` | Exige `rows` em array; define persister que cria/atualiza e retorna stream NDJSON via `CmsActionLogger::measure`. |

O persister (`:331-363`):

1. Exige `title` não vazio (lança `InvalidArgumentException`).
2. Normaliza `number` (string ou `null`) e `year` (int ou `null`).
3. Converte `publication_date` para `Y-m-d 00:00:00` ou usa `date('Y-m-d H:i:s')`.
4. Resolve/cria categoria por nome ou slug (`findOrCreateImportCategory`).
5. Se `number` **e** `year` estão preenchidos e existe linha correspondente,
   remove `slug` dos atributos e atualiza; senão cria com slug único.

### 4.6 Anexos (`:508-606`)

| Ação | Validação | Efeito |
|---|---|---|
| `uploadFile` | `file` obrigatório ≤ 50 MB; `title` ≤255; `description` ≤500; `view_pdf` booleano | Chama o service; `RuntimeException` vira flash de erro. |
| `updateFile` | `title`, `description`, `view_pdf`, `order` | Atualiza somente metadados; nunca troca o arquivo. |
| `deleteFile` | — | Remove físico (se prefixo conhecido) e soft-deleta. |
| `reorderFiles` | `order` array obrigatório; cada item existe em `v2_legislation_files` | Grava posições; responde `{"success":true}`. |

`updateFile` e `deleteFile` escopam por `legislation_id` **e** `id`
(`V2LegislationFile::where('legislation_id',$id)->findOrFail($fileId)`), impedindo
mover anexo entre normas por URL.

## 5. Form Requests e validações inline

### 5.1 `LegislationRequest`

```php
'title' => 'required|string|max:255',
'slug' => 'nullable|string|max:255|unique:v2_legislation,slug,' . $id . ',id',
'summary' => 'nullable|string|max:500',
'content' => 'nullable|string',
'number' => 'nullable|string|max:50',
'year' => 'nullable|integer|min:1900|max:2100',
'publication_date' => 'nullable|date',
'file_url' => 'nullable|string|max:500',
'is_active' => 'boolean',
'is_highlighted' => 'boolean',
'v2_legislation_category_id' => 'nullable|string|exists:v2_legislation_categories,id',
'files' => 'nullable|array',
'files.*.file' => 'nullable|file|max:51200',
'files.*.title' => 'nullable|string|max:255',
'files.*.view_pdf' => 'nullable|boolean',
```

(`Requests/LegislationRequest.php:27-43`)

O `unique` de slug compara com a tabela inteira, **sem** `withTrashed`, então um
slug de norma soft-deleted passa a ser considerado livre pelo validator — mas a
constraint do banco ainda o rejeita.

### 5.2 Validações inline

| Local | Regras |
|---|---|
| Categoria | `name` obrigatório ≤100; `description` ≤500; `is_active` boolean; `order` 0–9999 (`:461-466`). |
| Preview de importação | extensão em `xlsx,xls,csv`; `max:20480` KB; falha vira 422 JSON. |
| Upload de anexo | `file` ≤51200 KB; `title` ≤255; `description` ≤500; `view_pdf` boolean. |
| Edição de anexo | `title` ≤255; `description` ≤500; `view_pdf` boolean; `order` 0–9999. |
| Reordenação | `order` array; `order.*` string existente na tabela. |

## 6. Formatos de resposta

| Rota | Sucesso | Erro |
|---|---|---|
| Index/create/edit/categories | HTML Blade (`v2-legislacao::admin.*`). | 403 por permissão; 404 por id. |
| Escritas do CMS | 302 para listagem/categorias com `success`/`error` em sessão. | 422 do FormRequest (redirect back com erros) ou 403. |
| `reorderFiles` | JSON `{"success":true}`. | 422 de validação. |
| `importPreview` | JSON com `summary`, `rows` e contadores do motor. | 422 JSON `{success:false,message,summary}`. |
| `processImportExcel` | Stream NDJSON do `ImportExcelService`. | 422 JSON para payload inválido. |
| Portal index/show | HTML. | 404 para slug não publicado/inexistente. |
| Download | `BinaryFileResponse` com `Content-Type` e `Content-Disposition: attachment`. | 404 para anexo inexistente/não publicado; 404 para caminho inválido. |

Não há API Resource nem presenter. O portal consome models diretamente.

## 7. Permissões

| Ação do sistema | Token legado | Equivalente V7 (ADR-033) |
|---|---|---|
| Ver módulo/listar | `Legislacao.read` | `legislacao.ver` |
| Criar/editar/excluir/anexos/importar | `Legislacao.write` | `legislacao.editar` |
| Publicar/despublicar | não existe | `legislacao.aprovar` |
| Arquivar/restaurar | não existe (só soft delete) | `legislacao.arquivar` |

Resolução no legado: `v2_roles.permissions_v2` (jsonb) + `users.v2_role_id`,
níveis `none|read|write`, bypass por `config('v2.support.email')` e papel
`super-admin` (`V2/Core/Services/PermissionService.php:65-90`). O módulo declara
apenas leitura e escrita: não há separação entre publicar e editar, nem entre
arquivar e excluir.
