# 02 — Modelagem de Dados do Módulo Diário Oficial

> Tipos e nullability conferidos no PostgreSQL em execução (`docker exec
> v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5`).

---

## 1. Diagrama de entidades

```
                ┌──────────────────────┐
                │       diarios        │
                ├──────────────────────┤
                │ id (PK)              │
                │ id_certificado (FK)  │──┐
                │ id_complemento (FK)  │◄─┤ auto-relacionamento
                │ data_publicacao      │  │
                │ lei_validadora       │  │
                │ autor                │  │
                │ email                │  │
                │ edicao               │  │
                │ status (bool)        │  │
                │ permission_edit      │  │
                │ columns_number       │  │
                │ has_line             │  │
                │ status_workflow      │  │
                │ created/updated/published_by_user_id (FK users.id)
                │ published_at         │  │
                │ line_style/line_color│  │
                │ cover_image_id (FK)  │──┼──▶ archives.id
                │ generation_*         │  │
                │ deleted_at           │  │
                └──────────┬───────────┘  │
                           │ 1:N          │
                           ▼              │
              ┌────────────────────────┐  │
              │   diario_has_areas     │  │
              ├────────────────────────┤  │
              │ id (PK)                │  │
              │ titulo                 │  │
              │ id_diario (FK)         │  │
              │ id_area (FK)           │──┼──▶ diario_areas.id
              │ id_subarea (FK)        │──┼──▶ diario_sub_areas.id
              │ conteudo (text, null)  │  │
              │ columns_number         │  │
              │ position               │  │
              │ type ('text'|'file')   │  │
              │ file_in (bool)         │  │
              │ path (string, null)    │  │
              │ deleted_at             │  │
              └────────────────────────┘  │
                                           │
              ┌────────────────────────┐  │
              │   diario_certificados  │◄─┘
              ├────────────────────────┤
              │ id (PK)                │
              │ name                   │
              │ path                   │
              │ cert (text)            │
              │ pkey (text)            │
              │ deleted_at             │
              └────────────────────────┘

              ┌────────────────────────┐
              │     diario_configs     │
              ├────────────────────────┤
              │ id (PK) — registro 1   │
              │ lei_padrao (text)      │
              │ certificado_padrao (FK)│──▶ diario_certificados.id
              │ ativos (bool)          │
              │ logo_position          │
              │ header_img (FK)        │──▶ archives.id
              │ deleted_at             │
              └────────────────────────┘
```

## 2. Tabelas

### 2.1 `diarios`

| Coluna | Tipo | Null | Default | Observação |
|--------|------|:----:|---------|------------|
| `id` | integer | não | `nextval('diarios_id_seq')` | PK |
| `id_certificado` | integer | **não** | — | FK `diario_certificados.id` |
| `id_complemento` | integer | sim | — | FK auto-referente `diarios.id` |
| `data_publicacao` | date | não | — | |
| `lei_validadora` | varchar(191) | sim | — | |
| `autor` | varchar(191) | não | — | |
| `email` | varchar(191) | não | — | |
| `edicao` | integer | não | — | Número da edição |
| `status` | boolean | sim | — | Publicado / desativado |
| `permission_edit` | boolean | sim | `true` | Liberado para edição após publicação? |
| `columns_number` | bigint | não | `1` | 1, 2 ou 3 colunas |
| `has_line` | boolean | não | `false` | Linha divisória entre colunas |
| `status_workflow` | smallint | não | `1` | 1 rascunho, 2 em revisão, 3 publicado |
| `created_by_user_id` | bigint | sim | — | FK `users.id` |
| `updated_by_user_id` | bigint | sim | — | FK `users.id` |
| `published_by_user_id` | bigint | sim | — | FK `users.id` |
| `published_at` | timestamp | sim | — | |
| `line_style` | varchar(191) | sim | — | `solid`, `dashed`, `dotted` |
| `line_color` | varchar(191) | sim | — | Hexadecimal `#RRGGBB` |
| `cover_image_id` | bigint | sim | — | FK `archives.id` ON DELETE SET NULL |
| `generation_status` | varchar(20) | sim | — | `pending`, `processing`, `completed`, `failed` |
| `generation_error` | text | sim | — | Mensagem da última falha |
| `generation_progress` | text | sim | — | JSON de progresso |
| `generation_started_at` | timestamp | sim | — | |
| `generation_completed_at` | timestamp | sim | — | |
| `deleted_at` | timestamp | sim | — | Soft delete |
| `created_at` / `updated_at` | timestamp | sim | — | |

