# Aproveitamento no V7 — Licitações

> Julgamento do módulo `V2/Modules/Publicacoes/Licitacoes` para o projeto
> `public-v7`. Cada linha indica o padrão concreto, o destino e a regra/ADR que
> sustenta a decisão. Caminhos `src/...` e `wiki/adr/...` são relativos a
> `public-v7/`; caminhos `V2/...` e `tests/...` são relativos a `lotep/`.

## 1. Veredito

**Portar o domínio, reescrever a implementação.** O módulo tem um recorte de
domínio correto e pequeno — licitação, anexos ordenados, órgãos N:N, ciclo de
arquivamento e controle de exibição do valor — mas a implementação acumula
duas verdades para arquivos, conteúdo rico em coluna de texto, filtro no
navegador, autorização de dois níveis e dependência de helpers do Laravel 5.5.
Quase nada do código PHP é copiável; as **regras** e a **UX** são aproveitáveis.

O V7 já possui os serviços que substituem as gambiarras: `RichText` (ADR-028),
`Media` (ADR-026), `Seo` (ADR-027), `Access` (ADR-033), auditoria nativa
(ADR-035) e descoberta de módulos (ADR-014/ADR-034).

## 2. O que portar (padrão concreto)

| # | Item do legado | Evidência | Padrão no V7 | Regra/ADR |
|---|---|---|---|---|
| P1 | Vocabulário do domínio: `number`, `title`, `object`, `process_number`, `value`, `show_value`, três datas, `is_active`, `reason`, `local`, `management_unit` | `V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:91-101` | manter os mesmos conceitos e nomes de coluna em snake_case; `value` com cast `decimal:2` | ADR-001 (nomes explícitos), ADR-017 (schema de domínio) |
| P2 | `show_value` como decisão editorial separada do valor | `V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:103-115`; `V2/Modules/Publicacoes/Licitacoes/Views/portal/show.blade.php:68` | campo booleano; listagem, detalhe, SEO e qualquer API omitem o valor quando falso | ADR-019 (privacidade), ADR-027 (não emitir dado sigiloso em JSON-LD) |
| P3 | Três datas independentes e opcionais | `V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:103-115` | `timestamptz` nulo; UI `type=date`; exibição `d/m/Y` | ADR-017 (tipos), ADR-012 (formato local) |
| P4 | Anexos 1:N com `order`, `title` opcional, `original_name`, MIME e tamanho | `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000010_create_v2_licitacao_anexos_table.php:62-85`; `V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoAnexo.php` | tabela `licitacoes.anexos` com `media_data` jsonb, `order`, soft delete | ADR-017, ADR-026 |
| P5 | Ordenação editorial dos anexos (setas e posição) | `V2/Modules/Publicacoes/Licitacoes/Views/cms/includes/attachments.blade.php:36-61` | manter a UX sem drag-and-drop; controles acessíveis, `PATCH` transacional | ADR-010 (mínimo JS), ADR-025 (teclado) |
| P6 | Criação inline de órgão sem sair do formulário | `V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoOrgaoController.php:124-143`; `V2/Modules/Publicacoes/Licitacoes/Views/cms/includes/orgao-modal.blade.php` | endpoint `POST /cms/licitacoes/orgaos` com resposta JSON/HTML parcial; `<dialog>` nativo | ADR-009 (daisyUI/dialog), ADR-010, ADR-022 (sem inline) |
| P7 | Órgãos N:N com nome, ativo e soft delete | `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000011_create_v2_licitacao_orgaos_table.php:63-85` | tabelas `orgaos` + `licitacao_orgao`, unique em `name` | ADR-017 (integridade) |
| P8 | Ciclo arquivar/restaurar/excluir em definitivo | `V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:180-236` | soft delete + restore + hard delete explícito, com confirmação forte | ADR-035 (evidência), ADR-033 (permissão `arquivar`) |
| P9 | Listagem do CMS com busca, ordenação, status e ações | `V2/Modules/Publicacoes/Licitacoes/Views/cms/index.blade.php:99-165` | mesma informação, componentes do shell V7, filtros realmente aplicados no servidor | ADR-009 (CMS próprio), ADR-016 (shell como dado) |
| P10 | Página pública com objeto, dados, órgãos, anexos e observação | `V2/Modules/Publicacoes/Licitacoes/Views/portal/show.blade.php` | mesma hierarquia semântica; `<h1>` único, `<table>` com `<caption>`, links com contexto | ADR-010 (HTML semântico), ADR-025 (WCAG 2.2 AA) |
| P11 | 404 real para licitação inativa/inexistente | `V2/Modules/Publicacoes/Licitacoes/Controllers/Portal/LicitacaoPortalController.php:45-48` | `firstOrFail` no escopo público; `X-Robots-Tag: noindex` | ADR-027 (anti-soft-404) |
| P12 | Mapa de cores por status | `V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoStatus.php:70-93` | constante/config no módulo; não persistir cor no banco | ADR-001, ADR-016 (apresentação fora do dado) |
| P13 | Auditoria das mutações com nomes semânticos (`created`, `updated`, `archived`, `restored`, `force_deleted`, `toggled`, `attachment_added`, `attachment_removed`) | `V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:128-259`; `V2/Modules/Publicacoes/Licitacoes/Services/LicitacaoAttachmentService.php:114-270` | mapear para a trilha automática do V7; eventos de domínio carregam `module`, `action`, `target` | ADR-035, ADR-024 |
| P14 | Regra de conteúdo dos anexos: validar o **conteúdo**, não o `Content-Type` do cliente; assinatura em offset para MP4 | `V2/Modules/Publicacoes/Licitacoes/Services/LicitacaoAttachmentService.php:51-77,144-197` | requisito de teste do serviço `Media`; a implementação de bytes fica no Media | ADR-026 §2 (MIME por finfo no servidor), ADR-020 (upload seguro) |

