# Arquivo (Drive) — Modelagem de Dados

> **Escopo:** v5. Banco `prefeituras_v5` (PostgreSQL 10). Schema verificado com
> `psql \d archives`, `\d archive_categories` e `\d` de cada pivot.

---

## 1. Tabela `archives` — o "arquivo morto" central

É a tabela mais compartilhada do sistema. **26 colunas**, 8 FKs de saída,
soft delete, e nenhum escopo por módulo (a mesma linha pode ser "de" um
conteúdo, de uma página estática, de uma obra, de um contrato… ao mesmo
tempo).

### 1.1 Colunas

| # | Coluna | Tipo (PostgreSQL) | Null | Origem | Descrição / uso real |
|---|---|---|---|---|---|
| 1 | `id` | `integer` (serial) | **NOT NULL** | `2018_04_02_181807` | PK. `increments('id')`. |
| 2 | `title` | `varchar(300)` | sim | `2018_04_02_181807` | Título exibido ao downloads. Preenchido só por alguns fluxos (destaque, obras, manual). |
| 3 | `description` | `text` | sim | `2018_04_02_181807` | Descrição livre; aparece no portal no lugar do nome. |
| 4 | `type` | `varchar(191)` | sim | `2018_04_02_181807` | MIME do arquivo (`image/jpeg`, `application/pdf`). |
| 5 | `hash` | `varchar(300)` | sim | `2018_04_02_181807` | `md5_file()` do temporário. **Não é único** e não é usado para deduplicação. |
| 6 | `path` | `varchar(300)` | sim | `2018_04_02_181807` | Caminho/URL do arquivo. **Nullable e com formatos heterogêneos** (ver §1.4). |
| 7 | `name` | `varchar(300)` | sim | `2018_04_02_181807` | Nome do arquivo (com ou sem hash). |
| 8 | `extension` | `varchar(191)` | sim | `2018_04_02_181807` | Extensão sem ponto, em minúsculas (`strtolower`). |
| 9 | `status_create` | `boolean` | sim | `2018_04_02_181807` | **Sem uso** — nenhuma escrita no código da v5. |
| 10 | `highlight` | `boolean` | sim | `2018_04_02_181807` | `true` = imagem de destaque; `NULL` = item de galeria. Dualidade booleano/NULL usada como tri-estado. |
| 11 | `deleted_at` | `timestamp(0)` | sim | `2018_04_02_181807` | Soft delete (`SoftDeletes` no model). |
| 12 | `created_at` | `timestamp(0)` | sim | `2018_04_02_181807` | `timestamps()`. |
| 13 | `updated_at` | `timestamp(0)` | sim | `2018_04_02_181807` | `timestamps()`. |
| 14 | `contents_id` | `integer` | sim | `2018_04_02_181807` | FK → `contents(id)`. Galeria/destaque de conteúdo. |
| 15 | `static_pages_id` | `integer` | sim | `2018_04_02_181807` | FK → `static_pages(id)`. Galeria de página estática. |
| 16 | `biddings_id` | `integer` | sim | `2018_04_02_181807` | FK → `biddings(id)`. **Legado**: o fluxo atual usa o pivot `biddings_has_archives`. |
| 17 | `calendar_events_id` | `integer` | sim | `2022_01_07_111645` | FK → `calendar_events(id)`. Galeria de evento. |
| 18 | `order` | `integer` | sim | `2022_04_12_124253` | Posição na galeria. **Nullable** e sem índice. |
| 19 | `view_pdf` | `boolean` | sim | `2022_11_29_092240` | `true` = arquivo embutido em `<iframe>` na página do portal. |
| 20 | `reference_id` | `text` | sim | `2023_03_31_142413` | **FK lógica em `text`** (não há constraint). Usada por `PaginasSecretaria`. |
| 21 | `type_size_id` | `bigint` | sim | `2023_07_24_130504` | FK → `types_sizes(id)`. Tamanho de exibição. |
| 22 | `width` | `varchar(191)` | sim | `2023_08_03_100129` | Largura em px (personalização do destaque). |
| 23 | `height` | `varchar(191)` | sim | `2023_08_03_100129` | Altura em px. |
| 24 | `requerimento_id` | `bigint` | sim | `2023_09_19_164831` | FK → `requerimento_diarias(id)` ON DELETE CASCADE. |
| 25 | `highlight_id` | `integer` | sim | `2025_03_31_210313` | FK → `highlights(id)` ON DELETE CASCADE. (coluna aditiva; o *outro* highlight é o booleano `highlight`) |
| 26 | `category_id` | `bigint` | sim | `2025_10_16_105256` | FK → `archive_categories(id)` ON DELETE SET NULL. **Somente v5.** |

