# P8 — Licitações

| Campo | Valor |
|---|---|
| Programa | F7+ — operação, estrutura e conteúdo |
| Status | `planejado` |
| Depende de | P2, P6 |
| Habilita | P9 (2º consumidor de busca/feed) |
| Tamanho | **G** (4–5 sessões) |
| ADRs | ADR-007, ADR-012, ADR-017, ADR-018/027, ADR-026, ADR-030, ADR-033, ADR-043 |

## 1. Objetivo

Publicar o ciclo das licitações municipais — processo, objeto, modalidade, datas, valor (quando
público), órgãos e anexos — com portal, CMS e API read-only, sem duplicar a verdade de anexos e sem
expor valor quando `show_value = false`.

**Resultado observável:** o operador cadastra uma licitação com número+ano, anexa o edital via
Media, publica; o portal lista/filtra no servidor, o detalhe mostra os anexos, a API devolve o mesmo
recorte e um valor marcado como sigiloso não aparece em lista, detalhe, JSON-LD, sitemap ou API.

## 2. Estado atual

- Não existe módulo de licitações no V7.
- `Media`/`Gallery` já resolvem upload, variantes, catálogo e download same-origin (`media.stream`).
- `Seo`/`SitemapRegistry` já são o caminho para `@graph`/sitemap.
- O dossiê V2 prova o domínio: 5 tabelas, anexos ordenados, órgãos N:N, `show_value`, arquivamento
  reversível; os bugs a evitar: filtro no navegador, slug com sufixo `-1`, jsonb legado convivendo
  com tabela de anexos, force-delete ignorando anexos.

## 3. Escopo fechado

### S1 — Schema e modelo

- Módulo `Publicacoes/Licitacoes` (token `licitacoes`, grupo `Publicacoes`, permissions
  `[VIEW, EDIT, APPROVE, ARCHIVE]`), schema `licitacoes`.
- Tabelas:

| Tabela | Colunas principais |
|---|---|
| `licitacoes` | `id` uuidv7, `numero` text, `ano` int, `processo` text, `objeto` text, `modalidade` text, `status_id` FK, `valor_estimado numeric(14,2) null`, `valor_homologado numeric(14,2) null`, `show_value bool default true`, `data_abertura`, `data_publicacao`, `data_encerramento` (timestamptz/null), `slug` text único, `publicado_em` timestamptz null, `arquivado_em` timestamptz null, `metadata` jsonb, timestamps |
| `licitacao_status` | `id`, `slug` único, `nome` jsonb, `position`, `publico bool` |
| `licitacao_orgaos` | `id`, `nome` jsonb, `slug` único case-insensitive |
| `licitacao_orgao` | `licitacao_id`, `orgao_id`, PK composta, cascade |
| `licitacao_anexos` | `id`, `licitacao_id` FK cascade, `media_id` (catálogo Media), `titulo` jsonb, `tipo` enum (`termo_referencia|justificativa|parecer_tecnico|parecer_juridico|orcamento|edital|ata|impugnacao|recurso|resultado|adjudicacao|homologacao|outro`), `position`, `publico bool`, timestamps |

- Unicidade `(numero, ano)` case-insensitive (`lower(numero)` + `ano`), índice único parcial
  `WHERE arquivado_em IS NULL`? Decisão: número+ano é único mesmo arquivado (não reutilizar
  processo); arquivamento é reversível. Use único sempre.
- Slug determinístico `{numero}-{ano}-{slug-objeto}` com sufixo só em colisão real; nunca `-1`
  silencioso; `slug` único.
- `down()` preserva tabelas como `backup_*`; nada de drop.
- Status como dado (seed idempotente): `aberta`, `em-andamento`, `encerrada`, `deserta`, `revogada`,
  `suspensa`; `publico` controla exibição de prazos? Manter simples: status não decide
  visibilidade; `publicado_em` e `arquivado_em` decidem.

### S2 — Portal

- `GET /licitacoes` — lista paginada, filtros server-side (status, modalidade, ano, órgão, texto),
  ordenação `data_publicacao desc, id desc`; nenhuma carga total no navegador. Inclui dispensas e
  inexigibilidades na mesma relação, com filtro próprio; CSV e série de 3 anos.
