# Modelagem de dados — Licitações

Banco: PostgreSQL. Todas as tabelas do módulo usam PK `varchar(36)` preenchida
pela aplicação com `v2_uuid()` (UUID v4), `timestamps` e, quando indicado,
`softDeletes`. Não há sequence nem identity.

## 1. `v2_licitacoes` — registro principal

Criada em
`V2/Modules/Publicacoes/Licitacoes/Migrations/2026_07_17_000003_create_v2_licitacoes_table.php`
e alterada por quatro migrations posteriores.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | varchar(36) | não | — | PK; UUID v4 gerado no `creating` do model |
| `number` | varchar(50) | não | — | número administrativo; **sem unique** |
| `object` | text | não | — | texto puro, sanitizado pelo mutator |
| `process_number` | varchar(100) | sim | — | número do processo |
| `value` | decimal(15,2) | não | `0` | valor estimado/contratado |
| `show_value` | boolean | não | `true` | controla exibição pública do valor |
| `publication_edital_date` | datetime | sim | — | data do edital |
| `competition_date` | datetime | sim | — | data da competição |
| `homologation_date` | datetime | sim | — | data da homologação |
| `is_active` | boolean | não | `false` | visibilidade no portal |
| `reason` | text | sim | — | observação livre exibida no detalhe |
| `jurisdicionados` | jsonb | não | `'[]'` | array de nomes (origem V1) |
| `arquivos` | jsonb | não | `'[]'` | array `{url,title}` do importador |
| `metadata` | jsonb | sim | — | objeto livre; importador grava `imported_from` e `jurisdicionados` |
| `created_at` / `updated_at` | timestamp | sim | — | timestamps do Eloquent |
| `deleted_at` | timestamp | sim | — | soft delete |
| `status_id` | varchar(36) | sim | — | FK → `v2_licitacao_status.id` `ON DELETE SET NULL` |
| `local` | text | sim | — | local da sessão (origem V1) |
| `management_unit` | varchar(191) | sim | — | unidade gestora (nome resolvido na V1) |
| `v1_id` | integer | sim | — | PK original em `biddings` |
| `imported_at` | timestamp | sim | — | data/hora da importação |
| `title` | varchar(255) | sim | — | exigido pela aplicação, mas **nullable no banco** |
| `slug` | varchar(255) | sim | — | gerado de `number + title`; único quando não nulo |

### 1.1 Índices e constraints

| Nome | Tipo | Definição | Origem |
|---|---|---|---|
| `v2_licitacoes_pkey` | PK | `id` | migration 000003 |
| `v2_licitacoes_number_index` | btree | `number` | 000003 |
| `v2_licitacoes_is_active_index` | btree | `is_active` | 000003 |
| `v2_licitacoes_status_id_foreign` | FK | `status_id` → `v2_licitacao_status.id` | 000007 |
| `idx_v2_licitacoes_status_id` | btree | `status_id` | 000007 |
| `v2_licitacoes_v1_id_unique` | unique parcial | `v1_id WHERE v1_id IS NOT NULL` | 000007 |
| `v2_licitacoes_slug_unique` | unique parcial | `slug WHERE slug IS NOT NULL` | 000009 |
| `idx_v2_licitacoes_slug` | btree | `slug` | 000009 |

O unique parcial de `v1_id` é o que garante idempotência do importador
(`destinationHasV1Id` consulta `withTrashed()`); ele **não** cobre `number`,
portanto duas licitações com o mesmo número são aceitas.

### 1.2 JSONB: formatos observados

`jurisdicionados` — array de strings:

```json
["Secretaria de Educação", "Secretaria de Saúde"]
```

`arquivos` — formato do importador V1 (não é o formato da tabela de anexos):

```json
[
  {"url": "licitacao/123/arquivos/edital.pdf", "title": "Edital"},
  {"url": "licitacao/123/arquivos/ata.pdf", "title": "Ata"}
]
```

`metadata` — objeto livre; o importador grava:

```json
{"imported_from": "v1", "jurisdicionados": ["Secretaria de Educação"]}
```

O campo `arquivos` **não** é lido por nenhuma view do módulo. A tabela
`v2_licitacao_anexos` é a fonte usada pelo portal e pelo CMS.

### 1.3 Comportamento das migrations de alteração

- `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_24_000007_extend_v2_licitacoes_for_legacy_import.php` adiciona
  `status_id`, `local`, `management_unit`, `v1_id`, `imported_at`, a FK de
  status e dois índices, com guardas `hasColumn`/`constraintExists`/`indexExists`.
- `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000008_sanitize_v2_licitacoes_object_html.php` percorre as linhas
  cujo `object` casa com `~* '<[a-z]'` e aplica
  `strip_tags(html_entity_decode(...))`; o `down()` é vazio porque a operação é
  irreversível. É o único ponto que corrige dados históricos.