> Observação de contagem: a migration `2018_04_02_181807` criou 16 colunas;
> as demais 10 vieram de 9 migrations posteriores. A coluna
> `order` **não** está no `$fillable` do model v5 — só é gravada via
> `DB::table()` ou por atribuição de propriedade (`$archive->order = …`).

### 1.2 Foreign keys (do `psql \d archives`)

| Constraint | Coluna | Referência | On delete |
|---|---|---|---|
| `archives_biddings_id_foreign` | `biddings_id` | `biddings(id)` | RESTRICT |
| `archives_calendar_events_id_foreign` | `calendar_events_id` | `calendar_events(id)` | RESTRICT |
| `archives_category_id_foreign` | `category_id` | `archive_categories(id)` | **SET NULL** |
| `archives_contents_id_foreign` | `contents_id` | `contents(id)` | RESTRICT |
| `archives_highlight_id_foreign` | `highlight_id` | `highlights(id)` | **CASCADE** |
| `archives_requerimento_id_foreign` | `requerimento_id` | `requerimento_diarias(id)` | **CASCADE** |
| `archives_static_pages_id_foreign` | `static_pages_id` | `static_pages(id)` | RESTRICT |
| `archives_type_size_id_foreign` | `type_size_id` | `types_sizes(id)` | RESTRICT |

Índices: apenas `archives_pkey`. **Nenhum índice** em `contents_id`,
`static_pages_id`, `order`, `category_id`, `path` — as listagens do portal
fazem varredura sequencial.

### 1.3 Tabelas que referenciam `archives`

| Tabela | Coluna | Constraint |
|---|---|---|
| `answer_archives` | `archive_id` | FK → `archives(id)` |
| `answers` | `archives_id` | FK → `archives(id)` |
| `contests` | `archives_id` | FK → `archives(id)` |
| `contract_archives` | `archive_id` | FK → `archives(id)` |
| `diario_configs` | `header_img` | FK → `archives(id)` ON DELETE SET NULL |
| `diarios` | `cover_image_id` | FK → `archives(id)` ON DELETE CASCADE |
| `emendas_has_archives` | `archive_id` | FK → `archives(id)` ON DELETE CASCADE |
| `biddings_has_archives` | `archives_id` | FK → `archives(id)` |
| `manual_areas` | `archive_id` | FK → `archives(id)` |
| `ombudsman_answer_archives` | `archive_id` | FK → `archives(id)` ON DELETE CASCADE |
| `ombudsman_answers` | `archives_id` | FK → `archives(id)` |
| `work_archives` | `archive_id` | FK → `archives(id)` ON DELETE CASCADE |

> A model `Archive::contests()` declara o pivot `contests_has_archives`, que
> **não existe** no banco (verificado: `contests_has_archives` e
> `static_pages_has_archive` não estão em `pg_tables`). A relação
> `contests()` **lança `QueryException` em tempo de execução** (verificado
> via `php artisan`): `relation "contests_has_archives" does not exist`.
> O mesmo vale para `Archive::biddings()`, que gera
> `on "biddings_has_archives"."bidding_id"` — coluna inexistente (a real é
> `biddings_id`).

