# Arquivo (Drive) — Regras e Portabilidade

> **Escopo:** v5. Regras não óbvias, acoplamentos e armadilhas reais
> encontradas no código e no banco.

---

## 1. Regras de negócio não óbvias

### R01 — `highlight` é um tri-estado, não um booleano

| Valor | Significado |
|---|---|
| `true` | É a **imagem de destaque** do conteúdo/evento/página. |
| `NULL` | É **item de galeria** (aparece na galeria de fotos). |
| `false` | Item de galeria de página de secretaria (`PaginasSecretaria::archives()` filtra `highlight = false`). |

Consequência: `whereNull('highlight')` e `where('highlight', true)` **não** são
complementares — há um terceiro estado (`false`) que fica fora das duas
consultas.

### R02 — `view_pdf` é um interruptor de exibição, não um tipo

`view_pdf = true` marca o arquivo que o portal **embute** em `<iframe>`
(ou exibe como `<img>` se for JPEG/PNG) e **exclui** o arquivo da lista de
downloads no desktop (`view_pdf/files-list.blade.php`).

### R03 — `order` só existe em alguns fluxos

- Upload no **Drive** (`/content/midias`): `order = id` (assign em
  `saveArchivesFromGalleryOrDriver`).
- Upload via `ContentController@ajaxUploadFiles` (bucket `midias`):
  `order` **não** é gravado (a migration `ExtrasSeeder` normaliza depois).
- Galeria de conteúdo: `order` só existe se o JS `sortable-galerias.js`
  chamar `POST /order` — o que **não acontece** (JS morto).
- `ExtrasSeeder` normaliza apenas arquivos com `path LIKE '%midias%'`.

Na prática, `order` está **populado ou não, conforme o fluxo**, sem
garantia.

### R04 — O caminho do arquivo determina o "escopo" do Drive

Não há coluna de escopo: o "bucket" do Drive é identificado por
`path LIKE '%midias%'`. Qualquer arquivo cujo caminho contenha a substring
`midias` aparece no Drive — e vice-versa.

### R05 — Compressão gera variantes, não substitui

`FileTrait::compress()` grava, para imagens, as variantes `_min` (200px),
`_med` (380px) e `_larg` (1480px) ao lado do original, e só reescreve o
original se ele tiver > 100 KB (`extractedCompress`).

As variantes **não** têm registro em `archives` — o portal as localiza por
convenção de nome (`getMinFile()`, `getMedFile()`, `getLargFile()`).

`compress()` **não roda** em GIF (`in_array($imgInfo[2], [1,2,3])` cobre
GIF/JPEG/PNG, mas o `ContentController` evita explicitamente GIF e vídeo).

### R06 — A categoria é obrigatória no front, opcional no back

O modal SweetAlert exige nome **e** categoria; o controller aceita
`category` ausente (grava `category_id = null`). Como a coluna é nullable
com `ON DELETE SET NULL`, `null` é um estado válido.

### R07 — Exclusão é lógica, mas o arquivo continua público

`deleteArchives` → `destroy()` → `deleted_at` preenchido. O binário **permanece**
em `storage/app/public/` e continua acessível por `GET /storage/...`.
A exclusão só "despublica" via filtro de listagem.

### R08 — `reference_id` é uma FK lógica em `text`

Usada por `PaginasSecretaria` (`hasOne`/`hasMany` via `reference_id`).
Como o tipo é `text`, não há integridade referencial nem índice.

## 2. Acoplamentos

### 2.1 Dependências do módulo (o que o `arquivo` precisa)

| Depende de | Para quê |
|---|---|
| `contents`, `static_pages`, `calendar_events` | Galerias e destaques. |
| `category_contents`, `category_calendar_events` | Montar o path do arquivo. |
| `archive_categories` | Categoria do arquivo (somente v5). |
| `types_sizes` | Tamanho de exibição da imagem. |
| `storage/app/public` | Binários. |
| `configurations` (`advanced_drive_settings_enabled`) | Liga/desliga o modo avançado. |
| `activity_log` | Log de cadastro de arquivo. |