- `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000009_add_title_and_slug_to_v2_licitacoes_table.php` adiciona
  `title` e `slug` nulos, cria os índices e faz backfill:
  `title = number` e `slug = lower(replace(replace(replace(number,'/','-'),' ','-'),'.','-'))`.
  Esse backfill **não** usa `Str::slug` e pode produzir slugs com caracteres
  que o `generateSlug` nunca produziria.
- `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000012_drop_modalidade_tipo_from_v2_licitacoes_table.php` remove
  `v2_modalidade_id`/`v2_tipo_id` e renomeia `v2_modalidades_licitacao` e
  `v2_tipos_licitacao` para `backup_*`. As tabelas de backup permanecem no
  banco; nenhum model as referencia.

## 2. `v2_licitacao_status`

Criada em `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_24_000006_create_v2_licitacao_status_table.php`.

| Coluna | Tipo | Nulo | Default |
|---|---|---|---|
| `id` | varchar(36) | não | — |
| `name` | varchar(191) | não | — |
| `is_active` | boolean | não | `true` |
| `created_at` / `updated_at` | timestamp | sim | — |

Constraints: PK em `id` e unique em `name`. **Não tem soft delete** e não tem
ordem/descrição/cor persistida.

### 2.1 Seed embutido na migration

A migration cria, de forma idempotente, três registros canônicos:

```php
$statuses = ['Aberta', 'Finalizada', 'Cancelada'];
foreach ($statuses as $name) {
    $exists = DB::table('v2_licitacao_status')->where('name', $name)->exists();
    if (!$exists) {
        DB::table('v2_licitacao_status')->insert([
            'id' => v2_uuid(),
            'name' => $name,
            'is_active' => true,
            'created_at' => date('Y-m-d H:i:s'),
            'updated_at' => date('Y-m-d H:i:s'),
        ]);
    }
}
```

(`V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_24_000006_create_v2_licitacao_status_table.php:87-101`)

O comentário da migration registra que os nomes reais da V1 eram
`Aberta`, `Finalizada` e `Cancelada` — o plano original especulava
`Encerrada`/`Homologada`/`Suspensa`, que não existiam. O importador ainda pode
criar outros nomes via `firstOrCreate`, porque resolve o status V1 por nome.

### 2.2 Cor derivada

`LicitacaoStatus::getColorAttribute()` (`V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoStatus.php:70-93`)
mapeia substrings do nome para classes Bootstrap:

| Nome contém | Cor |
|---|---|
| `aberta` | `success` |
| `andamento` / `em andamento` / `suspensa` | `warning` |
| `finalizada` | `secondary` |
| `concluida` | `success` |
| `cancelada` / `revogada` | `danger` |
| `homologada` / `adjudicada` | `info` |
| `publicada` | `primary` |
| qualquer outro | `primary` |

A ordem do `foreach` importa: `em andamento` é testado antes de `andamento`,
mas ambos devolvem `warning`.

## 3. `v2_licitacao_anexos`

Criada em `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000010_create_v2_licitacao_anexos_table.php`.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | varchar(36) | não | — | PK UUID v4 |
| `licitacao_id` | varchar(36) | não | — | FK → `v2_licitacoes.id` `ON DELETE CASCADE` |
| `file_path` | varchar(500) | não | — | caminho relativo no disco `public` |
| `original_name` | varchar(255) | não | — | nome enviado pelo navegador |
| `title` | varchar(255) | sim | — | título editorial; fallback para `original_name` |
| `mime_type` | varchar(100) | não | — | detectado por `UploadedFile::getMimeType()` |
| `file_size` | bigint unsigned | não | — | bytes |
| `order` | integer | não | `0` | menor = primeiro |
| `created_at` / `updated_at` | timestamp | sim | — | |
| `deleted_at` | timestamp | sim | — | soft delete |

Índices: PK, FK `licitacao_id`, índice em `order`. A FK cascade é acionada
apenas no `forceDelete()`; o soft delete da licitação **não** propaga aos
anexos.

## 4. `v2_licitacao_orgaos`

Criada em `V2/Modules/Publicacoes/Licitacoes/Migrations/2026_08_31_000011_create_v2_licitacao_orgaos_table.php`.

| Coluna | Tipo | Nulo | Default |
|---|---|---|---|
| `id` | varchar(36) | não | — |
| `name` | varchar(255) | não | — |
| `is_active` | boolean | não | `true` |
| `created_at` / `updated_at` | timestamp | sim | — |
| `deleted_at` | timestamp | sim | — |

Índice em `name`; sem unique no banco, embora o controller valide
`unique:v2_licitacao_orgaos,name` na criação e atualização. A validação de
update usa `unique:...,name,' . $id`, que ignora o registro atual.

## 5. Pivô `v2_licitacao_orgao_licitacao`

