---
id: MOD-INSTITUCIONAL-SOBRENOS-DADOS
tipo: modulo
onda: 5
status: concluido
unidades: [MOD-INSTITUCIONAL-SOBRENOS]
fontes:
  - V2/Modules/Institucional/SobreNos/
depende_de: [MOD-INSTITUCIONAL-SOBRENOS, CORE-DATA, MOD-SUPPORT-STRUCTURALLANDINGEDITOR, SVC-GALLERY]
consumidores: [MOD-INSTITUCIONAL-SOBRENOS, MOD-INSTITUCIONAL-SOBRENOS-CMS, MOD-INSTITUCIONAL-SOBRENOS-PORTAL, MOD-INSTITUCIONAL-SOBRENOS-WIDGETS]
criticidade: media
dono: SobreNos
---

# Sobre Nos — dados e modelo

> Anexo de [`spec.md`](spec.md). Descreve as duas tabelas próprias
> (`v2_bienniums`, `v2_board_members`), a tabela compartilhada que guarda o conteúdo
> (`v2_layout_base_configs`), o formato exato do payload de seções, o arquivo de fatores
> econômicos por UF e as regras de derivação (slug, `photo`, contadores). Foca em **quem
> escreve cada coluna** e no que é calculado em tempo de render.

## 0. Ficha de identidade

| Campo | Valor |
|---|---|
| Efeito primário | Persiste a diretoria (biênios + membros) em duas tabelas com UUID v4 e soft delete, e o layout editorial em um JSON único (`sobre_nos_sections`); fornece os insumos para os contadores. |
| Entrada | Requests do CMS (§5) e execução do `SobreNosDefaultSectionsSeeder`; leitura do JSON `Data/empresas-impactadas-por-estado.json`. |
| Saída | Linhas em `v2_bienniums`, `v2_board_members`, `v2_layout_base_configs`, `activity_log`; array de seções preparado; números de associados/anos/empresas. |
| Efeitos colaterais | Preenchimento de `slug`/`photo` por hooks; upsert de índices de busca; exclusões lógicas; nenhuma FK de `photo_id`. |
| Particularidade central | O conteúdo da página não tem tabela: é um JSON sem esquema rígido, normalizado apenas em `id`/`order`; já a diretoria é relacional e temporal (biênio corrente). |
| Como se integra | `V2\Modules\Institucional\SobreNos\Models\{Biennium,BoardMember}`; `LayoutBaseConfig` pertence a `MOD-SUPPORT-STRUCTURALLANDINGEDITOR`; `GalleryMedia` pertence a `SVC-GALLERY`. |
| Se removido, quebra | `SobreNosDataIntegrator`, `SobreNosCountersWidget`, `SobreNosPortalController@show`, CRUD CMS, índices de busca e rótulos do Dashboard. |
| Superfície pública | Models, scopes e accessors listados em §4.1; colunas descritas em §6. |

## 1. Efeito

### 1.1 O que o usuário consegue fazer

- Manter o layout de `/sobre-nos` como uma lista ordenada de seções tipadas, com
  parâmetros arbitrários por widget (JSON) — sem migration a cada ajuste editorial.
- Manter biênios e membros de forma relacional, com URL pública estável por slug.
- Ver números atualizados na página sem editá-los manualmente, desde que o rótulo do
  contador siga a convenção (§3.2).

### 1.2 O que muda no sistema

| Efeito | Onde é produzido | Fonte |
|---|---|---|
| Cria/atualiza a linha do payload (`key='sobre_nos_sections'`) | `saveSections` | `Controllers/SobreNosCmsController.php:66-74` |
| Insere/atualiza biênio com `id=v2_uuid()` | `storeBiennium`/`updateBiennium` + hook `creating` | `Controllers/BoardMemberController.php:168-174,208-212`; `Models/Biennium.php:60-69` |
| Insere/atualiza membro com `id`, `slug`, `photo`, `created_by` | `fillMember` + hooks `creating`/`saving` | `Controllers/BoardMemberController.php:70-72,277-296`; `Models/BoardMember.php:86-99` |
| `deleted_at` em membro/biênio (linha preservada) | `destroy`/`destroyBiennium` | `Controllers/BoardMemberController.php:142,237` |
| Reindexação de busca no save; remoção no delete | hooks `saved`/`deleted` | `Models/BoardMember.php:101-116` |
| Fatores por UF lidos do disco a cada cálculo | construtor do calculator | `Services/EmpresasImpactadasCalculator.php:15-26` |

### 1.3 Contrato observável (entrada → saída)

