# Formulários Dinâmicos — Aproveitamento no V7

> **Slug:** `infraestrutura-formulariosdinamicos` · **Origem:** `lotep` (Laravel 5.5/PHP 7.4)
> **Destino:** `public-v7` (Laravel 12/PHP 8.4, DDD-lite)
> **Raiz de referência do legado:** `/home/one/projects/maxima/v7/lotep`
> **Raiz de referência do V7:** `/home/one/projects/maxima/v7/public-v7`

---

## 1. O que o V7 tem hoje

| Capacidade | Situação no V7 | Evidência |
|---|---|---|
| Motor de formulários dinâmicos | **não existe** | nenhum módulo/service equivalente em `src/modules` ou `src/services` |
| Formulário público como caso de uso | há apenas o DSAR de LGPD, com campos fixos | `src/modules/Portal/Lgpd/Logic/Controllers/LgpdDsarFormController.php` |
| Versionamento de conteúdo | existe para notícias/layout, não para formulários | `src/modules/Publicacoes/Noticias/Database/Migrations/2026_10_07_120000_create_noticias_table.php` |
| jsonb para configuração | sim (`placements`, `permissoes`, `metadata`) | `src/services/Layout/Models/LayoutProfile.php`; `src/core/Access/Database/Migrations/2026_10_16_000000_create_core_roles_table.php` |
| Permissão por módulo | `{modulo}.{ver|editar|aprovar|arquivar}` | `src/core/Access/Permission.php`; ADR-033 |
| Ativação de módulo em runtime | middleware + `ModuleActivation` | ADR-034 |
| Auditoria/telemetria | `AuditWriter`, log JSON, spans | ADR-024/ADR-035 |
| CSP sem inline | middleware de headers + Vite | `src/core/Internal/Http/Middleware/EnforceSecurityHeaders.php`; ADR-022 |
| i18n | `Lang/{pt_BR,en,es,fr}` por módulo/service | ADR-012 |
| Retenção/expurgo | `RetentionRegistry` + `retention:expunge` | `src/core/Contracts/RetentionRegistry.php`; `src/core/Internal/Retention/Console/ExpungeRetentionCommand.php` |
| Media/AVIF | service headless | `src/services/Media/Contracts/MediaIngestor.php`; ADR-026 |
| SEO `@graph` | service SEO | `src/services/Seo/ServiceProvider.php`; ADR-027 |

**Conclusão:** o V7 tem **todas as fundações** que faltavam no legado — permissões, i18n,
auditoria, retenção, mídia, CSP — mas **não tem o motor**. O aproveitamento é portar o modelo
mental e reescrever a implementação sobre essas fundações.

## 2. Encaixe proposto

```
src/services/Formularios/                 # motor headless (ADR-015)
├── ServiceProvider.php                   # implements Core\Contracts\Service
├── Contracts/                            # portas consumidas por módulos
│   ├── ValidadorRespostas.php
│   ├── CalculadoraPontuacao.php
│   ├── ClassificadorFaixa.php
│   ├── RegistroSubmissao.php
│   ├── RelatorioFormulario.php
│   ├── ExportadorCsv.php
│   └── AssinadorResultado.php
├── DTOs/                                 # FormularioAtivo, CampoDTO, RespostasDTO, ...
├── Enums/                                # TipoCampo, StatusFormulario, TipoFaixa
├── Models/                               # Formulario, Versao, Campo, Submissao
├── Actions/                              # Localizar, Validar, Calcular, Classificar, Registrar, Agregar
├── Migrations/                           # schema formularios (4 tabelas)
└── Lang/{pt_BR,en,es,fr}/                # erros/validações do motor

src/modules/Infraestrutura/Formularios/   # caso de uso (portal + CMS)
├── ModuleProvider.php                    # permissions + menu (ADR-014)
├── Database/Models (se houver algo próprio) 
├── Logic/{Controllers,Requests,Actions}  # controllers finos consumindo contratos
├── Routes/{portal,cms,api}.php
├── Layout/Views/                         # portal + cms
├── Lang/{pt_BR,en,es,fr}/
└── Test/FormulariosTest.php
```