| Coluna | Tipo | Nulo |
|---|---|---|
| `licitacao_id` | varchar(36) | não |
| `orgao_id` | varchar(36) | não |

PK composta (`licitacao_id`, `orgao_id`); FKs para `v2_licitacoes` e
`v2_licitacao_orgaos`, ambas `ON DELETE CASCADE`; índice em `orgao_id`.
Não há `timestamps` nem soft delete no pivô.

## 6. Relações Eloquent

```
Licitacao
  belongsTo  LicitacaoStatus   (status_id)
  hasMany    LicitacaoAnexo    (licitacao_id) ordenado por order
  belongsToMany LicitacaoOrgao (v2_licitacao_orgao_licitacao)

LicitacaoOrgao
  belongsToMany Licitacao      (v2_licitacao_orgao_licitacao)

LicitacaoStatus
  hasMany    Licitacao         (status_id)

LicitacaoAnexo
  belongsTo  Licitacao         (licitacao_id)
```

Código: `V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:189-217`, `V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoOrgao.php:69-81`,
`V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoStatus.php:61-66`, `V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoAnexo.php:77-84`.
A relação `anexos` já traz `orderBy('order')`; a view do portal reordena em
memória com `sortBy('order')`, o que é redundante.

## 7. Modelos: casts e mutators

### 7.1 `Licitacao`

```php
protected $casts = [
    'value' => 'float',
    'show_value' => 'boolean',
    'is_active' => 'boolean',
    'publication_edital_date' => 'datetime',
    'competition_date' => 'datetime',
    'homologation_date' => 'datetime',
    'jurisdicionados' => 'array',
    'arquivos' => 'array',
    'metadata' => 'array',
];
```

(`V2/Modules/Publicacoes/Licitacoes/Models/Licitacao.php:103-115`)

Mutators:

- `setObjectAttribute` — `strip_tags(html_entity_decode($value ?? ''))`
  (`:172-175`). Remove HTML de qualquer entrada, inclusive AST JSON colado como
  texto (o JSON não contém tags, mas o texto resultante mantém as chaves).
- `setValueAttribute` — transforma `''`/`null` em `0` e o resto em `float`
  (`:181-184`).

Acessores:

- `public_url` — `route('v2.licitacoes.portal.show', $this->slug)`
  (`:264-267`). Se `slug` for nulo, a rota recebe `null` e gera URL inválida.
- `has_anexos` — executa `anexos()->count() > 0` a cada acesso (`:273-276`);
  não é usado nas views atuais, mas é uma armadilha de N+1 se for adotado.

### 7.2 `LicitacaoAnexo`

Sem casts. Acessores `url` (`v2_storage_url`), `display_name`
(`title ?: original_name`), `icon` (mapa de MIME + prefixos `image/`, `audio/`,
`video/`) e `file_size_formatted` (MB/KB/B). Código em
`V2/Modules/Publicacoes/Licitacoes/Models/LicitacaoAnexo.php:86-140`.

### 7.3 `LicitacaoOrgao` e `LicitacaoStatus`

Ambos com `is_active` convertido para boolean e `id` gerado no `creating`
quando vazio. Nenhum dos dois usa `HasUuids` (Laravel 5.5 não tem).

## 8. Views SQL e seeds

- **Views SQL**: nenhuma. O módulo não cria `CREATE VIEW`.
- **Seeds**: apenas o seed embutido de `v2_licitacao_status` na migration
  000006. Não há `database/seeds` do módulo. Dados de demonstração de
  licitações são gerados pelo `EstatisticasMockCommand`, fora do módulo.
- **Factories**: não há factory de `Licitacao`; os testes criam registros com
  `Licitacao::create([...])`.

## 9. Dados legados e migração de schema

| Origem | Destino | Regra |
|---|---|---|
| `biddings.id` | `v2_licitacoes.v1_id` | inteiro; unique parcial |
| `biddings.object` | `object` | texto; passa pelo mutator que remove HTML |
| `biddings.local` | `local` | texto |
| `management_units.name` | `management_unit` | nome resolvido |
| `statuses.name` (categoria Licitações) | `status_id` | `firstOrCreate` por nome |
| `bidding_jurisdictionals` (contents) | `jurisdicionados` + `metadata.jurisdicionados` | mesmos nomes nas duas colunas |
| `archives` (pivot) | `arquivos` jsonb | `[{url,title}]` com cópia física via Gallery |

O importador (`V2/Modules/Support/MigracaoLegada/Services/LicitacoesLegacyImporter.php`)
calcula `is_active` a partir do status: `Cancelada` → `false`, qualquer outro →
`true`. Linhas soft-deleted na V1 não são extraídas.

**Risco de schema**: `title` é nullable, mas o `LicitacaoRequest` exige; o
backfill de `slug` pode colidir com a unique parcial se dois números gerarem o
mesmo slug (por exemplo, `001/2026` e `001-2026`). A migration não trata a
colisão.