| Entrada | Resultado |
|---|---|
| `POST /salvar` com `landing_sections_json` string JSON de array | Linha criada/atualizada; `payload` com `id` em todas as seções e `order=1..n`. |
| `POST /salvar` com JSON `{}` (objeto) | `json_decode(..., true)` devolve array associativo → `is_array` verdadeiro → `ensureIds` itera `array_values` e produz `[]` (payload vazio). |
| `POST .../members/novo` válido | Linha em `v2_board_members` com `id`, `slug`, `photo`, `created_by`, `is_active=true`, `sort_order=0`. |
| `PUT .../members/editar/{id}` sem `photo_id` | `photo` passa a `null` (RN-SOBRENOS-12). |
| `DELETE .../bienniums/{id}` | `deleted_at` do biênio; membros permanecem com `biennium_id` apontando para ele. |
| `new EmpresasImpactadasCalculator()` sem o JSON | `$fatores=[]` → impacto `0` → valor `0`; nenhum erro. |

### 1.4 O que este módulo NÃO faz

- Não cria tabela de seções, versões ou rascunhos do payload.
- Não grava contadores calculados no banco: eles vivem apenas no array preparado.
- Não mantém histórico de alteração por coluna (só o `activity_log` do Spatie).
- Não apaga imagens da Galeria ao remover membros ou seções.

## 2. Abordagem

### 2.1 Estratégia em uma página

```mermaid
erDiagram
  v2_bienniums ||--o{ v2_board_members : "biennium_id → id (ON DELETE CASCADE)"
  v2_gallery_media ||--o{ v2_board_members : "photo_id (lógico, sem FK)"
  v2_layout_base_configs {
    uuid id PK
    varchar key UK "sobre_nos_sections"
    jsonb payload "array de seções"
    uuid transaction_snapshot_id
    timestamp deleted_at
  }
  v2_bienniums {
    uuid id PK
    varchar label
    int start_year
    int end_year
    bool is_current
    jsonb metadata
  }
  v2_board_members {
    uuid id PK
    uuid biennium_id FK
    varchar name
    varchar slug
    varchar position
    text description
    jsonb bio
    varchar photo
    varchar photo_id
    varchar linkedin_url
    varchar email
    int sort_order
    bool is_active
    varchar created_by
  }
```

### 2.2 Mecânica passo a passo

1. `V2ServiceProvider` carrega `Migrations/` de todos os módulos; as migrations deste
   módulo rodam na ordem dos nomes (`2025_01_01_000001` … `2026_09_01_000001`)
   (`V2/Core/Providers/V2ServiceProvider.php:114-118`).
2. `CreateV2BienniumsTable` cria `v2_bienniums` em transação com `lock_timeout='3s'`.
3. `CreateV2BoardMembersTable` cria `v2_board_members` com FK cascade para biênios.
4. `AddPhotoIdToV2BoardMembersTable` adiciona `photo_id varchar(36)` após `photo`.
5. `AddBioAndSlugToV2BoardMembers` adiciona `bio jsonb`, `slug varchar(255)` e o índice
   `idx_v2_board_members_slug`.
6. `NormalizeSobreNosSectionIds` reescreve `payload` do config com `ensureIds`.
7. Em runtime, os models aplicam hooks (`creating`, `saving`, `saved`, `deleted`) que
   derivam UUID, slug, foto e índices de busca.

### 2.3 Decisões de design e por quê

- **UUID v4 em tudo, gerado por `v2_uuid()`** `[C]`: models usam `v2_uuid()`
  (`Models/BoardMember.php:88`, `Models/Biennium.php:66`); o `LayoutBaseConfig` usa
  `Ramsey\Uuid\Uuid::uuid4()` (`V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:33-37`).
  Não há UUID v7 apesar do nome do helper de migração do core.
- **`payload` como `jsonb` com cast `array`** `[C]`: qualquer estrutura é aceita; a
  validação de forma é inteiramente responsabilidade do editor/widgets.
- **`photo` legado + `photo_id` novo coexistindo** `[C]`: o caminho é copiado para `photo`
  a cada save para manter compatibilidade com views antigas
  (`BoardMemberController.php:304-324`).
- **Soft delete em todas as tabelas** `[C]`: permite restaurar; a FK cascade só atua em
  delete físico, que o módulo não executa.
- **Sem unique em `slug` nem em `label`** `[C]`: evita falha de save, mas permite
  colisões (ver §3.3).

### 2.4 Trechos de código pivô

```php
// V2/Modules/Institucional/SobreNos/Migrations/2025_01_01_000002_create_v2_board_members_table.php:18-42
Schema::create('v2_board_members', function (Blueprint $table) {
    $table->uuid('id')->primary();
    $table->uuid('biennium_id');
    $table->string('name', 255);
    $table->string('position', 255);
    $table->text('description')->nullable();
    $table->string('photo', 255)->nullable();
    $table->string('linkedin_url', 255)->nullable();
    $table->string('email', 255)->nullable();
    $table->integer('sort_order')->default(0);
    $table->boolean('is_active')->default(true);
    $table->jsonb('metadata')->nullable();
    $table->string('created_by', 36)->nullable();
    $table->timestamps();
    $table->softDeletes();
    $table->foreign('biennium_id')->references('id')->on('v2_bienniums')->onDelete('cascade');
    $table->index('biennium_id', 'idx_v2_board_members_biennium');
    $table->index('is_active', 'idx_v2_board_members_active');
    $table->index('sort_order', 'idx_v2_board_members_order');
});
```

