# Transparência Fiscal — Modelagem de Dados

> Schema inspecionado no PostgreSQL 10 (banco `prefeituras_v5`) via `\d` e `pg_get_viewdef`.
> Comandos de leitura apenas.

---

## 1. Tabelas do módulo

O módulo **não possui tabela própria**. Ele se apoia em tabelas genéricas de conteúdo do
CMS. Não existe nenhuma tabela de `despesas`, `credores`, `empenhos` ou `orcamento`
nesse banco — a transparência fiscal é um catálogo de links.

### 1.1 `contents` — item de transparência

| Coluna | Tipo | Null | Default | Papel no módulo |
|---|---|---|---|---|
| `id` | integer | no | `contents_id_seq` | PK; `id` exposto pela view |
| `title` | varchar(191) | sim | — | Título do item (ex.: "Contas de Gestão") |
| `text` | text | sim | — | Não usado pelo portal da transparência |
| `clicks` | integer | sim | — | Contador genérico |
| `highlights` | boolean | sim | — | Não usado |
| `galery` | boolean | sim | — | Não usado |
| `status` | boolean | sim | — | **Ativo/Desativado**; só ativos aparecem no portal |
| `source`, `author` | varchar(191) | sim | — | Não usados |
| `icon` | varchar(191) | sim | — | Classe CSS do ícone (ex.: `fas fa-users`) |
| `slug` | varchar(191) | sim | — | Usado na rota `transparencia.show` |
| `publication_date` | timestamp(0) | sim | — | Não usado |
| `order` | integer | sim | — | **Posição**; base da ordenação da view SQL |
| `category_contents_id` | integer | **no** | — | FK → `category_contents(id)`; raiz = 25 |
| `created_at`, `updated_at` | timestamp(0) | sim | — | — |
| `deleted_at` | timestamp(0) | sim | — | Soft delete; a view filtra `deleted_at IS NULL` |
| `secretaria` | varchar(191) | sim | — | Não usado |
| `carousel` | boolean | sim | — | Não usado |
| `orgaos_id` | integer | sim | — | FK → `contents(id)` (auto) |
| `position_id` | bigint | sim | — | Referência a `positions` (sem constraint de FK no banco) |
| `acronym` | varchar(191) | sim | — | Não usado |
| `layout` | integer | sim | — | Não usado |
| `councilmen_id` | integer | sim | — | FK → `councilmen` |
| `linha_fina` | varchar(300) | sim | — | Não usado na transparência |
| `referencia` | varchar(50) | sim | — | **Referência legal** exibida em badge (ex.: "2.1 a 2.5") |

FKs: `contents_category_contents_id_foreign`, `contents_councilmen_id_foreign` (ON DELETE SET NULL),
`contents_orgaos_id_foreign` (auto-referência).

### 1.2 `category_contents` — categorias (pastas)

| Coluna | Tipo | Null | Default | Papel no módulo |
|---|---|---|---|---|
| `id` | integer | no | `category_contents_id_seq` | PK. **A raiz da transparência tem `id = 25`** (`slug = 'transparencia-fiscal'`, `name = 'Transparência Fiscal'`, `category_contents_id = NULL`) |
| `name` | varchar(191) | no | — | Nome da categoria (exibido como título de seção no portal) |
| `slug` | varchar(191) | no | — | `transparencia-fiscal` na raiz |
| `status_create` | boolean | sim | — | — |
| `quantidade_itens` | integer | sim | — | Não usado |
| `order` | integer | sim | — | Ordem das seções no portal |
| `layout` | integer | sim | — | Não usado |
| `created_at`, `updated_at`, `deleted_at` | timestamp(0) | sim | — | — |
| `category_contents_id` | integer | sim | — | **Auto-referência**: id do pai (NULL = raiz) |
| `status` | boolean | sim | `true` | Não usado na listagem do portal |
| `display_type` | varchar(191) | sim | — | `grouped`/`direct` — usado pela **home** (`PortalController`), não pela página de transparência |
| `custom_icon` | varchar(191) | sim | — | Ícone da categoria |
| `description` | text | sim | — | Texto descritivo exibido abaixo do título da seção |

