# Gaps, dívidas e perguntas — Licitações

## 1. Defeitos confirmados por leitura de código

### 1.1 Portal carrega todas as linhas e filtra no navegador

`V2/Modules/Publicacoes/Licitacoes/Controllers/Portal/LicitacaoPortalController.php:28-31` executa
`Licitacao::active()->with(['status','orgaos','anexos'])->ordered()->get()`.
Não há `paginate`, `limit`, cache de resposta ou busca no servidor. Com muitas
licitações, o custo cresce em memória, banda e tempo de render. O filtro da
view (`V2/Modules/Publicacoes/Licitacoes/Views/portal/index.blade.php:118-128`) só esconde linhas já baixadas.

**Impacto**: alto. **Correção no V7**: paginação + filtros de servidor
(`06-MIGRACAO-MODERNA.md` §8).

### 1.2 `generateSlug` depende de `static::$oldSlug`, que nunca é definida

Busca global por `oldSlug` retorna apenas as duas ocorrências em
`V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:153-160`. A propriedade não existe na classe nem é
atribuída em lugar algum. Consequências:

- na edição, a consulta de unicidade inclui a própria linha; se o slug gerado
  for igual ao atual, o laço acrescenta `-1`;
- a URL pública muda sem intenção editorial;
- duas requisições concorrentes podem gerar o mesmo slug e estourar a unique
  parcial do banco.

**Correção no V7**: gerar slug em Action, com `where('id','!=',$id)` quando a
licitação já existe e retry em violação de unique.

### 1.3 Duas verdades para arquivos

`v2_licitacoes.arquivos` (jsonb, `[{url,title}]`) e
`v2_licitacao_anexos` (tabela) guardam arquivos em paralelo. Nenhuma view ou
controller lê o jsonb; o importador grava nele. A UI de anexos só enxerga a
tabela. Se a equipe usar o jsonb para algum relatório futuro, os dados
divergirão.

**Correção no V7**: uma tabela, migração editorial dos arquivos legados para
`anexos`, jsonb eliminado.

### 1.4 `forceDelete` deixa arquivos órfãos de anexos soft-deleted

`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:230-232` percorre
`$licitacao->anexos`, que ignora `deleted_at`. Anexos removidos pela UI já têm
`deleted_at`; seus arquivos nunca são apagados do disco, mesmo quando a
licitação é excluída em definitivo.

**Correção no V7**: `anexos()->withTrashed()->get()` antes do hard delete, ou
serviço Media com `delete` idempotente.

### 1.5 Filtros do CMS sem efeito

`V2/Modules/Publicacoes/Licitacoes/Views/cms/index.blade.php:113-139` renderiza selects `orgao` e `status`; o
controller lê apenas `search`, `sort` e `dir`
(`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:46-51`). O partial
`filter-card` ainda considera esses parâmetros para mostrar “Limpar filtros”.

**Impacto**: médio (funcionalidade anunciada que não existe).

### 1.6 Objeto em texto puro não renderiza no portal

`ContentRichEditorRendererService::ensureArray()` devolve `[]` quando
`json_decode` falha (`V2/Services/ContentRichEditor/Services/ContentRichEditorRendererService.php:36-44`).
O detalhe só cai no fallback quando há exceção
(`V2/Modules/Publicacoes/Licitacoes/Controllers/Portal/LicitacaoPortalController.php:52-59`). Objetos legados em
texto puro (e os criados nos testes com `object => 'X'`) produzem HTML vazio.

**Impacto**: alto para dados importados. **Correção no V7**: conteúdo em AST
obrigatório; migração editorial converte texto para AST; fallback explícito
para texto.

### 1.7 `update` apaga órgãos quando `orgao_ids` não vem no request

`V2/Modules/Publicacoes/Licitacoes/Controllers/Admin/LicitacaoCmsController.php:166` chama
`$licitacao->orgaos()->sync($r->input('orgao_ids', []))` sem condicional. Um
POST parcial (teste, integração, falha de formulário) remove todas as
associações. No `store` o comportamento é condicional (`if ($r->has(...))`),
criando assimetria.

### 1.8 Anexos: ordem duplicada e reordenação sem garantia

- O controller passa `$licitacao->anexos()->count()` como próxima ordem; após
  exclusões, a posição pode repetir (`:289-297`).