> É a tabela mais “relacional” do módulo: FK obrigatória, três índices simples e soft
> delete. Não há índice composto `(biennium_id, is_active, sort_order)`, que seria o
> predicado real do integrador; a query usa os índices isolados e ordena em memória.

```php
// V2/Modules/Institucional/SobreNos/Migrations/2026_07_20_000004_add_bio_and_slug_to_v2_board_members.php:18-22
Schema::table('v2_board_members', function (Blueprint $table) {
    $table->jsonb('bio')->nullable()->after('description');
    $table->string('slug', 255)->nullable()->after('name');
    $table->index('slug', 'idx_v2_board_members_slug');
});
```

> `slug` nasce nullable e sem unique; os registros anteriores à migration ficam sem slug
> até serem salvos (o hook só roda em `creating`/`saving`). URLs antigas dependem de
> `/diretoria/{slug}` → 301, que também exige slug preenchido.

```php
// V2/Modules/Institucional/SobreNos/Models/BoardMember.php:86-99
static::creating(function ($model) {
    if (empty($model->id)) {
        $model->id = v2_uuid();
    }
    if (empty($model->slug) && !empty($model->name)) {
        $model->slug = str_slug($model->name);
    }
});

static::saving(function ($model) {
    if (empty($model->slug) && !empty($model->name)) {
        $model->slug = str_slug($model->name);
    }
});
```

> `str_slug` (Laravel 5.5) normaliza acentos e espaços. A condição “só quando vazio” é o
> que estabiliza a URL: renomear o membro não muda o slug, e não há regeneração em massa.

```php
// V2/Modules/Institucional/SobreNos/Services/EmpresasImpactadasCalculator.php:32-50
$regionais = UnidadeRegional::active()->get();
foreach ($regionais as $regional) {
    $state = $regional->state;
    if (empty($state)) { continue; }
    $count = UnidadeRegionalEmpresa::where('unidade_regional_id', $regional->id)
        ->whereHas('empresa', function ($q) { $q->where('is_active', true); })
        ->count();
    if (!isset($resultado[$state])) { $resultado[$state] = 0; }
    $resultado[$state] += $count;
}
```

> Uma query por regional ativa (N+1). O vínculo empresa↔regional é
> `v2_unidade_regional_empresas`; só empresas `is_active=true` contam. Duas regionais na
> mesma UF somam. UF sem entrada no JSON é descartada depois (`:61-63`).

```php
// V2/Modules/Institucional/SobreNos/Seeds/SobreNosDefaultSectionsSeeder.php:120-131
foreach ($sections as $i => &$section) {
    $section['id'] = v2_uuid();
    $section['order'] = $i + 1;
}
unset($section);

$config = LayoutBaseConfig::withTrashed()->firstOrNew(['key' => 'sobre_nos_sections']);
if ($config->trashed()) {
    $config->restore();
}
$config->payload = $sections;
$config->save();
```

> O seeder **substitui** o payload inteiro. Executá-lo em ambiente com conteúdo editado
> destrói as edições; e cada imagem é reenviada para a Galeria sem deduplicação.

### 2.5 Algoritmos, consultas e custos

| Operação | SQL efetivo | Custo |
|---|---|---|
| Payload do portal | `SELECT * FROM v2_layout_base_configs WHERE key='sobre_nos_sections' LIMIT 1` | 1 query, sem cache próprio |
| Equipe | biênio `is_current=true` LIMIT 1 + membros `biennium_id`, `is_active=true` ORDER BY `sort_order` + mídias `WHERE id IN (...)` | 3 queries, independente do nº de membros |
| Foto de um membro | `photo_id` → `GalleryMedia` (belongsTo) ou `photo` | 0–1 query |
| Associados | `COUNT(*) v2_associado_instituicoes WHERE is_active=true` | 1 query por `prepare` (×2 por request) |
| Empresas impactadas | `v2_unidade_regionais WHERE is_active=true` + N × `COUNT(v2_unidade_regional_empresas … EXISTS empresa ativa)` | 1 + N queries, sem cache |
| Listagem CMS | `SELECT … LIMIT 15 OFFSET n` + count + eager load biênio/mídia | 3–4 queries/página |

## 3. Particularidades

### 3.1 O que só existe aqui

- **Biênio como entidade temporal** com `is_current` exclusivo e `end_year` derivado —
  nenhum outro módulo do corpus usa esse par.