### 1.4 Formatos de `path` observados (armadilha)

| Origem | Exemplo de `path` |
|---|---|
| `ArchiveController@saveArchivesFromGalleryOrDriver` | `storage/content/midias/{name}` |
| `ContentController@ajaxUploadFiles` (galeria) | `storage/content/{cat}/{id}/galeria/{name}` |
| `ContentController@ajaxUploadFiles` (midias) | `storage/content/midias/{id}/{name}` |
| `AnswerController` | `http://host/storage/answer/{id}/{name}` (**URL absoluta**) |
| `ContractController` | `http://host/storage/contratos/{id}/{name}` (**URL absoluta**) |
| `Works\WorkController::storeArchives` | `storage/obras/{workId}/arquivos/{name}` |
| `Works\WorkController::saveArchivesFromGallery` | `storage/works/{name}` |
| `ArchiveRepository::upInsert` | caminho **sem** o prefixo `storage` (faz `str_replace`) |
| `ObraParalisadaController` | `docs/{arquivo}` (fora da tabela `archives`) |

As views do portal usam `file_exist($archive->path)` e `url($archive->path)`,
o que **quebra** para os registros com URL absoluta (o `file_exists` recebe
`http://…`) e para os que não têm o prefixo `storage`.

## 2. Tabela `archive_categories` (somente v5)

```
id          integer      NOT NULL  (serial, PK)
name        varchar(100) NOT NULL
slug        varchar(100) NOT NULL  UNIQUE
created_at  timestamp(0)
updated_at  timestamp(0)
deleted_at  timestamp(0)
```

- Unique: `archive_categories_slug_unique`.
- Seed de 4 categorias em `v5/database/migrations/2025_10_16_105153_create_archive_categories_table.php`:
  `Documentos Oficiais / documentos-oficiais`, `Relatórios / relatorios`,
  `Fotos / fotos`, `Vídeos / videos`.
- **O model v5 não usa `SoftDeletes`** — apesar de a tabela ter
  `deleted_at`. O controller filtra manualmente:
  `ArchiveCategoryController@getCategories()` → `where('deleted_at', null)`.
  Consequência: **não há como excluir uma categoria pela aplicação**;
  `ON DELETE SET NULL` nunca é exercitado.
- `$fillable = ['name','slug']` — o `slug` é gerado por
  `slug_fix($data['name'])`, helper global em
  `v5/app/Helpers/functions.php:771`.
- **Não há índice** em `name`; a unicidade real é do `slug`.

### 2.1 Ligação `archive_categories` ↔ `archives`

**Não é ManyToMany.** A migration
`v5/database/migrations/2025_10_16_105256_add_category_into_archives.php`
cria **uma coluna `category_id` simples** com FK para
`archive_categories(id)`:

```php
Schema::table('archives', function (Blueprint $table) {
    // Adiciona a coluna category_id
    $table->unsignedBigInteger('category_id')->nullable()->after('id');

    // Cria a chave estrangeira com a tabela category
    $table->foreign('category_id')
          ->references('id')
          ->on('archive_categories')
          ->onDelete('set null'); // ao excluir uma categoria, o campo vira null
});
```

Portanto: **1 arquivo pertence a no máximo 1 categoria**
(`belongsTo`), e uma categoria possui N arquivos (`hasMany`).
Não existe tabela pivot `archive_category_archive`.

## 3. Tabelas pivot (junção)