- `reorder` executa um `UPDATE` por id, sem transação e sem validar que os ids
  pertencem à licitação (`V2/Modules/Publicacoes/Licitacoes/Services/LicitacaoAttachmentService.php:248-255`).
- `updateOrder` não é usado por nenhuma rota.

### 1.9 Magic bytes incompletos

- `image/webp` valida somente `RIFF`; um WAV declarado como WebP passaria.
- ZIP/DOCX/XLSX compartilham `PK`; não há distinção real entre eles.
- SVG não tem assinatura e é liberado sem checagem.
- A leitura de 16 bytes cobre as assinaturas atuais, mas não é uma allowlist
  robusta contra polyglots.

### 1.10 Órgão removido continua visível em licitações

`destroy` de órgão faz soft delete. A relação `Licitacao::orgaos()` não filtra
`orgaos.deleted_at`; o portal e o CMS continuam exibindo o órgão removido. Não
há regra que impeça remover órgão associado.

### 1.11 Detalhe exibe badge “Inativo” impossível

`V2/Modules/Publicacoes/Licitacoes/Views/portal/show.blade.php:38-43` imprime “Ativo”/“Inativo”, mas a consulta
filtra `active()` e devolve 404 para inativa. O ramo “Inativo” é inalcançável.

### 1.12 `has_anexos` e contagens repetidas

`getHasAnexosAttribute()` executa uma query por acesso. A view de edição chama
`$licitacao->anexos()->count()` dentro do laço de anexos e no `max` do input.
É N+1 clássico em uma tela com muitos anexos.

### 1.13 Datas sem ordem e sem fuso

Três datas independentes, `datetime` sem timezone; nada valida
`publication <= competition <= homologation`. Relatórios por ano dependem do
fuso do banco.

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| modalidade e tipo de licitação | removidos em `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000012_drop_modalidade_tipo_from_v2_licitacoes_table.php`; tabelas renomeadas para `backup_*`; models não existem |
| `jurisdicionados` / `metadata` / `arquivos` | gravados pelo importador, sem UI; dados opacos |
| `v1_id` / `imported_at` | só para o importador; não exibidos, não filtráveis |
| `has_anexos` | acessor sem uso |
| `has_anexos` vs `anexos_count` | `withCount` não é usado; contagem via coleção |
| filtros `orgao`/`status` | renderizados, ignorados |
| `attachmentError` | `div` no modal nunca preenchida |
| busca textual do portal | filtro local, sem URL, sem paginação, sem SEO |
| API/exportação | inexistente |
| CRUD de status | inexistente; só migration/seed e `firstOrCreate` do importador |
| restauração de anexo | inexistente (soft delete sem restore) |
| auditoria de órgãos | inexistente |
| e2e Playwright | inexistente para o módulo |

## 3. Riscos de dados

### 3.1 Migração de schema

- `title` é nullable no banco e obrigatório na aplicação. Linhas inseridas por
  script/importador sem título passam; o portal pode exibir vazio.
- O backfill de slug usa `lower(replace(...))`, não `Str::slug`; pode gerar
  slugs com `--`, acentos ou caracteres não previstos pela rota.
- A unique parcial de `slug` pode falhar no backfill se dois números
  normalizarem para o mesmo valor.
- `v2_licitacoes_slug_unique` não trata `NULL`; várias linhas sem slug são
  aceitas, e `public_url` gera URL inválida.
- `value` é `decimal(15,2)`, mas o model usa `float`; somas e comparações no
  PHP podem perder centavos.
- Três datas sem timezone; uma futura migração para `timestamptz` precisa
  decidir o fuso de origem.
- A FK de `status_id` é `SET NULL`; apagar um status (não há tela, mas o banco
  permite) deixa licitações sem situação.
- `v1_id` unique parcial impede reimportar a mesma linha, mas não impede
  duplicatas criadas manualmente com o mesmo `number`.

### 3.2 Dados legados no V7

ADR-042 determina conteúdo migrado à mão. Os riscos concretos:

- `object` legado é texto; converter para AST exige decisão editorial (um
  parágrafo por bloco) e revisão;
- `arquivos` jsonb aponta para caminhos do storage antigo; os arquivos precisam
  ser reenviados pelo Media para virar objetos `permanent/`;
- `jurisdicionados` é lista de nomes, não ids; a migração para `orgaos` exige
  reconciliar nomes e criar órgãos;
