# Legislação — Resumo Executivo

> **Versão analisada:** `lotep` (Laravel 5.5.50 / PHP 7.4) — camada modular `V2/`
> **Slug do módulo:** `Legislacao` (grupo `Publicacoes`, namespace `V2\Modules\Publicacoes\Legislacao`)
> **Natureza:** CMS (backoffice) + Portal público; sem API JSON própria
> **Estado:** implementado e testado (57 testes dedicados entre Feature e Unit)

---

## 1. Objetivo

O módulo **Legislação** mantém o acervo normativo da LOTEP — leis, decretos e
portarias — com número, ano, data de publicação, resumo, texto rico e múltiplos
anexos em PDF/documentos. Serve ao cidadão que consulta o ordenamento no portal e
ao operador do CMS que cadastra, importa em lote, categoriza e agenda a publicação.
É um módulo **de publicação**, não de tramitação: não há workflow, assinatura,
revogação nem histórico de versões.

## 2. Efeito / Impacto

| Faixa | Efeito |
|---|---|
| Cidadão | Lista e busca normas por palavra-chave, categoria e intervalo de datas; abre o detalhe com texto formatado e baixa/visualiza anexos. |
| Prefeitura (CMS) | CRUD de normas, gestão de categorias, upload de vários anexos com ordem, importação de planilha XLSX/XLS/CSV e trilha de auditoria. |
| Transparência e legalidade | Publica atos com data e número/ano estáveis; a ocultação de itens futuros ou inativos impede vazamento de pauta. |
| Operação técnica | Um único módulo autocontido em `V2/Modules/Publicacoes/Legislacao`; a importação legada V1 vive em `Support/MigracaoLegada` e grava nas mesmas tabelas. |

## 3. Comportamento

- **Publicação é derivada de dois campos**: `is_active = true` **e**
  `publication_date <= now()` (`scopePublished`). Um item ativo com data futura
  fica **agendado** e invisível no portal. Item com `publication_date` nula
  **nunca** aparece no portal, porque o `where` compara com `now()` e `NULL` não
  satisfaz a condição.
- **Slug é regenerado** a partir do título ao criar e ao alterar o título (se o
  slug não foi alterado na mesma operação). Não há redirecionamento 301 do slug
  antigo: a URL velha passa a responder 404.
- **Categorias** têm soft delete. Antes de excluir, o módulo **renomeia o slug**
  para `slug-deleted-<6 chars>` e desvincula os itens (`v2_legislation_category_id
  = NULL`), liberando o nome para recadastro. O `FK onDelete('set null')` não
  dispara em soft delete, por isso o desvínculo é manual
  (`LegislationCmsController.php:223-241`).
- **Unicidade considera soft-deleted**: tanto o model (`V2LegislationCategory`)
  quanto o controller (`categorySlugExists`) consultam `withTrashed()`. Um slug
  apagado continua ocupado até ser renomeado na exclusão.
- **A importação é idempotente por `number` + `year`**: se ambos vierem
  preenchidos e já existir linha correspondente, ela é atualizada (sem trocar o
  slug); caso contrário, cria. Categorias citadas na planilha são criadas
  automaticamente com ordem incremental.
- **Anexos** passam por allowlist de MIME (`LegislationFileService::ALLOWED_MIME_TYPES`)
  **e** verificação de *magic bytes*; o arquivo é gravado em
  `legislacao/<id>/<arquivo>` no disco `public`.
- **Download exige publicação**: `LegislationPortalController::downloadFile`
  busca o anexo via `whereHas('legislation', published)`, logo anexo de norma
  inativa/futura responde 404. URLs externas (http/https/protocol-relative)
  são redirecionadas em vez de baixadas.

## 4. Como o Usuário Interage

**CMS (`/cms/v2/legislacao`).** A tela principal lista com busca textual
(título, resumo, número), filtro por categoria, ordenação por coluna e paginação
de 20. Os botões de topo abrem Categorias, Importar e Nova Legislação. O
formulário tem título, categoria, número, ano, data/hora de publicação, resumo,
editor rico, link externo opcional, chave Ativo e uma seção de múltiplos anexos
novos. Na edição aparece o painel de anexos existentes com reordenação, edição de
metadados e remoção. A importação segue o padrão "mesa Poka-Yoke" do serviço
ExcelImporter: download do template, preview com relatório de erros e
processamento em streaming NDJSON.

**Portal (`/legislacao`).** Cabeçalho fixo, barra de filtros (palavra-chave,
tipo/categoria por slug, data inicial e final), contagem de resultados e lista
paginada de 10. O detalhe mostra número/ano, categoria, data, resumo, texto
renderizado e a lista de anexos: PDF marcado com `view_pdf` abre em nova aba;
demais formatos ou PDF sem a marca são oferecidos por uma rota de download.

## 5. Detalhes e Particularidades Técnicas

- Fluxo CMS: rota (`Routes/cms.php`) → `v2.authenticate` + `v2.permission:Legislacao,read`
  → controller → `LegislationRequest` (nos verbos de escrita) → model/service.
  Cada action **repete** a checagem `v2_can('Legislacao', 'write')`, mesmo quando
  a rota já exige permissão.
- O namespace das views é `v2-legislacao::`, carregado por convenção pelo
  `V2ServiceProvider` (`V2/Core/Providers/V2ServiceProvider.php:85-127`).