### 2.2 Dependentes do módulo (quem quebra se `archives` quebrar)

`contents`, `static_pages`, `calendar_events`, `biddings`, `biddings_has_archives`,
`answers`, `answer_archives`, `contracts`, `contract_archives`,
`ombudsman_answers`, `ombudsman_answer_archives`, `emendas_parlamentares`,
`emendas_has_archives`, `works`, `work_archives`, `contests`, `highlights`,
`diarios`, `diario_configs`, `manual_areas`, `requerimento_diarias`,
`requerimento_diarias_configs`, `paginas_secretarias`.

**Uma mudança em `archives` tem blast radius de ~13 módulos.**

### 2.3 Acoplamento com o "Drive" (menu)

O menu lateral usa `case 'midias':` (`v5/app/Providers/MenuServiceProvider.php:283-285`),
que depende do `resources.slug = 'midias'` (id 44) e da
`category_contents.slug = 'midias'` (id 31, "Mídias"). Trocar o slug
quebra o menu **e** a rota `/cms/conteudos/cadastrar/midias`.

## 3. Armadilhas

### A01 — Tabela gigante sem escopo por módulo

`archives` não tem coluna de módulo. Um arquivo de uma obra, de um
contrato e de um conteúdo coexistem na mesma tabela e são filtrados por
colunas distintas (`contents_id`, `work_archives`, `contract_archives`).
Não há CHECK nem trigger que impeça combinações inválidas (ex.: um
arquivo com `contents_id` **e** `static_pages_id` preenchidos).

### A02 — Colunas polimórficas sem discriminator

Sete colunas `*_id` (`contents_id`, `static_pages_id`, `calendar_events_id`,
`biddings_id`, `requerimento_id`, `highlight_id`, `category_id`) + uma
coluna textual (`reference_id`). Não há como saber, só pelo schema, qual
é o "dono" canônico do arquivo.

### A03 — `path` nullable e multi-formato

Ver `02-MODELAGEM.md` §1.4. Caminhos absolutos (`http://host/storage/…`)
quebram `file_exist()` e `url()`.

### A04 — `DB::table()` burla o `SoftDeletes`

`ArchiveController@updateOrder` usa `DB::table('archives')` → arquivos com
`deleted_at` preenchido **participam** da reordenação e podem receber
`order`.

### A05 — `updateOrder` não tem escopo

Troca `order` entre **qualquer** par de arquivos, inclusive de contextos
diferentes (Drive ↔ conteúdo). Um arquivo do Drive pode "roubar" a
posição 1 de um conteúdo.

### A06 — `dd()` em produção

- `ArchiveController@saveArchivesFromGalleryOrDriver`:
  `catch (Exception $exception) { dd($exception); return response()->json(...); }` —
  o `dd()` **interrompe** a execução: o `return` seguinte é inalcançável e o
  cliente recebe um **dump de stack trace** em HTML, não JSON.
- `Repository::upInsert()` base: `catch (\Throwable $th) { dd($th); }`.

### A07 — `where('extension', '!=', null)`

`invalidOperatorAndValue()` do Laravel 5.5 permite `!=` com `null`, e o
`Query\Builder::where()` traduz para `whereNull($column, 'and', true)`
→ `IS NOT NULL`. Funciona, mas é **confuso** e quebra se aquery for
portada para um builder que não faça essa tradução (Laravel 11 usa
`whereNotNull` explícito). Verificado em execução: SQL gerado
`("extension") is not null`.

### A08 — Nome de coluna vindo da URL

`getArchivesFromGalleryOrDriver` faz `->where($colunm, $id)` com `$colunm`
vindo de `{column?}`. O grammar do Laravel 5.5 **envolve o identificador
em aspas** (`wrapValue`), então **não há SQL injection** — verificado:
`where('id) or 1=1 --', 1)` → `"id) or 1=1 --" = ?` → erro do PostgreSQL
`column does not exist`. Mas é **enumeração de colunas**: qualquer coluna
da tabela pode ser consultada por um usuário não autenticado.