| Pivot | Colunas | PK | Timestamps | Soft delete | FKs |
|---|---|---|---|---|---|
| `work_archives` | `id`, `work_id` (bigint NN), `archive_id` (bigint NN), `type` (varchar NN), `deleted_at`, `created_at`, `updated_at` | `id` | sim | sim | `work_id`→`works(id)` CASCADE; `archive_id`→`archives(id)` CASCADE |
| `biddings_has_archives` | `id`, `biddings_id` (int NN), `archives_id` (int NN), `deleted_at`, `created_at`, `updated_at` | `id` | sim | sim | `biddings_id`→`biddings(id)`; `archives_id`→`archives(id)` (RESTRICT) |
| `answer_archives` | `id`, `answer_id` (int NN), `archive_id` (int NN) | `id` | **não** | **não** | ambas RESTRICT |
| `contract_archives` | `id`, `contract_id` (int NN), `archive_id` (int NN) | `id` | **não** | **não** | ambas RESTRICT |
| `ombudsman_answer_archives` | `id`, `ombudsman_answer_id` (int NN), `archive_id` (int NN), `created_at`, `updated_at` | `id` | sim | **não** | ambas **CASCADE** |
| `emendas_has_archives` | `emenda_id` (int NN), `archive_id` (int NN) | **nenhuma** | **não** | **não** | ambas **CASCADE** |

### 3.1 Migrations dos pivots

| Migration | Cria |
|---|---|
| `v5/database/migrations/2018_04_10_113409_create_biddings_has_archives_table.php` | `biddings_has_archives` |
| `v5/database/migrations/2019_08_06_171403_create_answer_archives_table.php` | `answer_archives` |
| `v5/database/migrations/2019_09_13_105950_create_contracts_archives_table.php` | `contract_archives` |
| `v5/database/migrations/2025_04_01_021111_create_work_archives_table.php` | `work_archives` |
| `v5/database/migrations/2026_03_13_103000_create_emendas_has_archives_table.php` | `emendas_has_archives` |
| `v5/database/migrations/2026_08_06_162636_create_ombudsman_answer_archives_table.php` | `ombudsman_answer_archives` |
| `v5/database/migrations/2018_11_21_121854_create_archives_biddings_legacy_table.php` | `archives_biddings_legacy` (legado, **sem FK para `archives`**) |

### 3.2 Coluna `type` em `work_archives`

`v5/app/Enum/Prefecture/Works/ArchiveTypesEnum.php`:

```php
class ArchiveTypesEnum {
    const GALLERY = 'gallery';
    const FILE = 'file';
}
```

- `GALLERY` → imagem da galeria da obra (`WorkController@saveArchivesFromGallery`).
- `FILE` → documento anexo (PDF etc.) da obra (`WorkController::storeArchives`).

O model `Work` usa `belongsToMany(Archive::class, 'work_archives')`; como o
pivot tem `type`, o filtro por tipo é feito com `whereHas('works', fn => …where('type', ArchiveTypesEnum::FILE))`.

## 4. Lookups e tabelas auxiliares

| Tabela | Colunas | Papel |
|---|---|---|
| `types_sizes` | `id` (bigserial), `type` (varchar), `size` (varchar), `timestamps` | Tamanhos de exibição da imagem: `Grande/600px`, `Médio/350px`, `Pequeno/200px` (seed em `2023_07_24_130504`), `Personalizado` (seed em `2023_08_03_100129`, com `size` NULL). |
| `highlights` | `id`, `title`, `subtitle`, `text_button`, `url`, `position`, `status`, … | Banners "destaque"; `archives.highlight_id` aponta para eles (CASCADE). |
| `manual_areas` | `archive_id` → `archives(id)` | Cada área do manual do sistema tem uma imagem. |
| `requerimento_diarias_configs` | `archive` (varchar), `path` (varchar) | Padrão de documento — **caminho em string**, não FK para `archives`. Duplicação de conceito. |

## 5. Migrations que criam/alteram `archives`