- `V2AuditLogger` registra `created/updated/deleted`, eventos de anexo e eventos
  de importação; `CmsActionLogger` registra telemetria de index/store/update/
  destroy e mede o tempo do import.
- O texto rico é um **AST TipTap em JSON** renderizado por
  `ContentRichEditorRendererService::toHtml()`. O parser escapa nós de texto, mas
  **não sanitiza** o nó `contentRichHtmlFree` nem valida o esquema de URLs de
  link: HTML armazenado é devolvido cru.
- Não existe versionamento, revogação, vigência, órgão emissor, assinatura nem
  URL canônica de citação. O modelo é uma linha mutável por norma.
- O conceito de "destaque" (`is_highlighted`) existe no banco e no request, mas
  nenhuma tela do módulo o edita; apenas o importador legado o preenche.

## 6. Estrutura de Dados

| Tabela | Papel | Chave |
|---|---|---|
| `v2_legislation_categories` | taxonomia plana (nome, slug único, ordem, ativo) | `id` uuid char(36) |
| `v2_legislation` | norma (título, slug único, número, ano, data, resumo, `content` longText, `file_url` legado, flags) | `id`; único parcial em `v1_id` |
| `v2_legislation_files` | anexos N:1 da norma (URL, título, descrição, ordem, `view_pdf`, MIME, tamanho) | `id` + FK `legislation_id` |

```sql
-- exemplo ilustrativo: subconjunto das colunas da migration
CREATE TABLE v2_legislation_files (
    id varchar(36) PRIMARY KEY,
    legislation_id varchar(36) NULL REFERENCES v2_legislation(id) ON DELETE SET NULL,
    file_url varchar(500) NOT NULL,
    title varchar(255) NULL,
    "order" integer DEFAULT 0,
    created_at timestamp NULL,
    updated_at timestamp NULL,
    deleted_at timestamp NULL
);
```

## 7. Como Testar

```bash
cd /home/one/projects/maxima/v7/lotep

# suíte inteira (container definido no Makefile do lotep)
docker compose exec -T php bash -c 'cd /var/www/html && vendor/bin/phpunit --no-coverage'

# apenas o módulo (dentro do container)
docker compose exec -T php vendor/bin/phpunit tests/Feature/V2/Modules/Publicacoes/Legislacao
docker compose exec -T php vendor/bin/phpunit tests/Unit/V2/Modules/Publicacoes/Legislacao
```

Cobrem: escopos de publicação/data/busca (6), model e slug (5), anexos e URL (5),
seed idempotente (3), CRUD CMS e anexos (14), categorias (7), importação (12) e
portal/download (5). Ver `07-TESTES.md` para o mapa Arrange/Act/Assert.

## 8. Migração para Laravel Moderno

O destino V7 exige: schema próprio (`legislacao.legislacao`), UUIDv7, `content`
em `jsonb` (ADR-017/ADR-028), versão de bloqueio otimista, RBAC
`legislacao.{ver|editar|aprovar|arquivar}` (ADR-033), ativação por dado
(ADR-034), auditoria nativa (ADR-035), SEO em `metadata['seo']` (ADR-027),
mídia pelo service `Media` (ADR-026) e sem HTML/JS inline (ADR-022). O que o
legado **não tem** e o backlog V7 exige — versionamento, consolidação e
revogação — precisa ser desenhado, não portado. Ver `06-MIGRACAO-MODERNA.md` e
`09-APROVEITAMENTO-V7.md`.

## 9. Resumo Geral

Legislação é um módulo de publicação com duas superfícies: um CMS com CRUD,
categorias, anexos ordenados e importação idempotente de planilhas, e um portal
com busca server-side e download protegido por estado de publicação. As tabelas
são simples e o comportamento é previsível: publicação = ativo e data não futura;
slug derivado do título; categoria excluída renomeia o slug e desvincula itens;
importação atualiza por número+ano. Os riscos concentram-se em quatro pontos: o
texto rico aceita HTML livre sem sanitização, não há versionamento nem revogação,
a exclusão de arquivos físicos só cobre prefixos conhecidos e a unicidade de slug
de legislação não é tratada no model (colisão de título gera erro de banco). O
aproveitamento para o V7 é alto em regras e UI, mas exige redesenho do núcleo
normativo para atender ao backlog de normas versionadas e citáveis.

## Índice do dossiê

| Arquivo | Conteúdo |
|---|---|
| `00-RESUMO.md` | Este documento |
| `01-ARQUITETURA.md` | Camadas, classes, fluxo e acoplamentos |
| `02-MODELAGEM.md` | Tabelas, colunas, índices, hooks e seeds |
| `03-ROTAS-E-CONTROLADORES.md` | Rotas, requests, permissões e formatos de resposta |
| `04-VIEWS-E-UX.md` | Blades, layouts, JS e experiência |
| `05-REGRAS-E-PORTABILIDADE.md` | Regras não óbvias, armadilhas e riscos |
| `06-MIGRACAO-MODERNA.md` | Plano de reconstrução no V7 |
| `07-TESTES.md` | Estratégia, casos e fixtures |
| `08-GAPS-E-DIVIDAS.md` | Lacunas, bugs e perguntas de negócio |
| `09-APROVEITAMENTO-V7.md` | Portar / adaptar / rejeitar com ADRs |