### 1.3 `links` — URL do item (1:1 com `contents`)

| Coluna | Tipo | Null | Default | Papel |
|---|---|---|---|---|
| `id` | integer | no | `links_id_seq` | PK |
| `title` | varchar(191) | sim | — | Copiado do `title` do conteúdo |
| `link` | varchar(3000) | sim | — | **URL exibida no `iframe` ou aberta em nova aba** |
| `alt` | varchar(191) | sim | — | Texto alternativo (v6 recebe o próprio título) |
| `name` | varchar(191) | sim | — | Identificador do link |
| `isBlank` | boolean | sim | — | **`true` ⇒ abre em nova aba (`target="_blank"`)** |
| `status_create` | boolean | sim | — | — |
| `created_at`, `updated_at`, `deleted_at` | timestamp(0) | sim | — | — |
| `contents_id` | integer | sim | — | FK → `contents(id)` |
| `static_pages_id` | integer | sim | — | FK → `static_pages(id)` (não usado aqui) |

### 1.4 `informations_requests` — pedidos de informação (e-SIC/LAI)

| Coluna | Tipo | Null | Papel |
|---|---|---|---|
| `id` | integer | no | PK; parametro do Ajax `getAnswers` |
| `request` | text | no | Texto original da solicitação (mascarado na exibição) |
| `mask_request` | text | sim | Versão mascarada (LGPD) exibida no modal |
| `ip` | varchar(191) | no | IP do solicitante |
| `browser` | varchar(191) | no | User agent |
| `protocol` | varchar(191) | no | Protocolo; exibido mascarado (`masked_protocol`) |
| `persons_id` | integer | sim | FK → `persons` |
| `status_id` | integer | sim | FK → `statuses` |
| `created_at`, `updated_at`, `deleted_at` | timestamp(0) | sim | `created_at` é a data do pedido (base do filtro por ano) |

### 1.5 `answers` e `answer_archives`

| Tabela | Colunas | Papel |
|---|---|---|
| `answers` | `id`, `answer` (text, NOT NULL), `archives_id` (FK `archives`), `informations_request_id` (FK `informations_requests`, NOT NULL), timestamps + `deleted_at` | Respostas do e-SIC. Uma solicitação pode ter várias respostas |
| `answer_archives` | `id`, `answer_id` (FK `answers`), `archive_id` (FK `archives`) | Pivot N:N entre resposta e arquivo |

### 1.6 `statuses` — status do e-SIC

| id | name | Uso no relatório |
|---|---|---|
| 1 | Em Tramitação | `emTramitacao_*` (prazo medido por `created_at`) |
| 2 | Atendido | `atendido_*` (prazo medido por `answers.created_at - created_at`) |
| 3 | Indeferido | `indeferido_*` (idem) |
| 4 | Prorrogado | `prorrogado_*` (prazo medido por `updated_at`) |
| 5 | Aberta | Não usado no relatório |
| 6 | Finalizada | Não usado no relatório |
| 7 | Cancelada | Não usado no relatório |

### 1.7 `configurations` — chaves usadas

| id | key | value (no banco inspecionado) | Uso |
|---|---|---|---|
| 33 | `full_list_requests_disabled` | `0` | Se `1`, esconde a listagem completa de solicitações no relatório |
| 50 | `layout` | `modelo1` | Define o template do portal (`getTemplateActived()`) |
| 85 | `portal_transparencia_ativo` | `0` | Ativa a busca no portal externo de transparência |
| 86 | `portal_transparencia_codigo_ente` | *(vazio)* | Código do município no portal Elmar |
| 87 | `portal_transparencia_url_portal` | `https://transparencia.elmartecnologia.com.br` | URL do portal externo |

### 1.8 `layouts` — chaves usadas