### A09 — `slug_fix()` não gera slug canônico

`v5/app/Helpers/functions.php:771`:

```php
function slug_fix($name)
{
    $name = clean_slug($name);
    $name = str_replace(' ', '-', $name);
    $name = preg_replace('/-+/', '-', $name);
    return $name;
}
```

`clean_slug()` remove tudo que não for `[A-Za-z0-9. ]` — **inclusive o
hífen** — e **não minúsculiza**. Verificado em execução:

| Entrada | Saída |
|---|---|
| `TESTE-PROBE` | `TESTEPROBE` |
| `Relatórios` | `Relatorios` |
| `Meu Documento Legal` | `Meu-Documento-Legal` |

Ou seja: `slug_fix("Relatórios")` = `Relatorios` ≠ `relatorios` (o slug
do seed). Criar a categoria "Relatórios" pela UI **não colide** com a
existente, mas cria uma **duplicata visual**.

### A10 — `slug` UNIQUE sem tratamento de duplicidade

`ArchiveCategoryController@store` não captura a `QueryException` de
violação da constraint `archive_categories_slug_unique` → 500 com stack
trace. Verificado em execução:
`duplicate key value violates unique constraint "archive_categories_slug_unique"`.

### A11 — `category_id` não é validado no upload

Se o `category` enviado não existir, o `INSERT` viola a FK
`archives_category_id_foreign` → exceção não capturada (mesma armadilha A06).

### A12 — `saveUpload()` reusa nome e apaga o arquivo anterior

```php
if ($this->fileExistsInPath($nameFile, $dir)) {
    $this->deleteFile($nameFile, $dir);
}
```

`fileExistsInPath()` usa `file_exist("storage/{$dir}/{$nameFile}")` — um
caminho **relativo** que, a partir do `public/` do servidor web, quase
nunca existe. Na prática, **dois uploads com o mesmo `nameFile` no mesmo
diretório sobrescrevem o arquivo em disco**, e as duas linhas em
`archives` apontam para o mesmo path (a primeira vira "arquivo órfão" no
banco).

### A13 — Compressão sobrescreve o arquivo original

`compress()` → `extractedCompress()` reescreve o binário original em
qualidade 90 (JPEG) / 9 (PNG). O `hash` gravado (`md5_file`) é do
**temporário**, não do arquivo final → o `hash` **nunca corresponde** ao
conteúdo servido.

### A14 — `order` fora do `$fillable`

`v5/app/Http/Model/Prefecture/Archive.php` — o `$fillable` **não**
inclui `'order'` nem `'biddings_id'`. `Archive::create(['order' => …])`
seria **silenciosamente ignorado** (mass assignment). O código contorna
com `$archive->order = …; $archive->save();` ou `DB::table()`.

Na **v6**, `'order'` entrou no `$fillable` e `'category_id'` saiu
(ver `08-GAPS-E-DIVIDAS.md`).

### A15 — Relações quebradas no model

Verificado em execução (`$archive->contests()->count()` etc.):

| Relação | Resultado |
|---|---|
| `Archive::contests()` | **QueryException**: `relation "contests_has_archives" does not exist` (o pivot não existe no banco). |
| `Archive::biddings()` | **QueryException**: `column biddings_has_archives.bidding_id does not exist` (a coluna real é `biddings_id`). O model `Bidding::archives()` está correto (`belongsToMany(..., 'biddings_id','archives_id')`). |
| `Archive::answers()` | Funciona, mas é `belongsTo` (não `hasOne`/`hasMany` como o PHPDoc sugere) — retorna **um** modelo. |
| `Archive::biddingModalities()` | Gera SQL contraditório (`"bidding_modalities"."id" is null and ... is not null`) — sempre vazio. |
| `Contest::archives()` | Aponta para `static_pages_has_archive`, tabela **inexistente**. |

### A16 — `ArchiveCategory` sem `SoftDeletes`