### 2.2 `diario_areas`

| Coluna | Tipo | Null | Default |
|--------|------|:----:|---------|
| `id` | integer | não | seq |
| `name` | varchar(191) | não | — |
| `deleted_at` | timestamp | sim | — |
| `created_at` / `updated_at` | timestamp | sim | — |

Seed inicial (migration `2023_01_17_152132_create_diario_areas_table.php`): 8 áreas —
"ATOS DO DEFENSOR PÚBLICO-GERAL", "ATOS DO CONSELHO SUPERIOR",
"ATOS DO(A) SUBDEFENSOR(A) PÚBLICO(A)-GERAL", "ATOS DA CORREGEDORIA GERAL",
"EDITAIS E AVISOS", "LICITAÇÕES, CONVÊNIOS E CONTRATOS", "COMISSÃO ELEITORAL",
"GERÊNCIA DE PLANEJAMENTO, ORÇAMENTO E FINANÇAS".

### 2.3 `diario_sub_areas`

| Coluna | Tipo | Null | Default |
|--------|------|:----:|---------|
| `id` | integer | não | seq |
| `name` | varchar(191) | não | — |
| `deleted_at` | timestamp | sim | — |
| `created_at` / `updated_at` | timestamp | sim | — |

Seed inicial (migration `2023_01_17_152336_create_diario_sub_areas_table.php`): 11
subáreas — "PORTARIA", "RESENHA", "EXTRATO", "EDITAL", "ATO", "RESOLUÇÃO", "ATA",
"RECOMENDAÇÃO", "AVISO", "RELATÓRIO DE GESTÃO FISCAL", "TERMO DE COMPROMISSO DE
ESTÁGIO". A migration `2023_02_06_152058_add_to_diario_oficial_itens.php` insere
mais uma: "LEI".

> A subárea **não tem efeito visual** no PDF: não abre faixa nem quebra página.

### 2.4 `diario_has_areas` (matérias)

| Coluna | Tipo | Null | Default | Observação |
|--------|------|:----:|---------|------------|
| `id` | integer | não | seq | PK |
| `titulo` | varchar(191) | não | — | |
| `id_diario` | integer | não | — | FK `diarios.id` |
| `id_area` | integer | não | — | FK `diario_areas.id` |
| `id_subarea` | integer | não | — | FK `diario_sub_areas.id` |
| `conteudo` | text | sim | — | HTML do Summernote |
| `columns_number` | bigint | não | `1` | Colunas específicas da matéria |
| `position` | integer | não | `0` | Ordem de exibição (drag & drop) |
| `type` | varchar(50) | não | `'text'` | `text` ou `file` |
| `file_in` | boolean | não | `true` | Anexo entra no fluxo de colunas? |
| `path` | varchar(191) | sim | — | Caminho do PDF anexado |
| `deleted_at` | timestamp | sim | — | |

### 2.5 `diario_certificados`

| Coluna | Tipo | Null | Observação |
|--------|------|:----:|------------|
| `id` | integer | não | PK |
| `name` | varchar(191) | sim | Nome amigável do certificado |
| `path` | varchar(191) | sim | Caminho do `.pfx`/`.p12`/`.crt` |
| `cert` | text | sim | Certificado X.509 em PEM |
| `pkey` | text | sim | Chave privada em PEM |
| `deleted_at` | timestamp | sim | Soft delete |

### 2.6 `diario_configs`

| Coluna | Tipo | Null | Observação |
|--------|------|:----:|------------|
| `id` | integer | não | PK — **registro único, id 1** |
| `lei_padrao` | text | sim | Lei sugerida em novas edições |
| `certificado_padrao` | integer | sim | FK `diario_certificados.id` |
| `ativos` | boolean | sim | Status padrão de novas edições |
| `logo_position` | varchar(20) | sim | `center`, `left`, `right` |
| `header_img` | bigint | sim | FK `archives.id` ON DELETE SET NULL |