- **`photo` espelho de `photo_id`**: duas colunas para a mesma imagem, uma legada
  (caminho) e outra nova (id da Galeria).
- **JSON de fatores econômicos versionado no código** (não em tabela), com 11 UFs e
  metadados de fonte/metodologia.

### 3.2 Invariantes e regras não óbvias

| ID | Regra | Efeito | Fonte |
|---|---|---|---|
| RN-SOBRENOS-D01 | `v2_board_members.biennium_id` é NOT NULL e FK cascade | Não existe membro sem biênio | `2025_01_01_000002_…:20,34-37` |
| RN-SOBRENOS-D02 | `photo_id` é `varchar(36)` sem FK, mas aponta para `v2_gallery_media.id` inteiro | Sem integridade; id órfão só quebra a imagem | `2026_07_20_000003_…:19`; Gallery `2026_06_28_000000_…:24` |
| RN-SOBRENOS-D03 | `bio` é `jsonb` com cast `array`, mas o controller grava a string JSON do CRE | No Laravel 5.5 o cast faz `json_encode` do valor: o JSON fica **duplamente codificado** (uma string JSON dentro do jsonb); o renderer decodifica duas vezes e o resultado é correto | `Models/BoardMember.php:68`; `BoardMemberController.php:283`; `V2/Services/ContentRichEditor/Services/ContentRichEditorRendererService.php:26-33` |
| RN-SOBRENOS-D04 | `created_by` recebe `Sentinel::getUser()->id` só quando vazio | Edições não sobrescrevem o autor original | `BoardMemberController.php:290-292` |
| RN-SOBRENOS-D05 | `is_current` é exclusivo por convenção de aplicação, não por constraint | Dois correntes são possíveis sob concorrência | `BoardMemberController.php:164-166,204-206` |
| RN-SOBRENOS-D06 | `end_year` é sempre derivado (`start_year+1`) | O campo não é editável de fato | `:172,210` |
| RN-SOBRENOS-D07 | `v2_bienniums.down()` renomeia para `backup_v2_bienniums_<timestamp>` em vez de dropar | Reverter migration preserva dados | `2025_01_01_000001_…:48-60` |
| RN-SOBRENOS-D08 | `v2_board_members.down()` dropa a FK e os índices antes de renomear | Idem | `2025_01_01_000002_…:60-73` |
| RN-SOBRENOS-D09 | `NormalizeSobreNosSectionIds::down()` remove `id` e `order` de todas as seções | Rollback não restaura valores antigos (destrutivo) | `2026_09_01_000001_…:48-65` |
| RN-SOBRENOS-D10 | O payload não tem chave única por seção nem referência externa | `id` é descartável; nada aponta para ele | `LayoutBaseConfig.php:18-27` |

### 3.3 Casos-limite e comportamentos surpreendentes

- **`slug` duplicado**: sem unique, o `firstOrFail` de `/equipe/{slug}` escolhe a primeira
  linha que o Postgres devolver; o sitemap publica a URL repetida
  (`Models/BoardMember.php:210-213`; `SeoPortalController.php:301-313`).
- **`biennium_id` de biênio soft-deleted**: `exists:v2_bienniums,id` valida contra a
  tabela (ignora `deleted_at`), então é possível salvar um membro num biênio “removido”;
  a relação Eloquent devolve `null` e o CMS mostra “—”.
- **Delete físico de biênio** (fora do módulo) apaga os membros por cascade, incluindo os
  já soft-deletados.
- **Falha ao ler o JSON de fatores**: `$fatores=[]` silenciosamente; todos os contadores
  de empresas ficam `0`.
- **UF não mapeada** (ex.: `DF`, `GO`): regional conta associados, mas o estado é ignorado
  no total; o valor exibido não muda ao cadastrar associados nessas UFs.
- **Payload com tipos de seção desconhecidos**: permanece no JSON; o portal tenta a view
  `v2-service-landingsectionseditor::render.<type>` e não renderiza nada se não existir
  (`landing-sections-viewer.blade.php:35-47`).
- **`bio` como array** (se algum código gravar via `fill()`): o cast armazena objeto JSON
  normal; o renderer aceita array. Os dois formatos coexistem no banco.
- **`metadata`** existe nas duas tabelas mas nunca é lido/escrito pelo módulo (campo
  morto funcional).

### 3.4 Armadilhas de reimplementação

- **Não** use `photo` como fonte de verdade: ele é reescrito de `photo_id` em cada save;
  definir apenas `photo` no banco será apagado no próximo update.
- **Não** adicione unique em `slug` sem migrar dados: existem homônimos possíveis e o
  portal tolera colisão.
- **Não** trate `payload` como objeto: o código sempre espera lista; `ensureIds` usa
  `array_values`.