### 2.1 A regra de magic bytes, portada como contrato

O legado acerta o princípio: o MIME declarado é confrontado com os primeiros
bytes do arquivo, e assinaturas fora do offset 0 são suportadas
(`video/mp4` procura `ftyp` no offset 4). O V7 **não** deve manter uma segunda
implementação de allowlist no módulo. O contrato correto é:

1. `MediaInputer` detecta MIME por `finfo` no servidor e aplica a allowlist do
   a configuração `media` do V7 (ADR-026);
2. o módulo restringe o subconjunto de tipos aceitáveis para anexos de
   licitação (PDF, ZIP, DOCX, XLSX, imagens, áudio, vídeo) na regra do
   `AddLicitacaoAnexoRequest`;
3. o teste do módulo cobre os casos que o legado cobre: PDF falso com MIME
   PDF, MP3 com frame sync, MP4 com `ftyp` no offset 4, WAV com `RIFF` e um
   arquivo de texto renomeado;
4. casos que o legado **não** cobre devem entrar no teste do Media/ módulo:
   `RIFF` sem `WEBP`, ZIP renomeado como DOCX, SVG com script.

Assim o conhecimento é preservado, mas a implementação não é duplicada.

## 3. O que adaptar

| # | Legado | Problema | Adaptação no V7 | Regra/ADR |
|---|---|---|---|---|
| A1 | PK UUID v4 gerada em PHP (`v2_uuid`) | helper enganoso, não ordenável | `uuidv7()` default no banco + `HasUuids` | ADR-017 |
| A2 | `v2_licitacao_status` como tabela editável | sem CRUD, sem ordem, sem cor persistida; nomes criados por importador | `status` text com CHECK e constantes do módulo; rótulos/cores em código | ADR-001 (simplicidade), ADR-017 |
| A3 | `jurisdicionados` jsonb + N:N | duas listas de órgãos | só N:N; migração dos nomes para `orgaos` | ADR-017 (sem duplicação de verdade) |
| A4 | `arquivos` jsonb + tabela `anexos` | duas verdades | só tabela `anexos`; jsonb descartado após curadoria | ADR-017, ADR-042 |
| A5 | `object` text com `strip_tags` e AST JSON | conteúdo rico tratado como texto; texto legado renderiza vazio | `object` jsonb (AST TipTap) + `object_text` para busca; conversão editorial | ADR-028, ADR-017 |
| A6 | `generateSlug` com `static::$oldSlug` | bug de sufixo `-1` e corrida | Action com `where id !=`, unique e retry | ADR-001 (determinismo) |
| A7 | `LicitacaoCmsController` com 14 actions | controller gordo, testes difíceis | um controller invocável por operação + Actions de domínio | ADR-013 (DDD-lite), ADR-001 |
| A8 | permissões `read`/`write` + `v2_can` + `@v2can` | vocabulário pobre; UI decide acesso | quatro ações `{licitacoes}.{ver|editar|aprovar|arquivar}`; `Permission::authorize` no controller | ADR-033 |
| A9 | `V2AuditLogger` + `CmsActionLogger` | duplicidade de logging; sem ator/antes-depois | trilha nativa automática + `log_event` técnico | ADR-035, ADR-024 |
| A10 | `Storage::disk('public')` + `v2_storage_url` | arquivo local, URL própria, sem variantes | `Media`/`EditorMediaStore` + S3 (`permanent/`) + `media.stream` | ADR-026, ADR-030 |
| A11 | Portal com `->get()` e filtro no navegador | não escala, não indexa, sem URL de filtro | paginação e filtros no servidor; cache/ETag | ADR-022, ADR-025 |
| A12 | Blade Bootstrap/AdminLTE + `onclick`/`confirm` | CSP e dependência de jQuery | Tailwind/daisyUI, `<dialog>`, JS em módulo Vite, sem inline | ADR-009, ADR-010, ADR-022 |
| A13 | SEO só com `setTitle` | sem descrição, canonical, OG, JSON-LD | `<x-service-seo::tags>` com `GovernmentService` + `BreadcrumbList`; sitemap | ADR-027 |
| A14 | `LicitacaoDataSource` lendo as tabelas por SQL | acoplamento do módulo a Estatísticas | expor `WidgetSource` ou read model; não duplicar SQL de domínio | ADR-015 (services agnósticos), ADR-014 |
| A15 | `active_pattern`, `position`, `group` no provider | conceito de menu do V2 | `ModuleProvider::config()` com título/ícone/rota/permissões; ativação por dado | ADR-014, ADR-034 |
| A16 | URLs `/licitacao/{slug}` | contrato público existente | manter as URIs; mudanças de slug geram 301 | ADR-027 §6 |
| A17 | `is_active` como interruptor | conceito válido, mas confuso com status | manter; publicar/despublicar é `aprovar`; status é rótulo | ADR-033 |
| A18 | Anexo `delete` apaga físico e faz soft delete | perda imediata, sem restore | manter soft delete; remoção física só no hard delete, via Media (ou retenção) | ADR-026 (retenção), ADR-035 |
| A19 | Três datas sem validação de ordem | dado inconsistente | validar `publication <= competition <= homologation` quando as três existirem | ADR-001 |
| A20 | `value` float | centavos imprecisos | `decimal:2` no cast e `numeric(15,2)` no banco | ADR-017 |