- status importados podem ter nomes fora dos três canônicos; o CHECK do V7 vai
  rejeitá-los, então a migração precisa de mapa e decisão para desconhecidos;
- datas importadas sem timezone precisam de um fuso documentado.

## 4. Perguntas em aberto para o negócio

1. **Publicação**: `is_active` deve continuar sendo um interruptor separado do
   status, ou “Aberta/Finalizada/Cancelada” deve determinar a visibilidade?
2. **Aprovação**: quem pode publicar/despublicar? A separação
   `editar`/`aprovar` do ADR-033 faz sentido para a equipe de licitações?
3. **Exclusão definitiva**: deve existir na UI ou apenas por operação de
   suporte? O legado permite qualquer perfil `write` excluir.
4. **Órgãos**: um órgão pode ser removido com licitações associadas? A
   resposta define se o V7 bloqueia, desassocia ou oculta na exibição.
5. **Anexos**: qual o limite real por licitação e por arquivo? O legado aceita
   50 MB por arquivo e não limita a quantidade.
6. **Tipos de arquivo**: a allowlist precisa mesmo incluir áudio e vídeo? O uso
   observável é edital/anexo em PDF/DOC/XLS.
7. **Datas**: as três datas têm semântica fixa? “Data de competição” é a
   abertura da sessão ou o prazo de propostas?
8. **Valor**: `show_value=false` deve omitir o valor também de futuras APIs e
   de dados abertos (ADR-018)?
9. **Histórico**: é necessário manter versões do edital/anexo (substituição
   com rastro) ou o modelo atual (delete/upload) basta?
10. **URLs**: slugs antigos precisam de redirect 301 permanente ou o portal
    pode mudar URLs sem compromisso?

## 5. Mapa entre versões

| Conceito | V1 (`biddings`) | V2 (`v2_licitacoes`) | V7 proposto (`licitacoes.licitacoes`) |
|---|---|---|---|
| PK | inteiro | uuid v4 | uuidv7 |
| número | `biddings.number` | `number` (sem unique) | `number` |
| objeto | `object` (texto/HTML) | `object` text com `strip_tags` | `object` jsonb AST + `object_text` |
| valor | `value` | `value` decimal + `show_value` | igual, cast `decimal:2` |
| datas | três datas | três `datetime` | três `timestamptz` |
| status | `statuses` por id | tabela `v2_licitacao_status` | enum `status` + CHECK |
| órgãos | `bidding_jurisdictionals` (names) | jsonb `jurisdicionados` **e** N:N | N:N apenas |
| arquivos | `archives` (pivot) | jsonb `arquivos` **e** tabela `anexos` | tabela `anexos` apenas |
| rastreio | — | `v1_id`, `imported_at`, `metadata` | não existe (ADR-042) |
| anexo | archive | `v2_licitacao_anexos` | `licitacoes.anexos` com `media_data` |
| órgão | secretaria | `v2_licitacao_orgaos` | `licitacoes.orgaos` com unique |
| soft delete | não observado | licitação, anexo e órgão | licitação, anexo e órgão |
| portal | listagem V1 | lista tudo + filtro local | paginação + filtro servidor |
| permissão | `check.route.permission` | `read`/`write` | `ver`/`editar`/`aprovar`/`arquivar` |

## 6. Dívidas técnicas que não devem atravessar a fronteira

- `V2AuditLogger` com buffer e Graylog: o V7 usa trilha nativa (ADR-035).
- `v2_uuid()` (v4): o V7 usa `uuidv7()`.
- `v2_storage_url()`: o V7 resolve URL por `media.stream` (ADR-026/ADR-030).
- Layouts `v2-cms`/`v2-portal`: o V7 tem shell próprio (ADR-016).
- Bootstrap/AdminLTE e ícones `bi-*`: o V7 usa Tailwind/daisyUI e Lucide
  (ADR-009), sem CDN (ADR-002).
- `str_limit`, `csrf_field()` e `method_field()`: helpers do Laravel 5.5.
- `@v2can`: o V7 usa `Permission::allows`/`authorize` no controller
  (ADR-033), não diretiva de view para decisão de acesso.
- JSON ad hoc: o V7 usa `ProblemDetails` (RFC 7807, ADR-024) e respostas
  tipadas.