## 3. Relacionamentos (Eloquent)

| Model | Relação | Tipo | Colunas |
|-------|---------|------|---------|
| `Diario` | `certificado()` | hasOne | `id_certificado` → `diario_certificados.id` |
| `Diario` | `complemento()` | hasOne (auto) | `id_complemento` → `diarios.id` |
| `Diario` | `diario_areas()` | hasMany | `id_diario` → `diario_has_areas.id_diario` |
| `Diario` | `areas()` | hasManyThrough | via `diario_has_areas`, ordenado por `diario_has_areas.id` |
| `Diario` | `subAreas()` | hasManyThrough | via `diario_has_areas` |
| `Diario` | `cover_image()` | hasOne | `cover_image_id` → `archives.id` |
| `DiarioHasAreas` | `area()` | hasOne | `id_area` → `diario_areas.id` |
| `DiarioHasAreas` | `subarea()` | hasOne | `id_subarea` → `diario_sub_areas.id` |
| `DiarioConfig` | `archive()` | belongsTo | `header_img` → `archives.id` |

Cast declarado em `Diario`: `published_at`, `generation_started_at`,
`generation_completed_at` → `datetime`.

`DiarioHasAreas` tem o mutator `setFileInAttribute()`, que normaliza o valor
para booleano.

## 4. Views SQL (PostgreSQL)

Todas criadas por `CREATE OR REPLACE VIEW`. O banco em execução tem **75 views**;
as do módulo são:

| View | Origem | Consumida por | Colunas relevantes |
|------|--------|---------------|--------------------|
| `diario_oficial` | `diarios` | `GET /cms/datatable/diario_oficial` | `value` (edição), `autor`, `data_publicacao` (DD/MM/AAAA), `status` (Ativado/Desativado), `status_name`, `status_workflow`, `status_workflow_label`, `id`, `permission_edit`, `generation_status`, `download_link` |
| `diario_oficial_pdf` | `diarios` + `diario_configs` + `diario_certificados` | Geração do PDF (dados do cabeçalho) | dados do diário, config e certificado |
| `areas_do_diario` | `diario_has_areas` | `GET /cms/datatable/areas_diario` | `value`, `titulo`, `id` |
| `sub_areas_do_diario` | `diario_sub_areas` | `GET /cms/datatable/sub_areas_diario` | `value`, `titulo`, `id` |

Definição atual de `diario_oficial` (verificada no banco):

```sql
SELECT d.edicao AS value,
       d.autor,
       to_char(d.data_publicacao::timestamp with time zone, 'DD/MM/YYYY'::text) AS data_publicacao,
       CASE WHEN d.status IS TRUE THEN 'Ativado'::text ELSE 'Desativado'::text END AS status,
       CASE WHEN d.status IS TRUE THEN 'true'::text ELSE 'false'::text END AS status_name,
       d.status_workflow,
       CASE WHEN d.status_workflow = 3 THEN 'Publicado'::text
            WHEN d.status_workflow = 2 THEN 'Em Revisão'::text
            ELSE 'Rascunho'::text END AS status_workflow_label,
       d.id,
       d.permission_edit,
       d.generation_status,
       concat('/cms/diario-oficial/', d.id) AS download_link
FROM diarios d
WHERE d.deleted_at IS NULL
ORDER BY d.edicao DESC
```

> A consulta acima reproduz a definição atual da view no banco em execução,
> com as colunas reordenadas para leitura.

> ⚠️ **As views `areas_do_diario_two` e `sub_areas_do_diario_two` NÃO existem
> no v6.** Elas são exclusivas do v5
> (`v5/database/migrations/2025_10_23_233343_create_view_to_areas_do_diario_two.php`
> e `v5/database/migrations/2025_10_23_235015_create_view_to_sub_areas_do_diario_two.php`),
> recriam SQL idêntico e não estão no banco em execução.

## 5. Migrations do módulo

### 5.1 Presentes em `v6/` e `v5/`

