# Modelagem de Dados — Editor Estrutural + Landing Sections Editor

> Caminhos relativos à raiz `lotep/`. Três tabelas próprias, nenhuma view SQL, nenhuma relação
> Eloquent declarada. A tabela `v2_gallery_media` é dependência externa (serviço Gallery).

---

## 1. Visão geral

| Tabela | Migration | Model | Retenção |
|---|---|---|---|
| `v2_structural_portal_contents` | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000000_create_v2_structural_portal_blocks_table.php` | `StructuralPortalContent` | sem soft delete |
| `v2_layout_base_configs` | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_03_000001_create_v2_layout_base_configs_table.php` | `LayoutBaseConfig` | soft delete |
| `v2_sle_assets` | `V2/Modules/Support/StructuralLandingEditor/Migrations/2026_07_06_000000_create_v2_sle_assets_table.php` | `SleAsset` | soft delete |

Todas usam UUID como PK e são criadas com `DB::beginTransaction()` + `SET lock_timeout = '3s'`;
o `down()` **renomeia** para `backup_{tabela}_{timestamp}` em vez de dropar.

---

## 2. `v2_structural_portal_contents`

Migration: `2026_07_03_000000_create_v2_structural_portal_blocks_table.php:22-30`.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | `uuid` | não | `v2_gen_uuid()` | PK |
| `area` | `varchar(30)` | não | — | **UNIQUE**; uma linha por área |
| `content` | `text` | sim | — | HTML **e Blade** executável |
| `content_type` | `varchar(20)` | não | `htmlmixed` | `htmlmixed`, `css`, `javascript` |
| `active` | `boolean` | não | `true` | área inativa cai no fallback |
| `created_at` / `updated_at` | `timestamp` | sim | — | — |

Trecho real:

```php
Schema::create('v2_structural_portal_contents', function (Blueprint $table) {
    $table->uuid('id')->primary()->default(DB::raw('v2_gen_uuid()'));

    $table->string('area', 30)->unique();
    $table->text('content')->nullable();
    $table->string('content_type', 20)->default('htmlmixed');
    $table->boolean('active')->default(true);

    $table->timestamps();
});
```

**Áreas canônicas** (`SleCmsController.php:379-382`): `header`, `main_left`, `main_right`, `footer`,
`css_head`, `css_foot`, `js_head`, `js_foot`.

**Model** `V2/Modules/Support/StructuralLandingEditor/Models/StructuralPortalContent.php`:

- `$incrementing = false`, `$keyType = 'string'`, `$dateFormat = 'Y-m-d H:i:s'`.
- `$fillable`: `id`, `area`, `content`, `content_type`, `active`.
- `casts`: `active => boolean`.
- `boot()` gera `Uuid::uuid4()` quando `id` está vazio (`:53-60`).
- `getForArea($area)` → `where('area', $area)->first()` (uma query por chamada).
- `hasActiveInArea($area)` → `active = true AND content != ''`.

Não há índice além do UNIQUE de `area`, nem constraint de check para os valores de `area` — a
whitelist é só de aplicação (`SleCmsController::saveEditor`).

---

## 3. `v2_layout_base_configs`

Migration: `2026_07_03_000001_create_v2_layout_base_configs_table.php:24-32`.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | `uuid` | não | — | PK; gerado no model |
| `key` | `varchar(255)` | não | — | **UNIQUE** |
| `payload` | `jsonb` | sim | — | estrutura arbitrária por chave |
| `transaction_snapshot_id` | `uuid` | sim | — | coluna existe; o módulo não a escreve |
| `created_at` / `updated_at` | `timestamp` | sim | — | — |
| `deleted_at` | `timestamp` | sim | — | `SoftDeletes` |

A migration tem guarda de idempotência:

```php
if (Schema::hasTable('v2_layout_base_configs')) {
    return;
}
```

Fonte: `2026_07_03_000001_create_v2_layout_base_configs_table.php:17-19`.

**Model** `LayoutBaseConfig.php`:

- `use SoftDeletes;`
- `$fillable`: `id`, `key`, `payload`, `transaction_snapshot_id`.
- `casts`: `payload => array` (JSONB ↔ array).
- `boot()` gera UUID v4.
- **Nenhuma relação** e nenhum accessor.

### 3.1 Chaves gravadas

| `key` | Escritor | Forma do payload |
|---|---|---|
| `logos` | `handleLogosUpdate` | objeto com uma entrada por logo (`logo_principal`, `logo_header`, `logo_header_contraste`, `sefaz`, `governo`, `membro_1`, `membro_2`), o favicon e chaves de alt (`*_alt`) |
| `links_header` | `handleLinksHeaderUpdate` | lista de nós `{title, url, children[]}` (até 3 níveis) |
| `links_footer` | `handleLinksFooterUpdate` | lista de nós equivalente |
| `contato` | `handleContatoUpdate` | `{endereco, map_link, telefone_1, telefone_2, email, horario}` |
| `social_media` | `handleRedesSociaisUpdate` | lista `{network, url, icon}` |
| `lai_items` | `handleTransparenciaLaiUpdate` | lista de itens de transparência |
| `global_banner` | `handleComunicadoGlobalUpdate` | `{url_destino, start_date, end_date, banner_image, banner_image_alt}` |
| `lai_enabled` | `SleCmsController::saveFeatures` | `{enabled: bool}` |
| `homepage_sections` | `handleHomepageUpdate` | ver 3.2 |

