# Modelagem

## 1. Visão geral

Duas tabelas, ambas com chave primária `string(36)` (UUID v4 gerado por
`v2_uuid()`), `timestamps`, `softDeletes` e `jsonb metadata`. A partir de
2026-10-22 ambas receberam colunas de rastreio de importação V1 (`v1_id`,
`imported_at`) com índice único parcial.

```
v2_transparency_categories
  id (PK, char36)
  ├─ parent_id ──────────────┐ (auto-relacionamento, SEM FK)
  └─ 1:N ──> v2_transparency_items.v2_transparency_category_id (FK, SET NULL)
```

## 2. `v2_transparency_categories`

Criada em `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_07_17_000001_create_v2_transparency_categories_table.php:17-30`.

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `string(36)` | não | — | PK |
| `name` | `string(255)` | não | — | rótulo exibido |
| `slug` | `string(255)` | não | — | **UNIQUE** + índice |
| `description` | `text` | sim | — | exibida no card do portal |
| `parent_id` | `string(36)` | sim | — | raiz = `NULL`; **sem FK** |
| `is_active` | `boolean` | não | `true` | escopo `active()` |
| `order` | `integer` | não | `0` | 1º critério de ordenação |
| `metadata` | `jsonb` | sim | — | sem uso em views |
| `created_at`/`updated_at` | `timestamp` | sim | — | — |
| `deleted_at` | `timestamp` | sim | — | soft delete |
| `v1_id` | `integer` | sim | — | importação V1 |
| `imported_at` | `timestamp` | sim | — | importação V1 |

Índices: `slug` (unique), `slug` (index comum redundante, `:28`), `parent_id`
(`:29`), `v2_transparency_categories_v1_id_unique` parcial
(`WHERE v1_id IS NOT NULL`).

Código real da criação:

```php
// 2026_07_17_000001_create_v2_transparency_categories_table.php:17-30
Schema::create('v2_transparency_categories', function (Blueprint $t) {
    $t->string('id', 36)->primary();
    $t->string('name', 255);
    $t->string('slug', 255)->unique();
    $t->text('description')->nullable();
    $t->string('parent_id', 36)->nullable();
    $t->boolean('is_active')->default(true);
    $t->integer('order')->default(0);
    $t->jsonb('metadata')->nullable();
    $t->timestamps();
    $t->softDeletes();
    $t->index('slug');
    $t->index('parent_id');
});
```

### Relacionamentos (model)

`V2/Modules/DadosAbertos/TransparenciaFiscal/Models/V2TransparencyCategory.php`:

| Método | Tipo | Linhas |
|---|---|---|
| `parent()` | `belongsTo(self::class, 'parent_id')` | `:82-85` |
| `children()` | `hasMany(self::class, 'parent_id')` | `:89-92` |
| `items()` | `hasMany(V2TransparencyItem::class, 'v2_transparency_category_id')` | `:96-99` |

Escopos: `active()` (`:104-107`), `ordered()` = `order ASC, name ASC`
(`:112-115`), `root()` = `whereNull('parent_id')` (`:120-123`).

### Ciclo de vida do slug

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Models/V2TransparencyCategory.php:63-77
static::creating(function ($m) {
    if (empty($m->id)) {
        $m->id = \v2_uuid();
    }
    if (empty($m->slug) && !empty($m->name)) {
        $m->slug = Str::slug($m->name);
    }
});
static::updating(
    function ($m) {
    if ($m->isDirty('name') && !$m->isDirty('slug')) {
        $m->slug = Str::slug($m->name);
    }
}
);
```

Risco: `slug` é `unique` **global** (não por `parent_id`). Duas categorias com
o mesmo nome em ramos diferentes (ex.: "Outras" em Receitas e em Despesas)
colidem no `Str::slug(name)` e a criação falha com violação de unicidade. O
seeder evita isso usando slugs distintos (`exploracao-de-jogos`,
`outras-receitas`).

## 3. `v2_transparency_items`

Criada em `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_07_17_000002_create_v2_transparency_items_table.php:17-30`;
`slug` adicionado em `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_08_31_000001_add_slug_to_v2_transparency_items_table.php:15-31`.

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `string(36)` | não | — | PK |
| `title` | `string(255)` | não | — | — |
| `slug` | `string(255)` | sim | — | **sem unique**; índice comum |
| `description` | `text` | sim | — | texto livre (sem sanitização/HTML rico) |
| `value` | `decimal(15,2)` | sim | — | cast `float` no model |
| `reference_date` | `date` | sim | — | competência; índice |
| `is_active` | `boolean` | não | `true` | escopo `active()` |
| `v2_transparency_category_id` | `string(36)` | sim | — | FK → categories |
| `metadata` | `jsonb` | sim | — | sem uso em views |
| `created_at`/`updated_at` | `timestamp` | sim | — | — |
| `deleted_at` | `timestamp` | sim | — | soft delete |
| `v1_id` | `integer` | sim | — | importação V1 |
| `imported_at` | `timestamp` | sim | — | importação V1 |

FK real:

```php
// 2026_07_17_000002_create_v2_transparency_items_table.php:28-29
$t->index('reference_date');
$t->foreign('v2_transparency_category_id')->references('id')->on('v2_transparency_categories')->onDelete('set null');
```

### Relacionamentos e escopos

| Item | Definição | Linhas |
|---|---|---|
| `category()` | `belongsTo(V2TransparencyCategory::class, 'v2_transparency_category_id')` | `V2/Modules/DadosAbertos/TransparenciaFiscal/Models/V2TransparencyItem.php:77-80` |
| `active()` | `where('is_active', true)` | `:85-88` |
| `ordered()` | `orderBy('reference_date', 'desc')` | `:93-96` |
| `search($t)` | `where('title', 'ILIKE', "%{$t}%")`; vazio ⇒ sem filtro | `:102-108` |

### Slug do item

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Models/V2TransparencyItem.php:63-72
static::creating(
    function ($m) {
    if (empty($m->id)) {
        $m->id = \v2_uuid();
    }
    if (empty($m->slug) && !empty($m->title)) {
        $m->slug = Str::slug($m->title) . '-' . substr($m->id, 0, 8);
    }
}
);
```