| id | key | section | columns | style | path |
|---|---|---|---|---|---|
| 21 | `transparencia` | content | — | — | `portal.layouts.modelo5.home.transparencia` |
| 22 | `image-transparencia-fiscal` | transparencia | — | — | idem |
| 23 | `transparencia-fiscal` | transparencia | **4** | `width: calc(100% - 312px); overflow-x: hidden;` | idem |
| 34 | `transparencia-fiscal-grid` | transparencia | **6** | `width: 100%; overflow-x: hidden;` | idem |

> `transparencia-fiscal-grid` está com `status = false` no banco inspecionado.
> Editadas em `Prefecture\LayoutController` (chaves `input-columnsTransparenciaFiscal` e
> `input-columnsTransparenciaFiscalGrid`).

---

## 2. View SQL `transparencia_fiscal`

A view é lida **apenas pelo CMS** (`DB::table("transparencia_fiscal")` em
`v6/app/Http/Controllers/Prefecture/DatatableController.php:264-268`).

### 2.1 Definição vigente no banco

Confirmada com `pg_get_viewdef('public.transparencia_fiscal'::regclass, true)`:

```sql
SELECT row_number() OVER (ORDER BY c."order") AS value,
    c.title AS titulo,
    c.id,
        CASE
            WHEN c.status IS TRUE THEN 'Ativado'::text
            ELSE 'Desativado'::text
        END AS status_name,
        CASE
            WHEN cc.category_contents_id IS NOT NULL THEN cc.name
            ELSE cc2.name
        END AS category_name,
    c.status
   FROM contents c
     JOIN category_contents cc ON cc.id = c.category_contents_id
     JOIN category_contents cc2 ON 'transparencia-fiscal'::text = cc2.slug::text
  WHERE c.deleted_at IS NULL AND cc.category_contents_id = cc2.id OR c.deleted_at IS NULL AND c.category_contents_id = cc2.id
  ORDER BY c."order";
```

Colunas expostas:

| Coluna | Tipo | Origem |
|---|---|---|
| `value` | bigint | `row_number()` pela ordem de `contents.order` |
| `titulo` | varchar(191) | `contents.title` |
| `id` | integer | `contents.id` |
| `status_name` | text | `Ativado` / `Desativado` |
| `category_name` | varchar(191) | nome da subcategoria (`cc.name`) ou o nome da raiz (`cc2.name`) |
| `status` | boolean | `contents.status` |

### 2.2 Evolução da view (4 migrations)

| Migration | O que mudou |
|---|---|
| `v6/database/migrations/2023_04_11_094706_create_view_to_transparencia_fiscal.php` | Versão inicial: `row_number()` por `(c.status IS TRUE) DESC, c.created_at DESC`; colunas `value, titulo, id, status_name, status`; filtra `cc.slug = 'transparencia-fiscal'` |
| `v6/database/migrations/2023_04_18_132447_update_transparencia_fiscal_view_to_order.php` | Troca a ordenação para `c.order ASC` |
| `v6/database/migrations/2023_04_28_112431_update_view_transparencia_fiscal.php` | `DROP VIEW` + recria; **inclui `category_name`** e passa a listar itens de subcategorias (`cc.category_contents_id = cc2.id OR c.category_contents_id = cc2.id`) |
| `v6/database/migrations/2023_05_04_160917_add_order_by_view_transparencia_fiscal.php` | Versão final (idêntica à vigente): recria a view com a mesma estrutura, corrigindo aIdx/predicados e o `ORDER BY c."order"` |

> As quatro migrations existem **também na v5** (`v5/database/migrations/`), com o mesmo
> conteúdo e as mesmas datas.

### 2.3 Fragilidade do predicado

A cláusula `WHERE` final é:

```sql
WHERE c.deleted_at IS NULL AND cc.category_contents_id = cc2.id
   OR c.deleted_at IS NULL AND c.category_contents_id = cc2.id
```

Por precedência de `AND` sobre `OR`, o predicado equivale a
`(c.deleted_at IS NULL AND cc.category_contents_id = cc2.id) OR (c.deleted_at IS NULL AND c.category_contents_id = cc2.id)`
— o efeito pretendido acontece, mas a redundância de `deleted_at IS NULL` e a falta de
parênteses são armadilhas para qualquer edição futura.