| Data | Migration | Efeito |
|---|---|---|
| 2018-04-02 | `2018_04_02_181807_create_archives_table.php` | Cria a tabela com 16 colunas + 3 FKs (`contents_id`, `static_pages_id`, `biddings_id`). |
| 2022-01-07 | `2022_01_07_111645_add_calendar_event_id_to_archives.php` | + `calendar_events_id` (FK). |
| 2022-04-12 | `2022_04_12_124253_add_column_to_archives_table.php` | + `order` (integer, nullable). |
| 2022-11-29 | `2022_11_29_092240_add_viewpdf_to_archives_table.php` | + `view_pdf` (boolean). |
| 2023-02-01 | `2023_02_01_083626_add_itens_to_archives.php` | **Migration de dados**: popula 91 arquivos do manual do sistema e os `manual_areas` correspondentes. |
| 2023-03-31 | `2023_03_31_142413_add_collum_reference_id_to_archives.php` | + `reference_id` (**text**). |
| 2023-07-24 | `2023_07_24_130504_add_column_type_size_id_to_archives.php` | + `type_size_id` (FK) e seed de 3 tamanhos. |
| 2023-08-03 | `2023_08_03_100129_add_width_and_height_to_archives.php` | + `width`, `height`; seed de "Personalizado". |
| 2023-09-19 | `2023_09_19_164831_add_requerimento_id_to_archives.php` | + `requerimento_id` (FK CASCADE). |
| 2025-03-31 | `2025_03_31_210313_create_highlights_table.php` | Cria `highlights` e **adiciona** `archives.highlight_id`. |
| 2025-10-16 | `2025_10_16_105153_create_archive_categories_table.php` | Cria `archive_categories` + 4 categorias. |
| 2025-10-16 | `2025_10_16_105256_add_category_into_archives.php` | + `category_id` (FK SET NULL). |

## 6. Seeds

| Seed | Efeito |
|---|---|
| `v5/database/seeds/ArchivesTableSeeder.php` | Insere 4 imagens de destaque de banners (ids 67-70 na base) com `highlight = true`. |
| `v5/database/seeds/ExtrasSeeder.php:60-71` | Normaliza `order` de todos os arquivos cujo `path` contém `%midias%` (usa `order = id`). |
| `v5/database/seeds/UploadPdfLegislation.php` | Insere arquivos de legislação (linhas 1750, 1775, 1798). |
| `v5/database/migrations/2023_02_01_083626_add_itens_to_archives.php` | Seed de 91 imagens + `manual_areas` (dados do manual). |
| `v5/database/migrations/2025_10_16_105153_create_archive_categories_table.php` | Seed de 4 categorias. |

> `ExtrasSeeder` só normaliza `order` para arquivos de `%midias%` — os
> arquivos das galerias de conteúdo recebem `order` apenas via
> `/order` (`defaultOrder`).

## 7. Views SQL

**Não há views SQL que leiam `archives`.** Verificado com
`pg_views WHERE definition ILIKE '%archives%'` → **0 linhas**. As ~60
views do banco (`noticias`, `publicacoes`, `licitacao`, `contratos`,
`listagem_obras`, …) não tocam a tabela de arquivos.

## 8. Estado atual da base `prefeituras_v5`

| Métrica | Valor |
|---|---|
| Total de linhas em `archives` | 91 |
| Linhas com `contents_id` | 4 (todas `highlight = true`) |
| Linhas com `static_pages_id` / `calendar_events_id` / `biddings_id` / `requerimento_id` / `highlight_id` / `type_size_id` / `category_id` | **0** |
| Linhas com `path LIKE '%midias%'` | **0** |
| Linhas com `order` preenchido | **0** |
| Linhas com `updated_at` preenchido | **0** |
| Linhas soft-deleted | 0 |
| Categorias em `archive_categories` | 4 (as do seed) |

> Os 91 registros são **exclusivamente as imagens do manual do sistema**
> (caminhos `storage/manual-usuario/area/N/destaque/…`). O acervo real
> do Drive nunca foi populado neste ambiente — a funcionalidade existe no
> código, mas o módulo está **operacionalmente vazio** na base analisada.