- **Não** rode o seeder em produção: ele substitui o conteúdo editorial e duplica mídias.
- **Não** presuma `created_by` como UUID: a coluna é `varchar(36)` e aceita o id do
  Sentinel (int ou string), dependendo da instalação.
- **Não** calcule “anos de história” a partir de `v2_bienniums`: a constante 1965 é do
  widget, não do domínio (`SobreNosCountersWidget.php:13`).

### 3.5 Divergências documentação × código e dívidas

- `wiki/v2/calculo-empresas-impactadas.md` descreve piso de 10.000 e multiplicador
  ajustável; o código usa `ceil` sem piso e constante `10` (`Calculator.php:73-77`).
- O JSON declara `multiplicador_nacional_sugerido: 5` e um exemplo com fator normalizado
  (`0.2046`), mas o código usa `10` e `fator_combinado` bruto (`0.2561` para SP)
  (`Data/empresas-impactadas-por-estado.json:119-148` vs. `Calculator.php:65-66`).
- `totais` e `calculo_impacto` do JSON não são lidos por nenhum código; são documentação
  embutida.
- A coluna `metadata` das duas tabelas nunca é usada.

## 4. Integrabilidade

### 4.1 Superfície pública

| Símbolo | Assinatura | Retorno | Precondição |
|---|---|---|---|
| `Biennium::members()` | `hasMany(BoardMember, 'biennium_id','id')` | `HasMany` | tabela criada |
| `Biennium::scopeCurrent()` | `scopeCurrent($query)` | `Builder` (`is_current=true`) | — |
| `BoardMember::biennium()` | `belongsTo(Biennium, 'biennium_id','id')` | `BelongsTo` | — |
| `BoardMember::photoMedia()` | `belongsTo(GalleryMedia, 'photo_id','id')` | `BelongsTo` | — |
| `BoardMember::getPhotoUrl()` | `(): ?string` | caminho relativo (`photo` ou `photoMedia->file_path`) | — |
| `BoardMember::getPhotoFullUrl()` | `(): ?string` | URL via `v2_storage_url()` | storage configurado |
| `BoardMember::getHtmlBio()` | `(): ?string` | HTML ou `null` | CRE disponível |
| `BoardMember::syncToSearch()` | `(): void` | — | índices existentes |
| `BoardMember::scopeActive/Ordered/BySlug` | `($query[, $slug])` | `Builder` | — |

### 4.2 Contrato de UI/JS

Não se aplica a componentes Blade (ver [`cms.md`](cms.md) e
[`widgets-e-integracoes.md`](widgets-e-integracoes.md)). O contrato relevante aqui é o
**formato do payload** consumido pelo editor:

```json
[
  {
    "id": "b1c2…",
    "order": 1,
    "type": "sobre-nos-team",
    "parameters": {
      "subtitle": "Diretoria Executiva",
      "title": "Biênio 2026-2027",
      "description": "…"
    }
  }
]
```

O editor mantém esse array em memória e o serializa no hidden `landing_sections_json`
(`V2/Services/LandingSectionsEditor/Views/components/forms/landing-sections-editor.blade.php:19-24`).

### 4.3 Ordem e ciclo de vida

1. Migrations do core (users/roles) → migrations dos módulos/serviços (aqui:
   biênios → membros → `photo_id` → `bio/slug` → normalização do payload).
2. Seeders: `SucesuHomepageSeeder` → `SobreNosDefaultSectionsSeeder` → demais
   (`database/seeds/DatabaseSeeder.php:92-100`).
3. Runtime: hooks do model (`creating` → `saving` → `saved`) e, no delete, `deleted`.

### 4.4 Receitas de uso

```php
// Biênio corrente com os membros que a página mostra
$biennium = \V2\Modules\Institucional\SobreNos\Models\Biennium::with(['members' => function ($q) {
    $q->with('photoMedia')->active()->ordered();
}])->current()->first();
```

```php
// Detalhamento do cálculo de impacto por UF (auditoria do contador)
$detalhe = (new \V2\Modules\Institucional\SobreNos\Services\EmpresasImpactadasCalculator())
    ->getDetalhamento();
// ['SP' => ['estado' => 'São Paulo', 'associados' => 12, 'fator_combinado' => 0.2561, 'impacto_estimado' => 31], …]
```

```php
// Ler o payload cru (sem integração/preparo) para inspeção
$payload = optional(\V2\Modules\Support\StructuralLandingEditor\Models\LayoutBaseConfig::where('key', 'sobre_nos_sections')->first())->payload ?? [];
```

### 4.5 Anti-uso

- **Gravar `photo` diretamente** sem `photo_id`: o próximo save zera o campo
  (`BoardMemberController.php:306-318`).
- **Usar `v2_layout_base_configs.id` como referência de seção**: o `id` relevante está
  dentro do JSON e é regenerável.
- **Consultar `metadata` esperando SEO**: o módulo não escreve SEO em `metadata`; o SEO
  vem do controller/`SEOService`.