## 4. O que rejeitar

| # | Item | Por que rejeitar | Regra/ADR |
|---|---|---|---|
| R1 | `static::$oldSlug` e o laço de slug atual | propriedade nunca definida; sufixo artificial e corrida | ADR-001 |
| R2 | `strip_tags(html_entity_decode())` em conteúdo rico | destrói HTML legado e não é o modelo do V7; o parser já escapa | ADR-028, ADR-020 |
| R3 | jsonb `arquivos` | segunda verdade para anexos | ADR-017 |
| R4 | jsonb `jurisdicionados` | segunda lista de órgãos | ADR-017 |
| R5 | `v1_id`, `imported_at`, `metadata.imported_from` | sem retrocompatibilidade e sem ETL | ADR-042 |
| R6 | Importador `LicitacoesLegacyImporter` e `LicitacoesAntigasLegacyImporter` | conteúdo entra à mão pelo CMS; ETL arrasta schema sujo | ADR-042 |
| R7 | Portal `->get()` + filtro client-side | não escala e não é indexável | ADR-022, ADR-025 |
| R8 | Implementação própria de magic bytes | o Media é a porta única; duplicar cria duas allowlists divergentes | ADR-026 §2, ADR-015 |
| R9 | `read`/`write` e `@v2can` | vocabulário do V7 é de quatro ações; view não decide acesso | ADR-033 |
| R10 | `V2AuditLogger`, `CmsActionLogger`, `v2_audit()` | trilha nativa com imutabilidade e hash | ADR-035, ADR-029 |
| R11 | `v2_uuid()` / UUID v4 em PHP | UUIDv7 nativo e ordenável | ADR-017 |
| R12 | `Storage::disk('public')` e `v2_storage_url()` | storage local, sem S3, sem variantes | ADR-026, ADR-030 |
| R13 | `onclick`, `onsubmit`, `confirm()` e Bootstrap JS | CSP estática sem inline; shell próprio | ADR-022, ADR-009 |
| R14 | `LicitacaoDataSource` com SQL direto | acoplamento invertido; Estatísticas não deve conhecer tabela de módulo | ADR-015, ADR-013 |
| R15 | Status criado por `firstOrCreate` no importador | status é vocabulário fechado; nomes desconhecidos exigem curadoria | ADR-001, ADR-042 |
| R16 | Modalidade e tipo de licitação | removidos no próprio V2; não há dado nem uso | ADR-042 |
| R17 | `has_anexos` como acessor com query | N+1; usar `withCount`/`exists` no ponto de uso | ADR-008 (otimização nativa) |
| R18 | `slug` `nullable` sem regeneração | grava `null` e quebra a URL pública | ADR-001 |
| R19 | `forceDelete` que confia em `anexos` sem trashed | deixa objetos órfãos | ADR-026, ADR-035 |
| R20 | Layouts `v2-cms`/`v2-portal` e ícones `bi-*` | shell e ícones do V7 são outros | ADR-016, ADR-009 |