A tabela tem `deleted_at`, mas o model **não** usa o trait. O
controller filtra `where('deleted_at', null)` manualmente, e **não existe
rota de exclusão** — a coluna nunca será preenchida.

### A17 — `IArchiveRepository` é uma interface vazia

`v5/app/Repositories/Contracts/IArchiveRepository.php` contém apenas
`interface IArchiveRepository {}`. Não há contrato a respeitar nem a
testar.

### A18 — `Repository::upInsert()` grava linha e arquivo sem transação

O `ArchiveRepository::upInsert()` grava a linha (via
`parent::upInsert()`, que usa `DB::transaction`) e **depois** grava o
arquivo fora da transação. Falha no upload deixa a linha órfã.

### A19 — `path` é gravado com `storage/` mas lido sem

`ArchiveRepository::upInsert()` faz `str_replace('storage', '', $path)`
(grava **sem** o prefixo), enquanto `ArchiveController` grava **com**
o prefixo. As views do portal usam `url($archive->path)` → para os
registros sem o prefixo a URL fica relativa ao root, não ao `/storage`.

### A20 — N+1 e HEAD por item na galeria

O `chargeGallery()` dispara **um `HEAD` por arquivo** (50 por lote) para
verificar a existência — 50 requisições HTTP por rolagem.

### A21 — `hash` duplicado, nunca consultado

`md5_file()` é gravado em `archives.hash`, mas **nenhuma consulta** usa
essa coluna. Não há deduplicação: o mesmo arquivo enviado 2 vezes ocupa
2× o disco.

### A22 — `status_create` e `biddings_id` são colunas mortas

- `status_create`: nenhuma escrita em todo o código v5.
- `biddings_id`: o fluxo atual usa o pivot `biddings_has_archives`; a FK
  direta permanece mas não é alimentada (0 linhas na base).

### A23 — `highlight_id` × `highlight`

Duas colunas com nomes parecidos e **semânticas diferentes**:
`highlight` (boolean, "é a imagem de destaque") e `highlight_id` (FK para
`highlights`, o módulo de banners). Confusão garantida.

### A24 — `work_archives.type` como string livre

`ArchiveTypesEnum` define `gallery`/`file`, mas a coluna é `varchar`
**sem CHECK** — um valor inválido quebra o filtro do `WorkController`.

### A25 — `emendas_has_archives` sem PK

A tabela tem apenas as duas colunas FK, **sem chave primária e sem
timestamps**. Não é possível inserir a mesma combinación duas vezes com
segurança (a duplicata silenciosa depende do plano de execução).

## 4. Código duplicado / copiado entre v5 e v6

| Artefato | v5 | v6 | Observação |
|---|---|---|---|
| `app/Http/Model/Prefecture/Archive.php` | ✅ | ✅ | v6 = v5 **menos** `works()` e `category()`, **mais** `tv_camara_id` e `order` no `$fillable`. |
| `app/Http/Controllers/Prefecture/ArchiveController.php` | ✅ | ✅ | v6 = v5 **menos** categorias/`order = id`/`with('category')`, **mais** `updateArchiveTitle()` e `updateArchivesOrder()`. |
| `app/Http/Controllers/Prefecture/ArchiveCategoryController.php` | ✅ | ❌ | **Removido na v6**. |
| `app/Http/Model/Prefecture/ArchiveCategory.php` | ✅ | ❌ | **Removido na v6**. |
| `app/Http/Model/Prefecture/Works/WorkArchive.php` | ✅ | ❌ | Removido na v6 (módulo "obras" não existe na v6). |
| `database/migrations/…_create_archive_categories_table.php` | ✅ | ❌ | **Removido na v6**. |
| `database/migrations/…_add_category_into_archives.php` | ✅ | ❌ | **Removido na v6**. |
| `public/assets/js/custom/contents/midias/index.js` | ✅ | ✅ | v6 = v5 **sem** todo o fluxo de categoria/cropper (diff de ~65 linhas). |
| `public/assets/js/custom/utilities/helpers/add_archive.js` | ✅ | ✅ | **Idêntico** (`diff` vazio). |
| `public/assets/js/custom/utilities/helpers/delete_archive.js` | ✅ | ✅ | **Idêntico**. |
| `public/assets/js/custom/contents/paginas-estaticas/archives-pages.js` | ✅ | ✅ | Presente na v6. |
| `resources/views/components/inputs/archive.blade.php` | ✅ | ✅ | v6 acrescenta ordenação Sortable + edição de descrição. |
| `resources/views/prefecture/contents/midias/form.blade.php` | ✅ | ✅ | v6 remove o botão "Adicionar Categoria", o modal de edição e o include do editor de imagem. |