| Migration | Efeito |
|-----------|--------|
| `2023_01_16_152754_create_diario_certificados_table.php` | Cria `diario_certificados` |
| `2023_01_17_151845_create_diarios_table.php` | Cria `diarios` |
| `2023_01_17_152132_create_diario_areas_table.php` | Cria `diario_areas` + 8 áreas |
| `2023_01_17_152336_create_diario_sub_areas_table.php` | Cria `diario_sub_areas` + 11 subáreas |
| `2023_01_17_152557_create_diario_has_areas_table.php` | Cria `diario_has_areas` |
| `2023_01_31_091405_alter_edicao_to_diario_oficial.php` | `edicao` → integer (`USING edicao::int`) |
| `2023_02_06_152058_add_to_diario_oficial_itens.php` | Insere subárea "LEI" |
| `2023_04_10_090049_create_view_to_diario_oficial.php` | View `diario_oficial` |
| `2023_04_19_091650_update_view_diario_oficial.php` | Update da view `diario_oficial` |
| `2023_05_22_142218_create_view_to_diario_oficial_pdf.php` | View `diario_oficial_pdf` |
| `2023_05_29_133343_create_view_to_areas_do_diario.php` | View `areas_do_diario` |
| `2023_05_29_135015_create_view_to_sub_areas_do_diario.php` | View `sub_areas_do_diario` |
| `2023_05_31_144619_create_diario_configs_table.php` | Cria `diario_configs` + FK do certificado padrão |
| `2023_09_18_110912_update_view_diario_pdf.php` | Update da view `diario_oficial_pdf` |
| `2024_07_03_221222_update_lei_validadora_in_diarios_table.php` | `lei_validadora` nullable |
| `2024_07_31_025708_add_columns_number_column_to_diario_has_areas_table.php` | `columns_number` na matéria |
| `2024_09_17_090831_add_permission_edit_to_diario_oficial.php` | `permission_edit` |
| `2024_09_17_092831_update_view_diario_oficial_with_permission_edit.php` | Update da view |
| `2024_10_31_022347_add_has_line_column_to_diarios_table.php` | `has_line` + `columns_number` do diário |
| `2024_11_19_100001_add_logo_position_and_header_img_columns_to_diario_configs_table.php` | `logo_position`, `header_img` |
| `2025_01_29_004016_add_order_column_to_diario_has_areas_table.php` | `position` |
| `2026_07_07_100002_add_generation_status_to_diarios_table.php` | `generation_status`, `generation_error`, `generation_started_at`, `generation_completed_at` |
| `2026_07_07_100003_update_view_diario_oficial_with_generation_status.php` | Update da view |
| `2026_07_07_100004_add_generation_progress_to_diarios_table.php` | `generation_progress` |

### 5.2 Exclusivas do `v6/` (re-escritas em 2026-09)

| Migration (v6) | Equivalente no v5 | Efeito |
|-----------------|-------------------|--------|
| `2026_09_03_100000_add_workflow_to_diarios_table.php` | — | `status_workflow` + `created_by_user_id`, `updated_by_user_id`, `published_by_user_id`, `published_at`; migra `status = true` para workflow 3 |
| `2026_09_03_100100_update_view_diario_oficial_with_status_workflow.php` | — | View `diario_oficial` com workflow |
| `2026_09_04_150000_add_file_attachment_to_diario_has_areas_table.php` | `2025_09_19_*` + `2025_09_22_*` | `conteudo` nullable, `type`, `file_in`, `path` |
| `2026_09_10_100000_add_line_customs_to_diarios_table.php` | `v5/database/migrations/2025_09_19_160837_add_central_line_customs_to_diarios_table.php` | `line_style`, `line_color` |
| `2026_09_10_100100_add_cover_image_to_diarios_table.php` | `v5/database/migrations/2025_09_19_164216_add_cover_image_diarios_table.php` | `cover_image_id` (FK `archives`, SET NULL) |
| `2026_09_10_100200_add_generation_status_to_diarios_table.php` | (equivalente a `2026_07_07_100002` + `100004`) | Consolida `generation_*` |
| `2026_09_10_100300_update_view_diario_oficial_with_generation_status.php` | — | View final |

### 5.3 Exclusivas do `v5/` (não existem no v6)