### 4.6 Custos e limites da integração

- `jsonb` sem índice GIN: qualquer busca dentro do payload exigiria full scan; o código
  sempre lê a linha inteira.
- O payload cresce com o número de seções e é transportado inteiro no POST do editor.
- Cálculo de empresas: N+1 por regional ativa e sem cache; o duplo `prepareSections`
  dobra o custo por render.

## 5. Rotas e contratos HTTP

As rotas que escrevem estes dados estão em [`cms.md`](cms.md) §5 (11 endpoints CMS). As
rotas que leem estão em [`portal.md`](portal.md) §5. Nenhuma rota expõe JSON destas
tabelas.

## 6. Dados e modelo de domínio

### 6.1 `v2_bienniums`

| Coluna | Tipo | Null | Default | Cast | Observação |
|---|---|---|---|---|---|
| `id` | `uuid` PK | não | — | string | `v2_uuid()` no hook `creating` |
| `label` | `varchar(255)` | não | — | — | sem unique |
| `start_year` | `integer` | não | — | integer | validado 1900–2100 |
| `end_year` | `integer` | não | — | integer | sempre `start_year+1` |
| `is_current` | `boolean` | não | `false` | boolean | exclusivo por aplicação |
| `metadata` | `jsonb` | sim | `null` | array | não usado |
| `created_at`/`updated_at` | `timestamp` | sim | — | Carbon | — |
| `deleted_at` | `timestamp` | sim | — | Carbon | soft delete |

Índices: `idx_v2_bienniums_current (is_current)`, `idx_v2_bienniums_years (start_year,end_year)`.
Fonte: `Migrations/2025_01_01_000001_create_v2_bienniums_table.php:18-30`;
`Models/Biennium.php:24-55`.

### 6.2 `v2_board_members`

| Coluna | Tipo | Null | Default | Cast | Observação |
|---|---|---|---|---|---|
| `id` | `uuid` PK | não | — | string | `v2_uuid()` |
| `biennium_id` | `uuid` FK | não | — | — | → `v2_bienniums.id` cascade |
| `name` | `varchar(255)` | não | — | — | obrigatório |
| `slug` | `varchar(255)` | sim | — | — | gerado por `str_slug`; índice |
| `position` | `varchar(255)` | não | — | — | obrigatório |
| `description` | `text` | sim | — | — | usada em card/meta/SEO |
| `bio` | `jsonb` | sim | — | array | AST do CRE (duplamente codificado) |
| `photo` | `varchar(255)` | sim | — | — | caminho legado, espelho de `photo_id` |
| `photo_id` | `varchar(36)` | sim | — | — | id da Galeria, sem FK |
| `linkedin_url` | `varchar(255)` | sim | — | — | validado como URL |
| `email` | `varchar(255)` | sim | — | — | validado como e-mail |
| `sort_order` | `integer` | não | `0` | integer | ordena seção e listagem |
| `is_active` | `boolean` | não | `true` | boolean | gate do portal/busca |
| `metadata` | `jsonb` | sim | `null` | array | não usado |
| `created_by` | `varchar(36)` | sim | `null` | — | id Sentinel |
| `created_at`/`updated_at`/`deleted_at` | `timestamp` | sim | — | Carbon | soft delete |

Índices: `biennium_id`, `is_active`, `sort_order`, `slug`.
Fonte: `Migrations/2025_01_01_000002_…:18-42`;
`Migrations/2026_07_20_000003_…:18-20`; `Migrations/2026_07_20_000004_…:18-22`.

### 6.3 `v2_layout_base_configs` (compartilhada)

| Coluna | Tipo | Observação |
|---|---|---|
| `id` | `uuid` PK | `Uuid::uuid4()` no `creating` |
| `key` | `varchar` | aqui sempre `sobre_nos_sections` |
| `payload` | `jsonb` | array de seções; cast `array` |
| `transaction_snapshot_id` | `uuid` | usado pelo logger |
| `deleted_at` | `timestamp` | soft delete; o módulo restaura ao salvar |

Fonte: `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php:13-38`.

### 6.4 Quem escreve cada coluna

| Escritor | Colunas |
|---|---|
| `BoardMemberController::fillMember` | `biennium_id`, `name`, `position`, `description`, `bio`, `photo_id`, `linkedin_url`, `email`, `sort_order`, `is_active`, `created_by` (se vazio), `photo` (resolvido) |
| Hook `creating` (BoardMember) | `id`, `slug` |
| Hook `saving` (BoardMember) | `slug` (se vazio) |
| Hook `creating` (Biennium) | `id` |
| `storeBiennium`/`updateBiennium` | `label`, `start_year`, `end_year`, `is_current` |
| `saveSections` | `payload` (linha inteira via `firstOrNew`) |
| Seeder | `payload` (substituição completa) |
| Migration de normalização | `payload` (`id`/`order`) |
| `slug`/`photo` em massa | **ninguém** — não há comando de backfill |