- `GET /licitacoes/{slug}` — detalhe com objeto, datas, órgãos, **documentos por tipo** (8.3),
  anexos ordenados, valor estimado/homologado condicional; breadcrumb; canonical; `X-Robots-Tag` só
  em arquivada (404/noindex).
- **PNTP 8.3/8.4:** o CMS exige o checklist de documentos (TR, justificativa, pareceres, orçamento,
  edital, atas, impugnações/recursos, resultado, adjudicação/homologação); dispensa/inexigibilidade
  tem seção e checklist próprios. O critério não é atendido por um PDF genérico.
- **PNTP 8.5/8.6:** atas SRP e PAC são P23; 8.7 (sancionados) é P17; o P8 linka, não duplica.
- `show_value = false`: valor **não** aparece na lista, no detalhe, no `@graph`/JSON-LD, no sitemap
  nem na API. Teste dedicado em cada superfície.
- Anexos: `<x-service-media::picture>` para imagem; PDF via link same-origin (`media.stream`) com
  `Content-Disposition: attachment`, `rel="noopener"`, tamanho e tipo visíveis; anexo `publico=false`
  só no CMS (nunca no portal).
- Acessibilidade: tabela/lista semântica, filtros com label, alvos ≥2.75rem, estado vazio,
  paginação do projeto; nada de modal para o detalhe.
- 4 locales; datas com `LocalizedDate`; dinheiro como string decimal formatada (nunca `float`).

### S3 — CMS

- Rotas `/cms/licitacoes` (lista/filtros), criar, editar, publicar/despublicar, arquivar/restaurar,
  anexos (upload/ordem/remoção/visibilidade), órgãos (criar inline + associar), status.
- Formulário: número/ano/objeto obrigatórios; valor opcional (`numeric`), `show_value` explícito;
  datas consistentes (`abertura <= encerramento`); modalidade de lista fechada; órgãos múltiplos.
- Upload de anexo via Media (uma requisição por arquivo; lote é do Gallery), MIME allowlist +
  magic bytes; limite de tamanho; nome original preservado em metadata; nunca confiar no client.
- Publicar exige objeto, número/ano, órgão e ao menos um anexo? Decisão: ao menos um anexo é
  recomendado, não obrigatório; o objeto/número/ano são obrigatórios (regra do dossiê).
- Arquivar não apaga anexos; restaurar volta ao estado anterior; auditoria de cada transição.
- Permissões: `licitacoes.ver/editar/aprovar/arquivar`; publicar/despublicar exige `aprovar`;
  arquivar exige `arquivar`.
- Auditoria: `licitacoes.created/updated/published/archived/restored`, sem conteúdo de anexo.

### S4 — API e SEO

- `GET /api/v1/licitacoes` read-only, paginada, filtros allowlist, rate limit (ADR-021), sem PII;
  `GET /api/v1/licitacoes/{slug}`; resposta JSON estável (campos, `ETag`); `show_value` respeitado.
- `@graph` com `GovernmentService`/`Dataset`? Usar schema adequado a licitação (ex.:
  `GovernmentService` + `CreativeWork` para edital); não inventar tipo inexistente; validar JSON-LD.
- `SitemapSource` do módulo registra licitações publicadas (nunca arquivadas); `hreflang`/canonical.
- Feed/busca: ainda locais; a busca compartilhada só em P9 (2º consumidor).
- Testes de contrato da API, rate limit, `ETag`/304, `show_value`, anexo privado.

### S5 — Fechamento

- E2E: criar → anexar → publicar → portal/API; arquivar → some do portal, anexo preservado;
  `show_value=false` em todas as superfícies; axe.
- `make check` 2× + `E2E=1`; cobertura ≥90%; query count (anti-N+1) no detalhe.
- Atualizar `INVENTARIO.md` e abrir `wiki/backlog/modulo-licitacoes.md` (promovido).

## 4. Fora de escopo

- Integração com sistemas de compras/contabilidade, assinatura digital, pregão eletrônico.
- Importação ETL do legado (ADR-042); carga em lote via CSV é P9, se necessária.
- Contratos, atas de adesão SRP, PAC e sanções: P23/P17 (não duplicar aqui).
- Portal de transparência fiscal completo (P22), obras (P17), planejamento (P24).

## 5. Fatias de sessão