## 5. Dados: o que fazer com o conteúdo existente

ADR-042 é explícito: **nenhuma camada de compatibilidade**, banco novo,
conteúdo migrado à mão. Para Licitações, isso significa:

1. exportar a lista de licitações ativas (e as arquivadas que o negócio quiser
   preservar) com número, título, objeto, processo, valor, datas, local,
   unidade gestora, status e órgãos;
2. revisar o objeto caso a caso e recriá-lo no editor TipTap (AST);
3. reenviar os anexos pelo Media, com título editorial; descartar o jsonb
   `arquivos`;
4. recriar os órgãos como entidades e associá-los;
5. mapear status para `aberta|finalizada|cancelada`; nomes fora desse
   conjunto exigem decisão;
6. não migrar `v1_id`, `imported_at`, `metadata.imported_from` nem as tabelas
   `backup_v2_modalidades_licitacao`/`backup_v2_tipos_licitacao`.

O volume é pequeno e a curadoria é humana de qualquer forma (texto, arquivos e
órgãos precisam de revisão). Um importador automático reintroduziria
`strip_tags`, caminhos antigos de storage e status arbitrários — exatamente o
que o V7 quer evitar.

## 6. Contratos de comportamento que não podem regredir

| Contrato | Verificação no aceite |
|---|---|
| `show_value=false` oculta o valor em **toda** superfície pública | teste de listagem, detalhe, JSON-LD e sitemap |
| licitação inativa não aparece na listagem e retorna 404 no detalhe | feature test |
| número, título e objeto são obrigatórios | Form Request + teste |
| anexos mantêm a ordem definida pelo operador | teste de reordenação |
| órgão pode ser criado sem sair do formulário | teste de feature do endpoint |
| arquivar não apaga anexos; restaurar devolve a licitação | teste de ciclo |
| excluir em definitivo remove também os objetos de anexos arquivados | teste com dublê do Media |
| URLs `/licitacoes` e `/licitacao/{slug}` continuam válidas | teste de rota + redirect de slug antigo |
| três datas são exibidas em `d/m/Y` | teste de view |
| conteúdo rico é renderizado com segurança | teste de XSS com `<script>` no AST |

## 7. Checklist de implementação

- [ ] `ModuleProvider` declara as quatro permissões e registra o sitemap.
- [ ] Migration idempotente com schema `licitacoes`, UUIDv7, `timestamptz`,
      JSONB de AST e CHECK de status.
- [ ] Modelos sem traits além de `HasUuids`/`SoftDeletes`; sem abstract/generics.
- [ ] Actions transacionais para salvar, publicar, arquivar, restaurar, excluir.
- [ ] Slug gerado com exclusão da própria linha, unique e retry.
- [ ] `object` em AST; `object_text` via `RichTextRenderer::toText`.
- [ ] Anexos pelo `MediaIngestor`/`EditorMediaStore`; nenhum `Storage::disk('public')`.
- [ ] Ordenação em transação e com validação de posse.
- [ ] `Permission::authorize` em cada controller (ver/editar/aprovar/arquivar).
- [ ] Portal paginado, filtros de servidor, 404 real e SEO `@graph`.
- [ ] Views sem `onclick`/`onsubmit`/inline script; `<dialog>` e JS em módulo.
- [ ] Testes de feature cobrindo permissões, ciclo de vida, XSS, `show_value`
      e anexos; cobertura dentro do gate do V7.