### 6.5 Dados derivados

| Derivado | Regra | Onde |
|---|---|---|
| `slug` | `str_slug(name)` quando vazio | `Models/BoardMember.php:90-99` |
| `photo` | `GalleryMedia::find(photo_id)->file_path` ou `null` | `BoardMemberController.php:304-324` |
| `end_year` | `start_year+1` | `BoardMemberController.php:172,210` |
| Membros da página | biênio corrente + `is_active` + `sort_order` | `Services/SobreNosDataIntegrator.php:42-46` |
| Contadores | rótulo → contagem / ano / impacto | `SobreNosCountersWidget.php:58-76` |
| Impacto por UF | `assoc × 10 × fator_combinado` | `EmpresasImpactadasCalculator.php:65-67` |

### 6.6 Fatores por UF (`Data/empresas-impactadas-por-estado.json`)

| Chave | Conteúdo |
|---|---|
| `metadata` | fonte (IBGE/Receita), data de geração (2026-09-01) e metodologia (`:2-5`) |
| `estados` | 11 UFs (`BA, CE, MT, MS, MG, PA, PB, PR, PE, RS, SP`) com `nome`, `capital`, `populacao_2025`, `empresas_ativas`, `pib_per_capita_2023`, `densidade_empresas_por_habitante`, `indice_potencial`, `fator_combinado` (`:7-118`) |
| `totais` | somas das 11 UFs e pesos de poder de compra (não lido pelo código) (`:119-136`) |
| `calculo_impacto` | descrição da fórmula, `multiplicador_nacional_sugerido: 5` e exemplo (não lido) (`:137-148`) |

## 7. Fluxos e estados

```mermaid
sequenceDiagram
  participant C as BoardMemberController
  participant M as BoardMember
  participant G as GalleryMedia
  participant S as Search indexes
  C->>M: fillMember (atribui campos)
  C->>G: find(photo_id)
  G-->>C: file_path | null
  C->>M: photo = file_path; save()
  M->>M: creating/saving (id, slug)
  M->>S: saved → syncToSearch (CMS + público)
  C-->>C: activity_log + clearPageCache
```

- `FL-SOBRENOS-D01` — criação de membro: validação → id → campos → foto → save (hooks) →
  busca → cache → auditoria.
- `FL-SOBRENOS-D02` — exclusão: `findOrFail` → `delete()` (soft) → hook `deleted` remove
  dos índices → cache → auditoria.
- `FL-SOBRENOS-D03` — troca de biênio corrente: `UPDATE … SET is_current=false WHERE is_current=true`
  → insert/update do alvo → cache.
- `FL-SOBRENOS-D04` — normalização retroativa: `withTrashed` → `ensureIds` → save.

## 8. UI CMS

Campos que escrevem cada coluna e as lacunas de formulário estão em [`cms.md`](cms.md)
§8. Aqui, apenas o mapa rápido: `name`, `position`, `biennium_id`, `linkedin_url`,
`email`, `description`, `bio` e `photo_id` são editáveis; `slug`, `sort_order`,
`is_active` e `created_by` não são.

## 9. UI Portal

Como os dados são exibidos (seção team, contadores, página do membro) está em
[`portal.md`](portal.md) §9.

## 10. Configuração, permissões e flags

- `cache.prefix` influencia a limpeza do cache (`SobreNosCmsController.php:98`).
- Permissão `SobreNos` (`v2_roles.permissions_v2`) para leitura/escrita.
- FLAG `module.active:SobreNos` afeta apenas rotas CMS.
- `filesystems.media_disk` (default `public`) é usado pelo `picture` via `v2_storage_url`
  (`V2/Services/Media/Views/components/picture.blade.php:1-18`).

## 11. Observabilidade, erros e limites

- `activity_log` para seções (old/new completos), membros e biênios.
- Logs de erro de foto/busca/bio/cache listados em [`spec.md`](spec.md) §11.
- Nenhuma validação de integridade referencial para `photo_id`; erros aparecem como
  imagem quebrada, não como exceção.
- Limite de 255 para textos curtos, 36 para ids de mídia, 15 itens por página na listagem.

## 12. Seeds e dados iniciais

| Seeder | Tabelas | Registros | Idempotência | Ordem | Fonte |
|---|---|---|---|---|---|
| `SobreNosDefaultSectionsSeeder` | `v2_layout_base_configs`, `v2_gallery_media` | 1 config (`sobre_nos_sections`) com 5 seções; 14 mídias (`hero/breadcrumb.png`, `about/about-2.png`, …) | **Não** para conteúdo (sobrescreve); mídias duplicam a cada execução | 2º em `DatabaseSeeder::callSeeds()` | `Seeds/SobreNosDefaultSectionsSeeder.php:20-140`; `database/seeds/DatabaseSeeder.php:92-93` |

