# Legislação — Modelagem de Dados

> Banco: PostgreSQL 10 + PostGIS. Estado inspecionado na base `prefeituras_v5`
> (migrations da v6 aplicadas). Nenhuma migration foi executada durante esta
> análise — apenas `SELECT` e leitura das migrations.

---

## 1. Tabela `category_contents` — árvore de categorias

É a estrutura central do módulo: uma **auto-referência** que guarda a
hierarquia de categorias de conteúdo de todo o portal.

| Coluna | Tipo PostgreSQL | Null | Default | Papel no módulo |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('category_contents_id_seq')` | PK. Raiz de Legislation = **2**; filhos 22, 23, 24. |
| `name` | `varchar(191)` | não | — | Rótulo exibido na lateral do portal e na grade do CMS. |
| `slug` | `varchar(191)` | não | — | Identificador usado nas URLs (`/legislacao/{slug}`). |
| `status_create` | `boolean` | sim | — | Indica categoria raiz de um módulo (`0` na raiz de legislação). |
| `quantidade_itens` | `integer` | sim | — | Não usado pelo módulo de legislação. |
| `order` | `integer` | sim | — | Ordem de exibição (reordenável por arrastar-e-soltar no CMS). |
| `layout` | `integer` | sim | — | Não usado pelo módulo. |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | — | `SoftDeletes` aplicado no model. |
| `category_contents_id` | `integer` | sim | — | **FK auto-referenciada** → `category_contents.id`. `NULL` = categoria raiz. |
| `status` | `boolean` | sim | `true` | Habilita/desabilita a categoria. |
| `display_type` | `varchar(191)` | sim | — | Não usado pelo módulo. |
| `custom_icon` | `varchar(191)` | sim | — | Não usado pelo módulo. |
| `description` | `text` | sim | — | Não usado pelo módulo. |

Indexes/FKs (verificado no banco):

```
"category_contents_pkey" PRIMARY KEY, btree (id)
"category_contents_category_contents_id_foreign" FOREIGN KEY (category_contents_id) REFERENCES category_contents(id)
```

### Categorias de legislação no banco atual

| id | name | slug | `category_contents_id` | status |
|---|---|---|---|---|
| 2 | Legislação | `legislacao` | `NULL` | `true` |
| 22 | Constituição Federal | `constituicao-federal` | 2 | `true` |
| 23 | Código Tributario | `codigo-tributario` | 2 | `true` |
| 24 | Decretos | `decretos` | 2 | `true` |

## 2. Tabela `contents` — as normas

| Coluna | Tipo PostgreSQL | Null | Papel no módulo |
|---|---|---|---|
| `id` | `integer` | não | PK; referenciado por `archives.contents_id`. |
| `title` | `varchar(191)` | sim | Título da norma (`required|max:120` no formulário). |
| `text` | `text` | sim | Corpo HTML (editado com Summernote). |
| `clicks` | `integer` | sim | Contador de acessos, incrementado em `contentView`. |
| `highlights` | `boolean` | sim | Destaque (uso do CMS; não consumido pelo portal de legislação). |
| `galery` | `boolean` | sim | Quando `1`, `contentView` monta `$galery`. |
| `status` | `boolean` | sim | Publicação do conteúdo. |
| `source` / `author` / `icon` | `varchar(191)` | sim | Não usados pelo módulo. |
| `slug` | `varchar(191)` | sim | URL da norma: `/legislacao/{categoria}/{slug}`. |
| `publication_date` | `timestamp(0)` | sim | **Eixo de ordenação** e de filtragem; compare com `now()`. |
| `order` | `integer` | sim | Não usado na ordenação do módulo (a ordenação é `publication_date DESC, id DESC`). |
| `category_contents_id` | `integer` | **não** | **FK** → `category_contents.id`; aponta para a subcategoria da norma. |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | `SoftDeletes`. |
| `secretaria` | `varchar(191)` | sim | Vínculo opcional (100% `NULL` no banco atual). |
| `carousel` | `boolean` | sim | Não usado pelo módulo. |
| `orgaos_id` | `integer` | sim | FK → `contents.id` (auto-referência). |
| `position_id` | `bigint` | sim | FK lógica para cargo. |
| `acronym` / `layout` | `integer`/`varchar` | sim | Não usados. |
| `councilmen_id` | `integer` | sim | FK → `councilmen.id`. |
| `linha_fina` | `varchar(300)` | sim | Não usado. |
| `referencia` | `varchar(50)` | sim | Não usado. |

Indexes/FKs relevantes:

```
"contents_pkey" PRIMARY KEY, btree (id)
"contents_category_contents_id_foreign" FOREIGN KEY (category_contents_id) REFERENCES category_contents(id)
```

> **Risco de dados:** `contents.category_contents_id` é `NOT NULL` e tem FK
> para `category_contents.id`. Excluir (soft delete) uma categoria deixa as
> normas órfãs do ponto de vista do portal, porque as consultas do módulo
> partem sempre do id da categoria.

### Volume atual

```
contents: 184 linhas totais, 184 com deleted_at IS NULL
contents com category_contents_id IN (2, 22, 23, 24): 0 linhas
```

Ou seja: **não há nenhuma norma publicada** no banco inspecionado. A
distribuição real é: `glossario` 115, `transparencia-fiscal` 23, `secretarias`
13, `perguntas-frequentes` 8, `a-instituicao` 5, `a-cidade` 4, `banners` 4,
`apolo-ao-usuario` 3, `orgaos` 3, `super-banners` 2, `acesso-rapido` 3,
`comite-de-investimentos` 1.

## 3. Tabela `archives` — anexos

Usada por `contentView` para listar os arquivos da norma e pelo
`ArchiveResource` da API.

| Coluna | Papel no módulo |
|---|---|
| `contents_id` | FK → `contents.id` (norma dona do arquivo). |
| `view_pdf` | Marca o arquivo exibido inline como PDF (`where('view_pdf', true)->first()`). |
| `highlight` | `null` = anexo comum; `1` = imagem de destaque. |
| `title`, `description`, `type`, `hash`, `path`, `name`, `extension` | Metadados do arquivo. |

## 4. Tabela `modulos` — registro do módulo

```
id | name    | slug        | status
19 | Legislação | legislacao | true
```

`resources.modulo_id` e `menu_orders.modulo_id` referenciam esta tabela. Como
`modulos.status = true`, a entrada "Legislação" aparece no menu lateral do CMS
(`v6/app/Providers/MenuServiceProvider.php:237-239`).

## 5. Views SQL

### `legislacao` — **órfã**

```sql
CREATE OR REPLACE VIEW legislacao
as select
    row_number() OVER (ORDER BY c.publication_date DESC) as value,
    c.title as titulo,
    to_char(c.publication_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text) AS data_publicacao,
    cc.name as categoria,
    cc.slug as categoria_slug,
    c.slug as slug,
    c.id as id
