# Licitações — Modelagem de Dados

> Fonte das colunas: `psql -U postgres -d prefeituras_v5 -c "\d <tabela>"` (somente leitura).
> Fonte do DDL: migrations em `v6/database/migrations/`.

---

## 1. Tabela `biddings`

Coluna real (verificada no banco `prefeituras_v5`):

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('biddings_id_seq')` | PK |
| `number` | `character varying(15)` | não | — | número da licitação |
| `publication_edital_date` | `timestamp(0) without time zone` | sim | — | data de publicação do edital (NOT NULL removido em 2019) |
| `competition_date` | `timestamp(0) without time zone` | sim | — | data e hora do certame |
| `homologation_date` | `timestamp(0) without time zone` | sim | — | data de homologação |
| `object` | `text` | não | — | objeto (HTML do summernote) |
| `process_number` | `character varying(191)` | sim | — | número do processo |
| `value` | `double precision` | não | — | valor estimado |
| `show_value` | `boolean` | não | — | publica o valor no portal? |
| `management_units_id` | `integer` | não | — | FK → `management_units(id)` |
| `bidding_modalities_id` | `integer` | não | — | FK → `bidding_modalities(id)` |
| `bidding_types_id` | `integer` | não | — | FK → `bidding_types(id)` |
| `statuses_id` | `integer` | não | — | FK → `statuses(id)` |
| `created_at` | `timestamp(0) without time zone` | sim | — | |
| `updated_at` | `timestamp(0) without time zone` | sim | — | |
| `deleted_at` | `timestamp(0) without time zone` | sim | — | soft delete |
| `local` | `text` | sim | — | local da licitação (coluna adicionada em 2022) |
| `is_show` | `boolean` | sim | — | marca “show artístico” (coluna adicionada em 2023) |

Índices / constraints:

| Nome | Tipo | Colunas |
|---|---|---|
| `biddings_pkey` | PRIMARY KEY | `id` |
| `biddings_bidding_modalities_id_foreign` | FOREIGN KEY | `bidding_modalities_id` → `bidding_modalities(id)` |
| `biddings_bidding_types_id_foreign` | FOREIGN KEY | `bidding_types_id` → `bidding_types(id)` |
| `biddings_management_units_id_foreign` | FOREIGN KEY | `management_units_id` → `management_units(id)` |
| `biddings_statuses_id_foreign` | FOREIGN KEY | `statuses_id` → `statuses(id)` |

**Não existe** índice único sobre `number`: a unicidade é garantida só em código
(`BiddingController@store`/`@update`).

## 2. Tabela `bidding_types`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | `nextval('bidding_types_id_seq')` |
| `name` | `character varying(191)` | não | — |
| `created_at` | `timestamp(0)` | sim | — |
| `updated_at` | `timestamp(0)` | sim | — |
| `deleted_at` | `timestamp(0)` | sim | — |

PK `bidding_types_pkey`; referenciada por `biddings.bidding_types_id`.
Conteúdo atual no banco: `Alienação`, `Compras e Serviços`,
`Obras e Serviços de Engenharia` (ids 1, 2, 3).

## 3. Tabela `bidding_modalities`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | `nextval('bidding_modalities_id_seq')` |
| `name` | `character varying(191)` | não | — |
| `created_at` | `timestamp(0)` | sim | — |
| `updated_at` | `timestamp(0)` | sim | — |
| `deleted_at` | `timestamp(0)` | sim | — |

PK `bidding_modalities_pkey`; referenciada por `biddings.bidding_modalities_id`.
**27 modalidades** cadastradas no banco (13 do seeder da Lei 8.666/93, 14 do
seeder de leis novas + `Credenciamento`).

## 4. Tabela `biddings_has_archives` (pivot)

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | `nextval('biddings_has_archives_id_seq')` |
| `biddings_id` | `integer` | não | — |
| `archives_id` | `integer` | não | — |
| `deleted_at` | `timestamp(0)` | sim | — |
| `created_at` | `timestamp(0)` | sim | — |
| `updated_at` | `timestamp(0)` | sim | — |

| Constraint | Tipo | Colunas |
|---|---|---|
| `biddings_has_archives_pkey` | PRIMARY KEY | `id` |
| `fk_biddings_has_archives_biddings1_idx` | INDEX + FOREIGN KEY | `biddings_id` → `biddings(id)` |
| `fk_biddings_has_archives_archives1_idx` | INDEX + FOREIGN KEY | `archives_id` → `archives(id)` |

Observação: a pivot tem `deleted_at`, mas `Bidding::archives()` **não** aplica
`SoftDeletes` — registros “removidos” na pivot continuam visíveis nas consultas.

## 5. Tabela `bidding_jurisdictionals`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | `nextval('bidding_jurisdictionals_id_seq')` |
| `biddings_id` | `integer` | não | — |
| `contents_id` | `integer` | não | — |
| `created_at` | `timestamp(0)` | sim | — |
| `updated_at` | `timestamp(0)` | sim | — |

FKs: `bidding_jurisdictionals_biddings_id_foreign` → `biddings(id)` e
`bidding_jurisdictionals_contents_id_foreign` → `contents(id)`.
**Sem** `deleted_at` (apesar de `BiddingJurisdictionals` importar `SoftDeletes`).

## 6. Tabela `contracts`

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | `nextval('contracts_id_seq')` |
| `number` | `character varying(191)` | não | — |
| `signature_date` | `date` | não | — |
| `publication_date` | `date` | não | — |
| `validity_date` | `date` | sim | — |
| `proposals_id` | `integer` | sim | — |
| `created_at` | `timestamp(0)` | sim | — |
| `updated_at` | `timestamp(0)` | sim | — |
| `deleted_at` | `timestamp(0)` | sim | — |
| `value` | `double precision` | sim | `0` |
| `object` | `text` | sim | — |
| `bidding_id` | `bigint` | sim | — |

| Constraint | Tipo | Colunas |
|---|---|---|
| `contracts_pkey` | PRIMARY KEY | `id` |
| `contracts_proposals_id_foreign` | FOREIGN KEY | `proposals_id` → `proposals(id)` |
| `contracts_bidding_id_foreign` | FOREIGN KEY | `bidding_id` → `biddings(id)` **ON DELETE SET NULL** |

Observação: `bidding_id` é `bigint` enquanto `biddings.id` é `integer` — a
referência cruzada foi criada com `unsignedBigInteger` na migration, mas o
`biddings.id` é `increments()` (integer). Funciona em PostgreSQL, mas é
inconsistente.
A coluna `agent` **não existe** na v6 (existe apenas na v5 — ver
`08-GAPS-E-DIVIDAS.md`).

## 7. Tabelas relacionadas (não exclusive do módulo)

| Tabela | Papel no módulo | FK relevante |
|---|---|---|
| `proposals` | propostas de cada licitação (`winner`, `value`, `proponents_id`, `biddings_id`, `statuses_id`) | `proposals_biddings_id_foreign` → `biddings(id)` |
| `archives` | arquivo do edital/anexo (`title`, `description`, `type`, `path`, `name`, `extension`, `biddings_id`, ...) | `archives_biddings_id_foreign` → `biddings(id)` |
| `statuses` | situações; categoria 2 = Aberta (5), Finalizada (6), Cancelada (7) | `biddings.statuses_id` |
| `management_units` | unidade gestora | `biddings.management_units_id` |
| `contents` | secretarias/órgãos (categoria de conteúdo `slug = 'secretarias'`) | `bidding_jurisdictionals.contents_id` |
| `contract_archives` | pivot contratos ↔ arquivos | `contract_archives_contract_id_foreign` → `contracts(id)` |

## 8. Relacionamentos declarados no model `Bidding`

Definidos em `v6/app/Http/Model/Prefecture/Bidding.php`:

| Método | Tipo | Relação | Observação |
|---|---|---|---|
| `archives()` | `belongsToMany` | `Archive` via `biddings_has_archives` (`biddings_id`,`archives_id`) | `withTimestamps()` |
| `proposals()` | `hasMany` | `Proposal` (`biddings_id`) | |
| `propostas()` | `hasMany` | `Proposal` (`biddings_id`) | **duplicata** de `proposals()` (mesma tabela, grafia diferente) |
| `managementUnits()` | `hasOne` | `ManagementUnit` (`management_units_id`) | deveria ser `belongsTo` |
| `biddingTypes()` | `belongsTo` | `BiddingType` (`bidding_types_id`) | correto |
| `statuses()` | `hasOne` | `Status` (`statuses_id`) | deveria ser `belongsTo` |
| `contents()` | `belongsToMany` | `Content` via `bidding_jurisdictionals` | `withTimestamps()` |
| `contracts()` | `hasMany` | `Contract` (`bidding_id`) | |
| `modalities()` | `hasOne` | `BiddingModality` (`bidding_modalities_id`) | deveria ser `belongsTo` |
| `biddingJurisdictionals()` | `belongsTo` | `BiddingJurisdictionals` (`id`, `biddings_id`) | **invertido**: funciona só porque a view usa `->contents` de um único registro |

Modelos auxiliares:

| Model | Relação declarada | Situação |
|---|---|---|
| `BiddingType::biddings()` | `belongsTo(Bidding, 'bidding_type_id', 'id')` | **coluna inexistente** (`bidding_types_id` é a real) — relação quebrada |
| `BiddingModality::biddings()` | `belongsTo(Bidding, 'bidding_modalities_id', 'id')` | correta |
| `Contract::bidding()` | `belongsTo(Bidding, 'bidding_id', 'id')` | correta |
| `Contract::proposals()` | `hasOne(Proposal, 'id', 'proposals_id')` | funciona |
| `Archive::biddings()` | `belongsToMany(Bidding, 'biddings_has_archives', 'biddings_id')` | correta |
| `BiddingHasArchive::biddings()` / `::archives()` | `hasMany` com chaves invertidas | relação duvidosa, não é usada pelo fluxo |
| `BiddingJurisdictionals::contents()` | `hasMany(Content, 'id', 'contents_id')` | usada pela view de detalhe |

## 9. Scopes de eager loading (portal)

Em `v6/app/Http/Model/Prefecture/Bidding.php`:

| Scope | Relations carregadas |
|---|---|
| `scopeRelationsBidding()` | `statuses`, `modalities`, `biddingTypes`, `managementUnits`, `biddingJurisdictionals`, `proposals.contracts`, `proposals.proponents`, `propostas.proponents`, `archives` |
| `scopeRelationsBiddingWithProposal()` | `statuses`, `modalities`, `biddingTypes`, `managementUnits`, `proposals.contracts`, `proposals.proponents` |

O scope de listagem carrega `proposals.contracts` **e** `proposals.proponents`
**e** `propostas.proponents` (a relação duplicada) — três leituras extras por
licitação.

## 10. View SQL `licitacao`

Criada por `v6/database/migrations/2023_04_06_100010_create_view_to_licitacao.php`
(equivalente em `v5/database/migrations/2023_04_06_100010_create_view_to_licitacao.php`).
DDL real transcrito do arquivo:

```sql
CREATE OR REPLACE VIEW licitacao
as select
    row_number() OVER (ORDER BY b.created_at desc) as value,
    b."number" as numero,
    b.value as valor,
    bm."name" as modalidade,
    to_char(b.publication_edital_date::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
    bt."name" as tipo,
    s."name" as situacao,
    b.id as id

from biddings b
    join bidding_modalities bm on bm.id = b.bidding_modalities_id
    join bidding_types bt on bt.id = b.bidding_types_id
    join statuses s on s.id = b.statuses_id
where
    b.deleted_at is null
order by b.created_at desc ;
```

Colunas resultantes (verificadas no banco):

| Coluna | Tipo | Origem |
|---|---|---|
| `value` | `bigint` | `row_number()` sobre `created_at desc` (numeração da linha, **não** valor monetário) |
| `numero` | `character varying(15)` | `b.number` |
| `valor` | `double precision` | `b.value` |
| `modalidade` | `character varying(191)` | `bm.name` |
| `data_publicacao` | `text` | `to_char(publication_edital_date, 'DD/MM/YYYY')` |
| `tipo` | `character varying(191)` | `bt.name` |
| `situacao` | `character varying(191)` | `s.name` |
| `id` | `integer` | `b.id` |

**O que a view abre**: é a fonte do DataTable do CMS
(`Prefecture\DatatableController@get_licitacao`, que faz
`DB::table('licitacao')->get()`) e, por consequência, do botão de exclusão em
lote. Ela materializa modalidade, tipo e situação como texto, evitando joins no
PHP. Consequências:

- `INNER JOIN` em `bidding_modalities`, `bidding_types` e `statuses` — licitações
  com FK órfã (ou com registro deletado logicamente) **somem** da listagem do CMS.
- Não expõe `is_show`, `show_value`, `local`, `value` monetário como nome
  intuitivo (o valor está em `valor`), nem os secretários jurisdicionados.
- A coluna `value` é a numeração da linha; o valor monetário é `valor` — a
  nomenclatura confunde e é replicada no JS.
- `to_char(...::timestamp with time zone ...)` converte para o fuso do servidor;
  em UTC isso pode deslocar a data em um dia.
- `down()` faz `DROP VIEW if exists public.licitacao;`.

## 11. Migrations do módulo (v6)

| Arquivo | Data | Efeito |
|---|---|---|
| `v6/database/migrations/2018_04_02_181800_create_bidding_modalities_table.php` | 2018-04-02 | cria `bidding_modalities` (id, name, timestamps, softDeletes) |
| `v6/database/migrations/2018_04_02_181801_create_bidding_types_table.php` | 2018-04-02 | cria `bidding_types` |
| `v6/database/migrations/2018_04_02_181804_create_biddings_table.php` | 2018-04-02 | cria `biddings` + FKs para `management_units`, `bidding_modalities`, `bidding_types`, `statuses` |
| `v6/database/migrations/2018_04_10_113409_create_biddings_has_archives_table.php` | 2018-04-10 | cria a pivot `biddings_has_archives` com softDeletes |
| `v6/database/migrations/2018_09_24_153635_create_bidding_jurisdictionals_table.php` | 2018-09-24 | cria `bidding_jurisdictionals` |
| `v6/database/migrations/2019_10_30_172701_remove_notnull_publication_edital_date_from_biddings_table.php` | 2019-10-30 | `ALTER TABLE biddings ALTER COLUMN publication_edital_date DROP NOT NULL` |
| `v6/database/migrations/2022_04_18_103059_add_column_to_biddings_table.php` | 2022-04-18 | adiciona `biddings.local` (text, nullable) |
| `v6/database/migrations/2023_03_01_113315_add_collumn_is_show_to_biddings.php` | 2023-03-01 | adiciona `biddings.is_show` (boolean, nullable) |
| `v6/database/migrations/2023_04_06_100010_create_view_to_licitacao.php` | 2023-04-06 | cria a view SQL `licitacao` |
| `v6/database/migrations/2024_02_08_134940_add_new_modalitie_to_biddings.php` | 2024-02-08 | insere a modalidade `Credenciamento` |
| `v6/database/migrations/2025_03_07_015102_add_biddings_foreing_key_to_contracts_table.php` | 2025-03-07 | adiciona `contracts.bidding_id` (FK `set null`) |

Migrations relacionadas fora do escopo estrito: `2018_11_21_121507_create_biddings_legacys_table.php`
e `2018_11_21_121854_create_archives_biddings_legacy_table.php` (licitações
"antigas", módulo `licitacoes-antigas`).

## 12. Seeds

| Arquivo | Efeito |
|---|---|
| `v6/database/seeds/BiddingTypeTableSeeder.php` | `DELETE` + insere `Alienação`, `Compras e Serviços`, `Obras e Serviços de Engenharia` |
| `v6/database/seeds/BiddingModalityTableSeeder.php` | `DELETE` + insere 13 modalidades da Lei 8.666/93 |
| `v6/database/seeds/NewBiddingModalities.php` | insere 14 modalidades (Lei 14.133/21, MP 1.047/21, internacionais) — **não** apaga as anteriores |
| `v6/database/seeds/ResourcesTableSeeder.php` | cria o recurso `licitacao` (modulo_id 21) e `licitacao-anterior` (modulo_id 20) |
| `v6/database/seeds/RolesTableSeeder.php` | concede as permissões `licitacao.*` (index, create, edit, delete, inactive, inactivate, restore, show) |

Atenção: `BiddingModalityTableSeeder` faz `DELETE` em `bidding_modalities`
— rodar os seeders fora de um ambiente novo apaga as modalidades cadastradas
pelo operador (incluindo `Credenciamento`).