Fontes: `LayoutBaseCmsController.php:78-333`; `SleCmsController.php:322-327`.

O payload de imagem é padronizado como:

```php
$payload[$logo] = [
    'original_name' => $galleryMedia->description ?: basename($galleryMedia->file_path),
    'path' => $galleryMedia->file_path,
    'disk' => $galleryMedia->disk ?? 'public',
    'mime_type' => $galleryMedia->mime_type,
    'size' => $galleryMedia->file_size,
    'type' => strpos($galleryMedia->mime_type, 'image/') === 0 ? 'image' : 'unknown',
    'variants' => is_array($galleryMedia->variants) ? $galleryMedia->variants : (json_decode($galleryMedia->variants, true) ?: []),
    'metadata' => [],
];
```

Fonte: `LayoutBaseCmsController.php:88-96`.

### 3.2 `homepage_sections` (versão 3)

Contrato persistido por `handleHomepageUpdate()`:

```php
$payload = [
    'version' => HomepageSectionsService::VERSION,
    'sections' => $clean,
];
```

Fonte: `LayoutBaseCmsController.php:313-318`.

Forma do documento (exemplo ilustrativo, montado a partir de
`HomepageSectionsService::defaultSectionsList()`):

```json
{
  "version": 3,
  "sections": [
    {
      "id": "home-2b8c0a3f9d11",
      "type": "home-hero",
      "parameters": {
        "eyebrow": "Governo do Estado da Paraíba",
        "title": "Regulação, fiscalização e controle lotérico.",
        "primary_cta": {"label": "Conheça a LOTEP", "url": "https://lotep.pb.gov.br/sobre-nos"}
      }
    },
    {
      "id": "home-77a1c4e0b2aa",
      "type": "home-agenda",
      "parameters": []
    }
  ]
}
```

Regras:

- `id` é gerado no cliente (UUID v4) ou, no prefill, por `stableId($type)` =
  `'home-' . substr(hash('sha1', $type), 0, 12)` (`HomepageSectionsService.php:242-247`).
- `parameters` é array; nas seções dinâmicas é ignorado na renderização.
- `hide` é opcional e booleano por item (`HomepageSectionsService.php:225-231`).
- `home-header` é um item especial: seus `parameters` viram `header`, fora de `order`
  (`HomepageSectionsService.php:213-219`).

---

## 4. `v2_sle_assets`

Migration: `2026_07_06_000000_create_v2_sle_assets_table.php:22-39`.

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | `uuid` | não | `v2_gen_uuid()` | PK |
| `type` | `varchar(20)` | não | — | `css`, `js`, `image` (validado na aplicação) |
| `original_name` | `varchar(255)` | não | — | nome enviado |
| `storage_path` | `varchar(500)` | não | — | caminho no storage |
| `mime_type` | `varchar(100)` | não | — | MIME **declarado pelo upload** |
| `file_size` | `bigint unsigned` | não | — | bytes |
| `gallery_media_id` | `int unsigned` | sim | — | vínculo lógico com `v2_gallery_media.id` |
| `snippet` | `text` | não | — | trecho Blade/HTML copiável |
| `created_at` / `updated_at` | `timestamp` | sim | — | — |
| `deleted_at` | `timestamp` | sim | — | `SoftDeletes` |

Índices: `idx_sle_assets_type`, `idx_sle_assets_gallery`; **não** há FK nem UNIQUE em
`gallery_media_id` (o código deduplica com `SleAsset::where('gallery_media_id', $id)->first()` em `createFromGallery`).

O `snippet` é gerado com diretivas Blade literais:

```php
return '@include(\'v2-service-media::components.picture\', [' . "\n"
    . "    'originalPath' => '" . addslashes($filePath) . "',\n"
    . "    'variants' => " . $variantsJson . ",\n"
    . "    'disk' => '" . $disk . "',\n"
    . "    'alt' => '" . $alt . "',\n"
    . "    'class' => 'img-fluid rounded',\n"
    . "    'lazy' => true,\n"
    . '])';
```

Fonte: `AssetManagerService.php:231-246`.

Para CSS/JS:

```php
if ($type === 'css') {
    return '<link rel="stylesheet" href="{{ v2_storage_url(\'' . $storagePath . '\') }}">';
}

if ($type === 'js') {
    return '<script src="{{ v2_storage_url(\'' . $storagePath . '\') }}"></script>';
}
```

Fonte: `AssetManagerService.php:216-224`.

---

## 5. Relacionamentos

Nenhum `belongsTo`/`hasMany` é declarado nos três models. Os vínculos são por valor:

| Origem | Destino | Como |
|---|---|---|
| `v2_sle_assets.gallery_media_id` | `v2_gallery_media.id` | inteiro; consultado direto com `DB::table('v2_gallery_media')->find()` |
| `homepage_sections.sections[].parameters.*_image_id` | `v2_gallery_media.id` | `whereIn` único em `prepareSections` |
| `logos` / `global_banner` payload | `v2_gallery_media` | `GallerySender::find()` no save; cópia do payload |
| `v2_layout_base_configs` | nenhuma | chave-valor |

`v2_gallery_media` é do serviço Gallery. `prepareSections` consulta a tabela diretamente, sem
`deleted_at` explícito no `whereIn` (`LandingSectionsEditorRendererService.php:339-348`).

---

## 6. Views SQL

**Nenhuma.** O módulo e o serviço não criam `CREATE VIEW`. O único SQL cru é o `whereIn` em
`v2_gallery_media` e os `DB::table` de e-mail em `UserConfigsService` (tabela
`v2_system_mail_configs`).

---

## 7. Seeds

| Seeder | O que grava | Observação |
|---|---|---|
| `V2/Seeds/StructuralPortalSeeder.php` | cria as 8 áreas vazias/inativas, com `content_type` por área | fallback gov ativo |
| `V2/Seeds/SleHeaderSeeder.php` | HTML do header na área `header` | específico SUCESU (heredoc) |
| `V2/Seeds/SleFooterSeeder.php` | HTML do footer na área `footer` | usa `$layoutConfigs` dentro do template |
| `V2/Seeds/SleConfigsSeeder.php` | `css_head` e `js_foot` com CSS/JS | SUCESU |
| `V2/Seeds/SleAssetSeeder.php` | `v2_sle_assets` + Gallery | ingestão via `seedFromPath()` |
| `V2/Seeds/BasicPages/LotepHomepageSeeder.php` | chaves de `v2_layout_base_configs` (entre elas as imagens/links da home) | INSERT/UPDATE direto, JSON por `json_encode` |

Trecho do seed de estrutura:

```php
foreach ($govAreas as $area) {
    $defaultType = 'htmlmixed';
    if (in_array($area, ['css_head', 'css_foot'])) {
        $defaultType = 'css';
    } elseif (in_array($area, ['js_head', 'js_foot'])) {
        $defaultType = 'javascript';
    }

    DB::table('v2_structural_portal_contents')->updateOrInsert(
        ['area' => $area],
        [
            'id' => DB::table('v2_structural_portal_contents')->where('area', $area)->value('id') ?? v2_uuid(),
            'content' => '',
            'content_type' => $defaultType,
            'active' => false,
            'created_at' => $now,
            'updated_at' => $now,
        ]
    );
}
```

Fonte: `V2/Seeds/StructuralPortalSeeder.php:38-56`.

O seed de home grava JSON diretamente:

```php
$exists = DB::table('v2_layout_base_configs')->where('key', $key)->exists();
if ($exists) {
    DB::table('v2_layout_base_configs')->where('key', $key)->update(['payload' => json_encode($payload), 'updated_at' => $now]);
} else {
    DB::table('v2_layout_base_configs')->insert(['id' => v2_uuid(), 'key' => $key, 'payload' => json_encode($payload), 'transaction_snapshot_id' => null, 'created_at' => $now, 'updated_at' => $now]);
}
```

Fonte: `V2/Seeds/BasicPages/LotepHomepageSeeder.php:274-279`.

---

## 8. Auditoria

`V2AuditLogger::activity()` é chamado com o model (`LayoutBaseConfig`, `SleAsset`) ou com
`new StructuralPortalContent()` como sujeito para eventos de tela. O histórico da tela lê
`getHistory($configModel)` (`V2/Modules/Support/StructuralLandingEditor/Views/cms/tabs/layoutbase/components/history-table.blade.php:1-8`).
O `transaction_snapshot_id` não é preenchido em nenhum fluxo deste módulo — a coluna e a menção na
tela de e-mail (`Views/cms/tabs/userconfigs/email.blade.php:22`) descrevem uma capacidade não
implementada aqui.

---

## 9. Riscos de dados

| Risco | Efeito | Evidência |
|---|---|---|
| `content` text sem limite | payloads de MB no banco e no preview | migration `:26` |
| `area` sem CHECK/whitelist | áreas novas escrevem linhas que nunca renderizam | `SleCmsController.php:393-425` |
| `v2_layout_base_configs.payload` jsonb sem schema | qualquer JSON é aceito; `homepage_sections` só é validada na aplicação | `LayoutBaseCmsController.php:336-348` |
| `snippet` guarda Blade | o texto é copiado para dentro de outro template e executado | `AssetManagerService.php:231-246` |
| imagens arquivadas | `prepareSections` não filtra `deleted_at` da galeria | `LandingSectionsEditorRendererService.php:339-348` |
| soft delete de config | `LayoutBaseConfig` usa `SoftDeletes`; consultas `first()` ignoram deletados, mas o registro pode ser recriado com a mesma `key`? Não: o UNIQUE inclui o deletado | `LayoutBaseConfig.php:41-47`; migration `:26` |