Só há hook de **criação**. Consequências:

1. renomear o título não atualiza a URL pública;
2. o sufixo de 8 caracteres do UUID é o que garante unicidade prática, já que a
   coluna **não** tem constraint `UNIQUE`;
3. o backfill da migration usa a mesma regra (`Str::slug(title) . '-' .
   substr(id, 0, 8)`, `2026_08_31_...:30`), mas processa em `orderBy('title')`,
   o que pode gerar slugs determinísticos para títulos iguais — resolvidos pelo
   sufixo de id.

### Casts

```php
// V2/Modules/DadosAbertos/TransparenciaFiscal/Models/V2TransparencyItem.php:59
protected $casts = ['value' => 'float','reference_date' => 'date','is_active' => 'boolean','metadata' => 'array'];
```

`value` como `float` perde exatidão em somatórios e formatação de centavos.
`reference_date` vira `Carbon` (usado em `format('d/m/Y')` nas views).

## 4. Migrations

| Arquivo | Data | Efeito |
|---|---|---|
| `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_07_17_000001_create_v2_transparency_categories_table.php` | 2026-07-17 | cria categorias |
| `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_07_17_000002_create_v2_transparency_items_table.php` | 2026-07-17 | cria itens + FK |
| `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_08_31_000001_add_slug_to_v2_transparency_items_table.php` | 2026-08-31 | adiciona `slug`, índice e backfill |
| `V2/Modules/DadosAbertos/TransparenciaFiscal/Migrations/2026_10_22_000001_add_v1_import_tracking_to_v2_transparency_tables.php` | 2026-10-22 | `v1_id`, `imported_at`, unique parcial nas 2 tabelas |

Padrões da casa observados nas migrations:

- `DB::beginTransaction()` / `commit` / `rollBack` manual (`2026_07_17_*:15-35`);
- `down()` **renomeia** para `backup_<tabela>_<YmdHis>` em vez de dropar
  (`2026_07_17_000001_...:43-47`);
- guardas `Schema::hasColumn` e `pg_indexes` para idempotência
  (`2026_10_22_...:19-23,41-60`);
- `SET lock_timeout = '3s'` antes de `ALTER TABLE` (`2026_10_22_...:31`).

## 5. Seed

`V2/Modules/DadosAbertos/TransparenciaFiscal/Seeds/TransparenciaSeeder.php`,
chamado pelo `V2/Seeds/V2EnvironmentSeeder.php:43`.

| Categoria | Slug | `order` | Filhas |
|---|---|---|---|
| Receitas | `receitas` | 1 | `exploracao-de-jogos` (1), `outras-receitas` (2) |
| Despesas | `despesas` | 2 | — |

Características:

- idempotente por `slug` com `withTrashed()` (`:44`, `:60`);
- reativa registros existentes (`:55`, `:71`);
- **não** atualiza nome/descrição/order de categoria já existente;
- dados de demonstração (4 itens) só com `$includeDemoData = true`
  (`:22`, `:76-97`), e não fazem parte da instanciação do ambiente.

Itens de demo: "Arrecadação Bruta — Concurso 2841" (R$ 2.850.000,00, 2026-05-02),
"Arrecadação — Janeiro 2026", "Repasse de Prêmios — Concurso 2841",
"Folha de Pagamento — Maio 2026".

## 6. Importação V1 (fora do módulo, grava nas tabelas)

`V2/Modules/Support/MigracaoLegada/Services/TransparenciaFiscalLegacyImporter.php`
lê `contents`/`category_contents`/`archives` da V1 e grava:

| Campo V2 | Origem V1 | Regra |
|---|---|---|
| `title` | `contents.title` | obrigatório |
| `slug` | `str_slug(title)` | `uniqueSlug` com `v1_id` (`:139`) |
| `description` | `summary` ou texto plano do HTML | `resolveDescription` |
| `value` | — | **sempre `null`** — perda declarada (`:141`) |
| `reference_date` | `publication_date` | só `Y-m-d` |
| `is_active` | `status` | `NULL`/`true` ⇒ `true` |
| `metadata.legado` | autor, source, clicks, header_image, categoria_path, arquivos | `:146-158` |
| `v1_id` / `imported_at` | id original | rastreio |
| categoria | cadeia V1 (raiz inclusa) | `firstOrCreate` por nome+parent |

Arquivos V1 são copiados para a galeria (`GalleryConfig('transparenciafiscal',
['image/*', 'application/pdf'])`, `:97-104`) e listados em
`metadata.legado.arquivos`, mas **não há UI que os exiba** — o destino não tem
galeria própria.

## 7. Dados e cardinalidade

- Categorias: 2 raízes + 2 filhas no seed padrão.
- Itens: 0 na instanciação; 4 apenas em modo demo.
- Volume esperado em produção: dezenas a poucas centenas de itens por exercício
  — não justifica particionamento nem cache.
- Não há `UNIQUE` em `(title)`, `(reference_date)` nem
  `(category_id, reference_date)` ⇒ duplicidade de lançamentos é possível e não
  detectada.