## 5. Onde a v6 divergiu da v5 (e por quê, se discoverível)

| Divergência | Motivo discoverrável |
|---|---|
| `ArchiveCategory` e `archive_categories` **removidos** | Não há changelog nem ticket. A v6 reescreveu o Drive como galeria de mídias **sem categorização** — o `modulos` da v6 mantém o slug `drive` (id 30), o que indica reclassificação do escopo, não Removal do módulo. |
| `order` removido da ordenação da listagem | `getArchivesFromGalleryOrDriver` na v6 ordena só por `id DESC` — a ordenação por galeria foi abandonada em favor da ordenação por lista de anexos (`updateArchivesOrder`). |
| `updateOrder` (troca de 2 valores) → `updateArchivesOrder` (lista completa) | A reordenação passa a ser **em lote**, com escopo `related_type`/`related_id` — corrigindo a armadilha A05 da v5. |
| `updateArchiveTitle` (novo) | Permite editar o título/descrição do arquivo **inline** na listagem (o componente `archive.blade.php` da v6 tem botão "Editar nome"). |
| `tv_camara_id` (novo) | Módulo "TV Câmara" (novo na v6) também usa `archives`. |
| `works()` removido do model | O módulo "obras" não existe na v6 (sem `v6/app/Http/Model/Prefecture/Works/`). |
| `category_id` removido do `$fillable` | Consequência da remoção das categorias. |
| Rotas `/cms/drive/*` removidas | Consequência direta da remoção das categorias e do novo modelo de ordenação. |

**Hipótese (não verificável nos repositórios):** a v6 consolidou o Drive
como um repositório de mídias **sem taxonomia**,transferindo a
responsabilidade de organização para o módulo que consome o arquivo. O
`modulos` da v6 mantém `drive` (id 30) e o `resources` correspondente
(`drive`, id 34), então **o Drive continua sendo uma tela do CMS** — o que
reforça que a remoção das categorias foi uma **simplificação** e não um
descarte.

## 6. Checklist de portabilidade para a v7

- [ ] Substituir as 7 colunas `*_id` por `attachable_type` + `attachable_id`
      (ou manter as colunas se a v7 precisar de FKs reais).
- [ ] Recriar `archive_categories` **com** a semântica da v5 (`belongsTo`
      opcional por `category_id`) — decisão a validar com o negócio.
- [ ] Introduzir `FormRequest` com `max:`, `mimes:`/`mimetypes:` e
      validação de `category_id` (`exists:archive_categories,id`).
- [ ] Corrigir a autorização: `Policy` + `authorize()` em **todas** as
      rotas do Drive (o bug A01/B01 é o item mais urgente).
- [ ] Normalizar `path` para um formato único (caminho relativo ao disco
      `public`), com accessor que prepende `/storage/`.
- [ ] Tornar `order` `NOT NULL` com índice composto
      `(attachable_type, attachable_id, order)`.
- [ ] Deletar o binário no soft delete (ou job de limpeza de órfãos).
- [ ] Remover `hash` (ou torná-lo útil para deduplicação com índice
      único parcial).
- [ ] Remover `status_create` e `biddings_id` (colunas mortas).
- [ ] Renomear `highlight` (boolean) para `is_featured` e manter
      `highlight_id` para o módulo de banners.
- [ ] Paginação **no servidor** (o Drive hoje carrega tudo).
- [ ] Auditoria: `created_by`/`updated_by` em `archives`.