| Sessão | Entrega | Prova | Depende |
|---|---|---|---|
| **P8.S1** | schema + modelos + status/órgãos seed | unique número+ano; slug; `down()` backup | P2/P6 |
| **P8.S2** | portal lista/detalhe + filtros + anexos | filtro server-side; `show_value`; a11y | S1 |
| **P8.S3** | CMS CRUD + upload + publicar/arquivar | permissões; magic bytes; arquivar preserva | S1 |
| **P8.S4** | API + JSON-LD + sitemap | rate limit; `ETag`; `show_value`; JSON-LD válido | S2 |
| **P8.S5** | E2E, docs, 2× check | evidência no `kd` | S2–S4 |

## 6. Contratos e dados

- **Módulo:** `Publicacoes/Licitacoes` (token `licitacoes`).
- **Tabelas:** `licitacoes`, `licitacao_status`, `licitacao_orgaos`, `licitacao_orgao`,
  `licitacao_anexos`.
- **Rotas:** `licitacoes.portal.*`, `licitacoes.cms.*`, `api.licitacoes.*`.
- **Permissões:** `licitacoes.ver/editar/aprovar/arquivar`.
- **Eventos:** `licitacoes.created/updated/published/archived/restored`.
- **Cache:** listagem/detalhe por slug + locale; invalidação em publicar/editar/arquivar.
- **Sitemap:** `SitemapSource` do módulo.

## 7. DoD

- [ ] Número+ano único (case-insensitive), slug determinístico, sem sufixo silencioso.
- [ ] `show_value=false` oculto em lista, detalhe, JSON-LD, sitemap e API (teste por superfície).
- [ ] Anexos via Media, ordenados, com `publico` respeitado; arquivar não apaga.
- [ ] Portal filtra no servidor; nenhuma carga total no navegador; paginação determinística.
- [ ] Publicar/arquivar exigem a permissão correta; tudo auditado.
- [ ] API read-only com rate limit e `ETag`; JSON-LD válido; sitemap sem arquivadas.
- [ ] `make check` 2× + `E2E=1`; cobertura ≥90%; PHPStan max 0.
- [ ] PNTP 8.1: valor estimado/homologado, dispensa/inexigibilidade, filtros e CSV.
- [ ] PNTP 8.3/8.4: checklist de documentos por tipo; seção de dispensa/inexigibilidade.
- [ ] PNTP 8.5/8.6/8.7: links para P23/P17, sem duplicar.

## Fechamento — ficha de revisão

> **Obrigatório antes de fechar:** criar `_to_revision/P8-licitacoes.md` e seguir
> `_to_revision/README.md` + `_to_revision/PROTOCOLO.md` (v2.0). A ficha não é gate de build; é a
> entrada da fila de revisão profunda.

- Cabeçalho: fase, `kd` épico/tasks/slices, âncoras (este arquivo, ADRs e código) e status
  `⬜ pendente` → `🟡 em revisão` → `✅ revisado` / `🔧 com correções`.
- Conteúdo: o que foi implementado, arquivos/superfícies a revisar, checklist D1–D14 aplicável,
  riscos e observações.
- Fechamento: preencher “Notas da revisão” com data/revisor/resultado e achados (`kd write
  --anchor`), atualizar a fila em `_to_revision/README.md` e `_to_revision/_inventario.md`.
- Achado com correção exige teste vermelho→verde; a fase só é `closed` no `kd` com a ficha
  registrada.

## 8. Riscos

| Risco | Mitigação |
|---|---|
| Valor sigiloso vazar | `show_value` no model/DTO/API/JSON-LD/sitemap; teste por superfície |
| Anexo privado acessível | checagem no controller de stream + `publico`; teste 403 |
| Duas verdades de anexo (jsonb + tabela) | só tabela; `metadata` não guarda lista de arquivos |
| Filtro no navegador | query server-side + paginação; teste de query count |
| Slug `-1`/colisão | slug derivado + sufixo determinístico só em colisão real; teste |
| Arquivar apagar anexo | arquivamento é data; teste de restauração |

## 9. Retomada

```bash
kd prime
kd ask "licitações anexos show_value órgãos"
kd task list --ready
```

Ler: `lotep-v2/publicacoes-licitacoes/09` (contratos que não podem regredir) →
`dossie_modules/licitacoes/**` → `src/services/Media/**` → `src/services/Seo/**` →
`src/modules/Portal/DadosAbertos/**` (padrão de módulo/portal/API).