Por que service **e** módulo:

- o **service** não pode ter rota pública (ADR-015). O motor é usado por mais de um caso de uso
  (portal genérico, AutoTeste, futuras pesquisas), então precisa ser agnóstico;
- o **módulo** é o caso de uso que expõe `/formularios/{slug}` e `/cms/formularios`, declara as
  permissões e traduz as strings do fragmento que publica (ADR-012);
- o módulo depende do **contrato**, nunca da implementação (ADR-015).

O token canônico único deve ser **`formularios`** (ADR-004): tabelas `formularios.*`, classes
`Formulario*`, rotas `portal.formularios.*` / `cms.formularios.*`, permissões `formularios.*`,
spans `formularios.*`. Não repetir o par `formularios`/`formulario-dinamico` do legado.

## 3. O que portar (padrão concreto)

| # | Padrão do legado | Como portar | Justificativa no V7 |
|---|---|---|---|
| P1 | **Versão imutável**: editar com submissões cria `v+1` e a submissão aponta para `versao_id` | preservar a regra na `Action SalvarFormulario`; sem soft delete em versão; FK `restrict`/`cascade` explícita | histórico é o requisito funcional central; ADR-017 (integridade) |
| P2 | **Campos por versão** com `tipo`, `nome`, `rotulo`, `obrigatorio`, `ordem`, `peso`, `opcoes` jsonb, `regras` jsonb | portar como colunas + jsonb, sem a tabela de perguntas solta | jsonb é o padrão do V7 para configuração (ADR-017) |
| P3 | **Submissão anônima**: `respostas` jsonb, `ip_hash`, `consentimento_em`, `origem` | portar; trocar HMAC de `app.key` por pepper versionado; adicionar `expira_em` | LGPD (ADR-019); mínimo de dados |
| P4 | **Scoring por peso de pergunta/opção e faixas percentuais/absolutas** | portar a matemática (incluindo `floor` no limite) e cobrir com testes de fronteira | regra observável; ADR-038 exige rigor de não regressão |
| P5 | **Diagnóstico congelado** (`resultado` jsonb com faixa/textos/canais) | portar; manter `faixa` como coluna indexável | histórico é evidência; ADR-017 (dados desacoplados) e ADR-035 (congelar o fato) |
| P6 | **Portal não revela diagnóstico** | portar a regra; resource/rota assinada só com link | minimização de dado pessoal (ADR-019); portal não gera trilha individual (ADR-035) |
| P7 | **CMS com editor de perguntas e faixas 0–100%** | portar a UX, não o código: componentes Blade + JS em módulo Vite | ADR-009/ADR-010; CSP sem inline (ADR-022) |
| P8 | **Slug reutilizável após soft delete** | portar renomeando o antigo para `slug--{hash}` e preservando histórico | integridade do slug e histórico (ADR-017); descarte do alias legado (ADR-042) |
| P9 | **Agregados para relatório** (total, média, por faixa, série diária) | portar o contrato, reimplementar em SQL | otimização nativa e resposta previsível (ADR-008/ADR-024) |
| P10 | **CSV** com BOM UTF-8 e cabeçalho estável | portar o formato; trocar memória por streaming e neutralizar fórmulas | exportação é acesso a dado pessoal e deve ser auditada (ADR-019/ADR-035) |

## 4. O que adaptar