Seções semeadas (nesta ordem): `sobre-nos-about` (título “Sobre a SUCESU”, CTA
“Conheça a Diretoria” → `#diretoria`, contador “78 / Data Investment”), `sobre-nos-team`
(“Biênio 2026-2027”), `sobre-nos-video` (YouTube `Cn4G2lZ_g2I`), `sobre-nos-counters`
(45+ / 5000+ / 100 mil+), `sobre-nos-testimonial` (“John Smith”, não renderizado).

## 13. Lacunas

- `LG-SOBRENOS-D01` — Volume real de `v2_board_members`/`v2_bienniums` não é conhecido;
  o índice composto recomendado depende da cardinalidade.
- `LG-SOBRENOS-D02` — Não há migration de backfill de `slug`; não é possível saber
  quantos registros antigos estão sem slug (impacta `/diretoria/{slug}` 301 e sitemap).
- `LG-SOBRENOS-D03` — O tipo do id de usuário do Sentinel (int/uuid) não foi confirmado
  no schema de `users`; `created_by` é `varchar(36)`.
- `LG-SOBRENOS-D04` — Não foi encontrado consumidor de `getDetalhamento()`, `metadata`,
  `totais` ou `calculo_impacto`; podem ser código/documentação mortos.
- `LG-SOBRENOS-D05` — A origem dos fatores do JSON (planilha externa) não está no
  repositório; a atualização é manual.

## 14. Checklist de paridade

- [ ] `PC-SOBRENOS-D01` — `v2_bienniums` criada com `id` uuid PK, `is_current` default `false` e índices `current`/`years` (`2025_01_01_000001_…:18-30`).
- [ ] `PC-SOBRENOS-D02` — `v2_board_members` com FK cascade e índices `biennium`/`active`/`order` (`2025_01_01_000002_…:18-42`).
- [ ] `PC-SOBRENOS-D03` — `photo_id varchar(36)` após `photo`, sem FK (`2026_07_20_000003_…:18-20`).
- [ ] `PC-SOBRENOS-D04` — `bio jsonb` + `slug` + índice de slug (`2026_07_20_000004_…:18-22`).
- [ ] `PC-SOBRENOS-D05` — `down()` preserva dados renomeando para `backup_*` (`2025_01_01_000001_…:48-60`).
- [ ] `PC-SOBRENOS-D06` — Sem `photo_id`, `photo` é gravado `null` (`BoardMemberController.php:306-309`).
- [ ] `PC-SOBRENOS-D07` — Slug gerado só quando vazio; renomear não altera (`Models/BoardMember.php:90-99`).
- [ ] `PC-SOBRENOS-D08` — `end_year` sempre `start_year+1` (`BoardMemberController.php:172,210`).
- [ ] `PC-SOBRENOS-D09` — Cálculo ignora UF sem fator no JSON (`EmpresasImpactadasCalculator.php:61-63`).
- [ ] `PC-SOBRENOS-D10` — Contadores de associados consideram só empresas ativas (`:40-44`).
- [ ] `PC-SOBRENOS-D11` — Normalização roda em `up()` e remove `id`/`order` em `down()` (`2026_09_01_000001_…:22-29,48-65`).
- [ ] `PC-SOBRENOS-D12` — Seeder substitui o payload e não deduplica mídias (`Seeds/SobreNosDefaultSectionsSeeder.php:120-131`).

## 15. Evidências consultadas

| Arquivo | Faixas |
|---|---|
| `Migrations/2025_01_01_000001_create_v2_bienniums_table.php` | 11-68 |
| `Migrations/2025_01_01_000002_create_v2_board_members_table.php` | 11-83 |
| `Migrations/2026_07_20_000003_add_photo_id_to_v2_board_members_table.php` | 11-54 |
| `Migrations/2026_07_20_000004_add_bio_and_slug_to_v2_board_members.php` | 11-65 |
| `Migrations/2026_09_01_000001_normalize_sobre_nos_section_ids.php` | 15-73 |
| `Models/Biennium.php` | 19-87 |
| `Models/BoardMember.php` | 33-258 |
| `Services/EmpresasImpactadasCalculator.php` | 9-103 |
| `Services/SobreNosDataIntegrator.php` | 12-74 |
| `Data/empresas-impactadas-por-estado.json` | 1-148 |
| `Seeds/SobreNosDefaultSectionsSeeder.php` | 20-141 |
| `V2/Modules/Support/StructuralLandingEditor/Models/LayoutBaseConfig.php` | 9-38 |
| `V2/Services/Gallery/Migrations/2026_06_28_000000_create_v2_gallery_media_table.php` | 23-44 |
| `database/seeds/DatabaseSeeder.php` | 88-104 |