from contents c
    JOIN category_contents cc ON cc.id = c.category_contents_id
where
    c.deleted_at is null
and
    cc.category_contents_id = 2
ORDER BY c.publication_date DESC;
```

| Coluna | Tipo | Origem |
|---|---|---|
| `value` | `bigint` | `row_number()` sobre `publication_date DESC` (numeração da linha). |
| `titulo` | `varchar(191)` | `contents.title`. |
| `data_publicacao` | `text` | `to_char(publication_date, 'DD/MM/YYYY às HH24:MI')`. |
| `categoria` | `varchar(191)` | `category_contents.name`. |
| `categoria_slug` | `varchar(191)` | `category_contents.slug` (**só na v6**). |
| `slug` | `varchar(191)` | `contents.slug` (**só na v6**). |
| `id` | `integer` | `contents.id`. |

Restrições do filtro: `c.deleted_at IS NULL` **e** `cc.category_contents_id = 2`
— ou seja, **apenas as subcategorias diretas** de Legislação. Uma norma em uma
sub-subcategoria (nível 3) **não aparece** na view.

> **Fato verificado:** nenhum PHP da v6 consulta a view `legislacao`
> (`grep` em `v6/app/` e `v6/resources/` não retorna nenhum resultado). A
> grade do CMS reconstrói as mesmas colunas em PHP, em
> `v6/app/Http/Controllers/Prefecture/DatatableController.php:101-120`.
> Consequência: a view e o PHP **divergem** — a view filtra só o 1º nível, o
> PHP usa `getAllDescendants()` (todos os níveis).

### Histórico da view (migrations)

| Migration | Versão | Mudança |
|---|---|---|
| `v6/database/migrations/2023_03_15_144853_create_view_to_legislation.php` | v5 e v6 | Cria a view; `row_number()` por `c.created_at asc`; data em `DD/MM/YYYY`. |
| `v6/database/migrations/2023_03_31_134832_update_legislation_view_to_publication-date.php` | v5 e v6 | Passa a ordenar por `c.publication_date DESC` (window e `ORDER BY`). |
| `v6/database/migrations/2023_05_29_130250_legislacao_formato_24_horas.php` | v5 e v6 | Data passa a `DD/MM/YYYY às HH24:MI`; `down()` volta ao formato sem hora. |
| `v6/database/migrations/2024_11_29_084625_update_view_legislacao.php` | **só v6** | `DROP VIEW` + recria com `categoria_slug` e `slug`. |

> **Correção importante:** a migration
> `v5/database/migrations/2026_03_09_000000_update_views_to_include_slug.php`
> **não** toca na view `legislacao` — ela recria `a_cidade`, `a_prefeitura`,
> `orgaos` e `secretarias` (verificado por `grep`: nenhuma ocorrência de
> "legislacao" no arquivo). Portanto, **na v5 a view `legislacao` não tem
> `categoria_slug` nem `slug`**.

## 6. Relações Eloquent

### `App\Prefecture\CategoryContent` (`v6/app/Http/Model/Prefecture/CategoryContent.php`)

| Relação | Declaração | Semântica real |
|---|---|---|
| `categoryContents()` | `hasMany(CategoryContent::class, 'id', 'category_contents_id')` | **Chaves trocadas.** Gera `child.id = parent.category_contents_id`, ou seja, retorna os *filhos da categoria apontada pelo pai*. Em uso com `whereHas(..., slug='legislacao')` devolve as subcategorias de legislação. |
| `rightWayToGetCategoryContents()` | `hasMany(CategoryContent::class, 'category_contents_id', 'id')` | Relação correta: `child.category_contents_id = parent.id`. É a base da navegação recursiva. |
| `categoryContentsBelongings()` | `belongsTo(CategoryContent::class, 'category_contents_id', 'id')` | Pai da categoria (chaves também trocadas em relação ao `belongsTo` usual). |
| `contents()` | `hasMany(Content::class, 'category_contents_id', 'id')` + scopes | Não usado pelo módulo de legislação. |
| `categories()` | `hasMany(CategoryContent::class, 'category_contents_id', 'id')` | Irmãos. |
| `getAllDescendants($withParents = false)` | método recursivo (linhas 54-74) | Devolve **todos os descendentes** (filhos + netos + ...) como `Collection` de **arrays** com apenas `id, name, order, slug, category_contents_id`. |
| `scopeAllDescendants()` | `with(['rightWayToGetCategoryContents' => fn($q) => $q->allDescendants()])` | Eager loading recursivo da árvore (~3 níveis). |

> **Armadilha `getAllDescendants()`:** (a) faz N+1 — uma query por nível;
> (b) devolve **arrays**, não modelos — por isso o código usa
> `data_get($getAllDescendants->where('slug', $x)->first(), 'id')`;
> (c) **não inclui a própria raiz** na coleção;
> (d) com `$withParents = true` prefixa o nome do pai, exceto quando o pai é
> `publicacoes` ou `legislacao` (linhas 64-66).

### `App\Prefecture\Content` (`v6/app/Http/Model/Prefecture/Content.php`)

| Relação | Declaração | Uso em legislação |
|---|---|---|
| `categoryContents()` | `belongsTo(CategoryContent::class, 'category_contents_id', 'id')` | Categoria da norma; usada em `contentView` para montar o título (`$content->categoryContents->name`). |
| `archives()` | `hasMany(Archive::class, 'contents_id', 'id')` | Anexos; usada no `ContentController` e na API. |
| `secretarias()` | `hasMany(Content::class, 'secretaria', 'slug')` | Liga a notícia ao **slug** da secretaria. Não usado em legislação. |
| `orgaos()` | `hasMany(Content::class, 'orgaos_id', 'id')` | Não usado em legislação. |

Casts e datasource relevantes:

```php
protected $table = 'contents';
protected $dates = ['publication_date'];
protected $casts = ['publication_date' => 'datetime'];
protected $primaryKey = 'id';
```

## 7. Seeds e dados iniciais

| Arquivo | Versão | Função |
|---|---|---|
| `v6/database/seeds/UploadPdfLegislation.php` | v5 e v6 | Semeia PDFs de legislação (carga inicial de normas). |
| `v6/database/seeds/UpdateLegislacaoResourceNameSeeder.php` | v6 | Ajusta o nome do recurso/arquivo de legislação já publicado. |

Categorias raiz e subcategorias **não** são criadas por seed: são registros
manuais do `category_contents` (ids 2, 22, 23, 24 no banco atual). A
referência estática à raiz é feita pelo **slug** `legislacao`, não pelo id —
com exceção da **view SQL**, que fixa `cc.category_contents_id = 2` (id
hardcoded).

## 8. Diagrama de entidades

```
category_contents (2 = 'legislacao', category_contents_id = NULL)
   │
   │  category_contents_id (auto-FK)
   ├──▶ category_contents (22 'Constituicao Federal')
   │        └──▶ (níveis abaixo, livres)
   ├──▶ category_contents (23 'Codigo Tributario')
   └──▶ category_contents (24 'Decretos')
                │
                │ contents.category_contents_id (FK, NOT NULL)
                ▼
             contents  ──────────────┐
                │                    │ contents_id
                ▼                    ▼
          contents.orgaos_id      archives (view_pdf, highlight)
          (auto-FK)

      (órfã)  view legislacao  ──►  contents + category_contents
```