| # | Item | Adaptação | Justificativa |
|---|---|---|---|
| A1 | **`result_token` UUID não assinado** | `URL::temporarySignedRoute` + middleware `signed`, validade configurável | o comentário do legado prometia assinatura; o V7 resolve com recurso nativo, sem criptografia própria |
| A2 | **Relatórios em PHP** | `jsonb_each`/`jsonb_array_elements` + `GROUP BY` no PostgreSQL, com filtro obrigatório por `formulario_id` | evita O(N·M) em memória; PG 18 é o alvo (ADR-007) |
| A3 | **GIN `jsonb_path_ops` sem uso** | só criar se houver consulta por resposta; se o relatório filtrar por valor, criar índice **e** consulta correspondente | ADR-017 (só o necessário; sem índice decorativo) |
| A4 | **Renderer por string** | DTO de campo + componentes anônimos; nada de HTML atravessando service → módulo | escape centralizado, CSP e acessibilidade (ADR-022/025) |
| A5 | **`consent_text` com HTML** | separar `consent_texto` (texto) de `consent_url` (link); view monta o `<a>` | ADR-017 proíbe HTML em dado; evita XSS |
| A6 | **Strings fixas em português** | `Lang/{locale}` no service (erros) e no módulo (rótulos/telas) | ADR-012 |
| A7 | **Tipo `file` órfão** | só portar quando o fluxo de upload usar Media; resposta guarda `media_id` | ADR-026; evita tipo que não resolve |
| A8 | **`schema` stub** | remover a coluna; a estrutura é `campos` | contrato falso gera erro de leitura |
| A9 | **Permissões `read`/`write`** | `formularios.{ver,editar,aprovar,arquivar}`; publicar/arquivar separados | ADR-033 |
| A10 | **Menu/ativação por módulo** | declarar `permissions` e `route` no `ModuleProvider`; ativação via dado | ADR-014/ADR-034 |
| A11 | **Auditoria `V2AuditLogger` + `Log::info`** | `AuditWriter` com evento `formularios.*`; log JSON com `request_id` e sem PII | ADR-024/ADR-035 |
| A12 | **Layout/SEO** | página de formulário é `cache.private`; usar `<x-service-seo::tags>` com `robots noindex` se o formulário for restrito | ADR-022/ADR-027 |
| A13 | **Slug e rotas** | `/formularios/{slug}` (portal) e `/cms/formularios` (CMS); API `/api/formularios` | token único e previsível (ADR-004) |
| A14 | **Idempotência de importação** | manter `origem_id` + unique parcial **se** houver importação; caso contrário não portar | ADR-017 |

## 5. O que rejeitar

| Item | Por que rejeitar | Regra do V7 |
|---|---|---|
| Módulo `V2/Modules/Infraestrutura/FormulariosDinamicos` inteiro | port sem telas, permissão duplicada, escrita residual com IP cru, migration inválida | ADR-013 (fronteira física) e ADR-042 (sem retrocompatibilidade) |
| Tabelas `v2_forms`, `v2_form_categories`, `v2_form_questions`, `v2_form_submissions` | modelo superado; categorias nunca usadas; submissões com IP cru | ADR-017 |
| `FormDefaultSeeder` | seed não é bootstrap | ADR-041 |
| `V2FormCategory` / categorias de formulário | nenhum caso de uso; aumenta o modelo sem valor | ADR-001 |
| Scripts inline do editor de faixas e do portal | CSP sem inline e ETag estável | ADR-022 |
| jQuery / bibliotecas de máscara/gráfico | zero CDN e mínimo de dependências; gráficos em SVG/Blade | ADR-002/ADR-010 |
| `has_result`/`hide_title`/`submit_info` | flags da UI antiga; comportamento deve ser dado do formulário, não booleanos soltos | ADR-017 |
| Redirecionamentos de compatibilidade | não há consumidor externo que justifique manter `/cms/v2/formularios` | ADR-042 |
| `result_level` visível no portal/API de submissão | expõe diagnóstico ao cidadão, contrariando a decisão do próprio legado | ADR-019 (minimização) |
| Hash de IP com `APP_KEY` sem versionamento | rotação invalida histórico sem sinal | ADR-019 |
| HTML em `consent_text`/mensagens | dado não é apresentação | ADR-017/ADR-028 |

## 6. Desenho mínimo do motor no V7

### 6.1 Contrato do service

```php
<?php

declare(strict_types=1);

namespace Services\Formularios\Contracts;

use Services\Formularios\DTOs\FormularioAtivo;
use Services\Formularios\DTOs\Respostas;
use Services\Formularios\DTOs\SubmissaoCriada;

interface RegistroSubmissao
{
    public function execute(FormularioAtivo $formulario, Respostas $respostas, ContextoSubmissao $contexto): SubmissaoCriada;
}
```