---

## 3. Relacionamentos

```
category_contents (raiz, id 25, slug 'transparencia-fiscal')
   │  self FK category_contents_id
   ├──< category_contents (subcategorias)
   │        └──< contents (itens)
   └──< contents (itens na raiz)

contents ──1:1──> links            (Content::linksUnique = hasOne(Link, 'contents_id', 'id'))
contents ──*──> archives
contents ──*──> menu_items

informations_requests ──1:1──> persons
informations_requests ──1:1──> statuses
informations_requests ──*──> answers ──*──> archives   (pivot answer_archives)
```

Relations Eloquent relevantes (`v6/app/Http/Model/Prefecture/`):

| Model | Relation | Código |
|---|---|---|
| `Content` | `categoryContents()` | `belongsTo(CategoryContent::class, 'category_contents_id', 'id')` |
| `Content` | `linksUnique()` | `hasOne(Link::class, 'contents_id', 'id')` |
| `Content` | `archives()` | `hasMany(Archive::class, 'contents_id', 'id')` |
| `InformationsRequest` | `status()` | `hasOne(Status::class, 'id', 'status_id')` |
| `InformationsRequest` | `person()` | `hasOne(Person::class, 'id', 'persons_id')` |
| `InformationsRequest` | `answers()` | `hasMany(Answer::class, 'id', 'answers_id')` — **chave estrangeira errada** (ver `08-GAPS-E-DIVIDAS.md`) |
| `Answer` | `archives()` | `belongsToMany(Archive::class, 'answer_archives')` |
| `Answer` | `informationsRequests()` | `belongsTo(InformationsRequest::class, 'informations_request_id', 'id')` |

---

## 4. Migrations relevantes

| Data / arquivo | Efeito |
|---|---|
| `v6/database/migrations/2023_04_11_094706_create_view_to_transparencia_fiscal.php` | Cria a view `transparencia_fiscal` |
| `v6/database/migrations/2023_04_18_132447_update_transparencia_fiscal_view_to_order.php` | Ordena por `contents.order` |
| `v6/database/migrations/2023_04_28_112431_update_view_transparencia_fiscal.php` | Adiciona `category_name` e inclui subcategorias |
| `v6/database/migrations/2023_05_04_160917_add_order_by_view_transparencia_fiscal.php` | Versão final da view |
| `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | Cria a tabela `modulos` (onde vive o slug `transparencia-fiscal`) |
| `v6/database/migrations/2023_02_01_083626_add_itens_to_archives.php` | Insere o manual do usuário "Manual de Transparência Fiscal" (linha ~587) e as figuras do manual |

> As tabelas `contents`, `category_contents` e `links` são criadas por migrations
> anteriores à trilha do módulo e são compartilhadas por dezenas de funcionalidades.

---

## 5. Seeds

| Arquivo | Efeito |
|---|---|
| `v6/database/seeds/AddPortalTransparenciaConfigurationsSeeder.php` | Insere/atualiza as 3 chaves `portal_transparencia_*` (`category_configurations_id = 1`, `status_create = 1`, `order` 80-82). É idempotente: só insere se a chave não existir; senão atualiza preservando o `value` já cadastrado |
| `v6/database/seeds/AddTerceirizadosInTransparenciaFiscalSeeder.php` | **Só roda se `configurations.key = 'estado'` for `PB`.** Cria a `static_pages` `terceirizados` e um `contents` (slug `terceirizados`, ícone `fas fa-users`) dentro da categoria `transparencia-fiscal`, com `links` apontando para `https://www.{cidade}.pb.gov.br/terceirizados` e `isBlank = true`. Não insere se já existir `contents` com o mesmo slug na categoria |

> `v5/database/seeds/AddTerceirizadosInTransparenciaFiscalSeeder.php` existe também na v5
> (mesma semântica); o seeder `AddPortalTransparenciaConfigurationsSeeder` é **novo na v6**.