| Migration (v5) | Efeito | Situação no v6 |
|----------------|--------|---------------|
| `2025_09_20_010852_create_diario_adendos_table.php` | Cria `diario_adendos` | **Não existe** no v6: sem tabela, sem model, sem rota |
| `2025_09_22_022119_add_adendo_id_column_to_diario_has_areas_table.php` | Adiciona `adendo_id` | **Não existe** no v6 |
| `2025_09_22_203749_add_path_to_dirario_has_areas_table.php` | Adiciona `path` e **remove** `adendo_id` | **Não existe** no v6 (o `path` vem direto em `2026_09_04_150000`) |
| `2025_10_23_233343_create_view_to_areas_do_diario_two.php` | Recria `areas_do_diario_two` | **Não existe** no v6 |
| `2025_10_23_235015_create_view_to_sub_areas_do_diario_two.php` | Recria `sub_areas_do_diario_two` | **Não existe** no v6 |
| `2023_02_14_094019_add_resource_to_diario_areas_and_subareas.php` | Insere `resources`/`menu_orders` de `diario-area` e `diario-subarea` | Substituída no v6 por `v6/database/seeds/ResourcesTableSeeder.php` e `v6/database/seeds/MenuOrderSeeder.php` |

### 5.4 Migrations do módulo no `v6/`

Total de migrations do projeto em `v6/`: 353; do módulo: **28** (23 comuns +
7 exclusivas de 2026-09). Em `v5/`: 349 migrations, 28 do módulo.

## 6. Seeds

| Seed (v6) | Efeito |
|-----------|--------|
| `v6/database/seeds/AtualizacaoDiario.php` | Trunca `diario_areas` (CASCADE) e reinsere as áreas padrão |
| `v6/database/seeds/AddDiarioOficialWorkflowRolesSeeder.php` | Cria os papéis `redator-diario-oficial` (index, create, edit, show + áreas e subáreas) e `revisor-diario-oficial` (index, publish, show) |
| `v6/database/seeds/AddDiarioOficialPublishPermissionSeeder.php` | Cria a permissão de publicação (`diario-oficial.publish`) |
| `v6/database/seeds/AddDiarioOficialSegregacaoFuncoesConfigSeeder.php` | Cria a chave de configuração `diario_oficial_segregacao_funcoes` |
| `v6/database/seeds/DiarioCaturiteSeeder.php` | Migra PDFs históricos para `contents`/`archives` (não é chamado pelo `DatabaseSeeder`) |
| `v6/database/seeds/UpdateManualAreasDiarioSeeder.php` | Ajusta o manual de áreas do diário |
| `v6/database/seeds/ResourcesTableSeeder.php` | Recursos `diario-oficial`, `diario-area`, `diario-subarea`, `certificado` com `modulo_id = 37` |
| `v6/database/seeds/MenuOrderSeeder.php` | `menu_orders` `certificado` e `diario-oficial` com `modulo_id = 16` |
| `v6/database/seeds/CategoryContentTableSeeder.php` | `CategoryContent` com slug `diario-oficial` |
| `v6/database/seeds/RolesTableSeeder.php` | Papéis e permissões gerais do sistema |

> ⚠️ `RolesTableSeeder` concede `diario-area.*` e `diario-subarea.*`, mas não
> concede `diario-oficial.*`; as permissões do diário vêm do
> `AddDiarioOficialWorkflowRolesSeeder.php`.

## 7. Caminhos de arquivos gerados

| Caminho | Conteúdo |
|---------|----------|
| `storage/app/public/diario-oficial/content/{id}/pdf/diário-{edicao}.pdf` | PDF gerado da edição |
| `storage/app/public/diario-oficial/covers/{archive_id}/` | Imagem de capa |
| `storage/app/public/diario-oficial/headers/{archive_id}/` | Imagem de cabeçalho das configurações |
| `storage/app/diario-oficial/pdf-text/` | Cache JSON do texto extraído do PDF (busca no portal) |
| `public/img/qrcode.png` | QR Code do diário (nome fixo) |
| `storage/app/public/diario-oficial/adendos/` | Anexos PDF das matérias |