Exemplo ilustrativo. O módulo injeta a interface; o Core faz o binding via
`ServiceProvider::contracts()` (ADR-015).

### 6.2 Fluxo de dados

```
CMS:  SalvarFormularioRequest → SalvarFormulario Action → Formulario/Versao/Campo
                                  └─ se há submissões: cria Versao(numero+1)
                                  └─ AuditWriter(event: formularios.editado|publicado|arquivado)

Portal: FormularioPortalController@show
          LocalizarFormularioAtivo::execute(slug) → FormularioAtivo
          view('formularios::portal.form', ['campos' => CampoDTO[]])

        FormularioPortalController@store
          SubmeterFormularioRequest (envelope + honeypot + time-trap)
          RegistrarSubmissao::execute(...)
            ValidarRespostas → CalcularPontuacao → ClassificarFaixa
            IP → hash com pepper versionado
            Submissao::create
            AuditWriter(event: formularios.submetido)
          redirect → URL assinada de resultado

API:    SchemaController → Resource sem `peso`
        SubmitController → mesmo RegistrarSubmissao
        ResultsController → GerarRelatorio (SQL)
```

### 6.3 Onde cada dado mora

| Dado | Local | Formato |
|---|---|---|
| Título/descrição do formulário | `formularios.formularios` | texto; tradução em jsonb se multilíngue |
| Configuração de scoring/faixas | `formularios.versoes.scoring_config` | jsonb (`bands`, `consentimento`, `disclaimer`) |
| Perguntas | `formularios.campos` | colunas + `opcoes`/`regras` jsonb |
| Respostas | `formularios.submissoes.respostas` | jsonb (`{nome: valor}`) |
| Diagnóstico congelado | `formularios.submissoes.resultado` | jsonb (`{faixa, titulo, mensagem, canais}`) |
| Link de resultado | calculado | URL assinada com `submissao.id` + validade |
| Anexos | service Media | `media_id` em `respostas` |

### 6.4 Permissões e ativação

```php
final class ModuleProvider implements ModuleProviderContract
{
    public function register(): void
    {
    }

    public function config(): array
    {
        return [
            'title' => 'formularios::menu.title',
            'icon' => 'lucide-clipboard-list',
            'position' => 60,
            'route' => 'cms.formularios.index',
            'permissions' => [Permission::VIEW, Permission::EDIT, Permission::APPROVE, Permission::ARCHIVE],
        ];
    }
}
```

Exemplo ilustrativo, no formato de `src/modules/Portal/Lgpd/ModuleProvider.php`. A ativação é dado
(`core:module:activate Formularios`), com guard de runtime (ADR-034).

## 7. Critérios de aceite do aproveitamento

1. Nenhuma tabela `v2_form*` é criada no V7; as 4 tabelas do motor vivem no schema `formularios`.
2. Uma submissão antiga continua legível depois de o formulário ser editado; teste prova que
   `versao_id` não muda.
3. O portal não devolve `pontuacao`/`faixa` em nenhuma resposta; teste de conteúdo garante.
4. `ip_hash` não contém o IP e usa pepper versionado; teste com dois peppers gera hashes distintos.
5. O link de resultado expira e não é reutilizável para outra submissão.
6. Relatório de 10.000 submissões responde sem carregar todas as linhas em memória.
7. Exportação CSV é streaming, escapa fórmulas e é auditada.
8. Toda string de interface/erro vem de `Lang/{locale}`; nenhum texto solto em Blade/controller.
9. Permissões `formularios.{ver,editar,aprovar,arquivar}` declaradas e testadas nas quatro ações.
10. Nenhum `<script>`/`style` inline; editor de faixas roda como módulo Vite.
11. Formulário com dado pessoal é classificado e coberto por `retention:expunge`.
12. `ModuleProvider` não expõe rota pública; o portal pertence ao módulo, o motor não (ADR-015).
