# Formulários Dinâmicos — Resumo Executivo

> **Versão analisada:** `lotep` (Laravel 5.5.50 / PHP 7.4) — camada modular `V2/`
> **Slug do dossiê:** `infraestrutura-formulariosdinamicos`
> **Natureza:** infraestrutura — motor de formulários versionados + módulo legado morto
> **Estado:** **dois caminhos no mesmo repositório**: um módulo legado **sem telas** que apenas redireciona/grava, e o **motor novo** em `V2/Services/FormularioDinamico`, implementado, testado e em uso (AutoTeste, portal, API)
> **Raiz de referência dos caminhos:** `/home/one/projects/maxima/v7/lotep`

---

## 1. Objetivo

O par Formulários Dinâmicos entrega duas coisas distintas:

1. **Um motor genérico** (`V2/Services/FormularioDinamico`) onde **qualquer módulo** descreve um
   formulário por configuração — campos tipados, pesos, opções, faixas de resultado e textos de
   diagnóstico — e recebe renderização, validação, pontuação, diagnóstico, persistência anônima,
   exportação CSV e agregações **sem reimplementar nada**.
2. **Um módulo legado** (`V2/Modules/Infraestrutura/FormulariosDinamicos`) que existia para manter
   formulários em quatro tabelas (`v2_forms`, `v2_form_categories`, `v2_form_questions`,
   `v2_form_submissions`) e foi **substituído** pelo motor. Hoje suas telas de CMS e portal são
   apenas redirecionamentos; métodos de escrita (`store`/`update`/`destroy` e o `submit` do portal)
   continuam existindo, mas não têm mais tela que os alcance.

O consumidor real do motor é o **AutoTeste Jogo Responsável**
(`V2/Modules/Servicos/AutoTesteJogoResponsavel`), que cria um instrumento de 13 perguntas com
escala 0–3 e faixas percentuais (0/31/65/100). O portal genérico atende
`/formularios/{slug}`, e a API pública atende `/api/v2/formulariodinamico/{slug}`.

## 2. Efeito / Impacto

| Faixa | Efeito |
|---|---|
| Cidadão | Responde formulários ativos por URL canônica, de forma **anônima por padrão**; após enviar, vê uma confirmação sem pontuação nem nome de faixa. |
| Prefeitura (CMS) | Cria e edita formulários (título, slug, perguntas, notas por opção, faixas de resultado), consulta submissões, relatórios e exporta CSV. |
| Transparência e legalidade | `ip_hash` HMAC-SHA256 em vez de IP cru, `consent_at` gravado, histórico de submissões nunca reescrito por edição do formulário (versões imutáveis). |
| Operação técnica | Um motor único para qualquer questionário; porém o índice GIN de `answers` não é consultado por nenhum código, e os relatórios carregam todas as respostas em memória. |

## 3. Comportamento

- **Versão imutável por formulário.** `v2_formulario_dinamico_formularios.current_version` aponta
  para uma linha de `v2_formulario_dinamico_versoes`; cada versão tem `schema` e `scoring_config`
  jsonb próprios. Editar um formulário **sem** submissões altera a versão corrente no lugar; com
  **pelo menos uma** submissão, cria a versão `v+1` e avança `current_version`
  (`FormularioCrudService.php:146-186`).
- **Os campos vivem por versão**, não em `schema`: `v2_formulario_dinamico_campos` guarda `type`,
  `name`, `label`, `help`, `required`, `order`, `weight`, `options` jsonb e `rules` jsonb. O
  `schema` jsonb da versão recebe apenas `{"campos": N}` — é um **stub**, não a estrutura.
- **Slug é único globalmente.** Um formulário ativo bloqueia o slug; um formulário soft-deleted é
  renomeado para `slug--<8 hex>` no momento de reutilização e o histórico é preservado
  (`FormularioDinamicoCmsController.php:112-133`).
- **Consentimento LGPD é exigido por padrão.** `scoring_config.consent_required` ausente equivale
  a `true`; sem `consent` a submissão falha com erro `lgpd_consent`
  (`SubmissionService.php:179-186`).
- **Pontuação por pergunta.** `weight` do campo é o valor da pergunta; opções com `weight > 0`
  habilitam o modo avançado (soma dos pesos escolhidos); `range` é proporcional ao máximo; sem
  faixas não há diagnóstico e a submissão fica sem `result_level`/`result_data`.
- **Diagnóstico por faixas.** `bands` com `max_percent` usa estratégia percentual (percentual
  truncado a 4 casas, `floor`, para não "subir" de faixa no teto); `bands` com `max_points` usa
  estratégia absoluta. Se houver qualquer faixa absoluta, a estratégia absoluta é escolhida, e
  ela ignora as faixas percentuais (`DiagnosisService.php:29-65`).
- **Submissão anônima:** `answers` jsonb, `score`, `max_score`, `result_level`, `result_data`
  (faixa/textos/canais congelados), `ip_hash`, `user_agent`, `session_id`, `origin`,
  `result_token`, `consent_at`.
- **Portal não revela diagnóstico:** a tela de resultado mostra sempre agradecimento; apenas uma
  "mensagem ao usuário" da faixa, quando configurada, é exibida
  (`FormularioDinamicoPortalController.php:111-152`).
- **CMS mostra o diagnóstico:** lista de submissões com percentual e faixa, detalhe da submissão,
  relatório com donut por pergunta e barras de pontuação média.

## 4. Como o Usuário Interage

**Operador do CMS (`/cms/v2/formulario-dinamico`, permissão `formulario-dinamico`).**

- `index`: busca por título/slug, ordenação, contagem de submissões, ações (editar, relatórios,
  submissões, ver no portal, remover com confirmação).
- `create`/`edit`: dados do formulário + **editor de perguntas** (três tipos oferecidos:
  Alternativa, Dissertativa, Múltipla escolha), com nota 0–100 por opção, mais o **editor de
  faixas** (linha do tempo 0–100%, nome, cor, mensagem, reordenação).
- `relatorios`: total, score médio, dias com submissões (30), distribuição percentual por pergunta
  e pontuação média por pergunta.
- `submissoes`: filtro por faixa, paginação de 20, exportação CSV.
- `show`: resumo (data, score, faixa, origem, consentimento, hash do IP) e respostas cruas.

**Cidadão (portal).** `GET /formularios/{slug}` monta o formulário a partir da versão ativa;
`POST` valida, pontua, diagnostica, grava e redireciona para
`/formularios/{slug}/resultado/{token}`. Erros voltam com `aria-invalid` e resumo no topo.

**API.** `GET /api/v2/formulariodinamico/{slug}` devolve o schema público (sem `weight`);
`POST` devolve 201 com `id`, `score`, `max_score`, `result_level`, `result_token`, ou 422 com
`errors`; `GET /resultados/{slug}` devolve o agregado.

**Módulo legado.** `GET /cms/v2/formularios` e `/cms/v2/formularios/create` redirecionam para o
motor com mensagem informativa. `/formulario/{slug}` redireciona para `/formularios/{slug}`.
Restam métodos de escrita sem UI (`store`, `update`, `destroy`, `submit`).

## 5. Detalhes e Particularidades Técnicas

- Camadas: rota → `v2.authenticate` + `v2.permission:*` (CMS) ou `throttle` (portal/API) →
  controller → `FormularioDinamicoService` (fachada) → `ValidationService` → `SubmissionService`
  → `ScoringService`/`DiagnosisService` → model → view ou JSON.
- Serviço auto-descoberto por `V2ServiceProvider.php:165-193`: migrations, views
  (`v2-service-formulariodinamico::`), rotas `Routes/{web,cms,api}.php`.
- Provider registra singletons e resolve `SubmissionRepositoryInterface → SubmissionService`
  (`FormularioDinamicoServiceProvider.php:29-87`).
- `RendererService` devolve **string HTML** por tipo; cada `FieldType` renderiza uma view Blade.
  Não há sanitização central: o escape depende do `{{ }}` de cada componente.
- Tipos de campo: existem **9 classes** em `FieldTypes/`, mas o config registra **8** — `file`
  ficou fora de `config/formulario-dinamico.php`, não aparece no editor e o importador legado
  converte arquivos para `text`.
- `ip_hash` = `hash_hmac('sha256', $ip, config('app.key'))` (`SubmissionService.php:203-209`).
- `result_token` = `v2_uuid()` (v4), coluna unique; o comentário da migration diz "URL assinada",
  mas **não há assinatura, expiração nem verificação criptográfica**.
- `distribution` e `questionScores` executam `select('answers')->get()` e percorrem todas as
  submissões no PHP, campo a campo (`ExportService.php:102-232`).
- O motor não usa i18n: mensagens de validação, rótulos do editor, CSV e views são strings fixas
  em português.
- O módulo legado contém a migration
  `2026_07_17_000004_create_v2_form_submissions_table.php` com espaçamento degenerado
  (`Blueprint$t`, `\Exception$e`, `throw$e`). **A sintaxe é válida** (confirmado com `php -l`); é sinal
  de arquivo editado fora do padrão, não defeito de execução.

## 6. Estrutura de Dados

**Motor (4 tabelas):** `v2_formulario_dinamico_formularios` (slug único, status
`draft|active|archived`, `current_version`, `published_at`, `expires_at`, `metadata` jsonb),
`v2_formulario_dinamico_versoes` (`formulario_id`, `version`, `schema` jsonb, `scoring_config`
jsonb, unique `[formulario_id, version]`), `v2_formulario_dinamico_campos` (`versao_id`, `type`,
`name`, `label`, `help`, `required`, `order`, `weight`, `options` jsonb, `rules` jsonb, unique
`[versao_id, name]`) e `v2_formulario_dinamico_submissoes` (`formulario_id`, `versao_id`, `answers`
jsonb, `score`, `max_score`, `result_level`, `result_data` jsonb, `ip_hash`, `user_agent`,
`session_id`, `origin`, `result_token` unique, `consent_at`).

Índice relevante: GIN `v2_fdd_submissoes_answers_gin_idx` em `answers` com `jsonb_path_ops`
(`V2/Services/FormularioDinamico/Migrations/2026_09_28_000004_create_v2_formulario_dinamico_submissoes_table.php:41`). Índices de apoio: `[formulario_id, versao_id]`,
`[formulario_id, created_at]`, `[formulario_id, result_level]`.

**Rastreio de importação:** `v1_id` (integer) + `imported_at` em formulários e submissões, com
unique parcial `WHERE v1_id IS NOT NULL` (`V2/Services/FormularioDinamico/Migrations/2026_10_22_000001_add_v1_import_tracking_to_v2_formulario_dinamico_tables.php:33-75`).

**Legado (4 tabelas):** `v2_forms`, `v2_form_categories`, `v2_form_questions`,
`v2_form_submissions` — mantidas no banco, sem uso de tela. O `submit` legado ainda grava
`v2_form_submissions` com **IP cru** em `ip`.

## 7. Como Testar

- **Unitários** — `tests/Unit/V2/Services/FormularioDinamico/`: `FieldTypesTest` (normalização e
  limites por tipo), `ValidationServiceTest`, `ScoringServiceTest` (13 itens × 0–3 = 39),
  `DiagnosisServiceTest` (fronteiras 0/31/65/100 e faixas absolutas), `ExportServiceTest` (CSV,
  agregados, distribuição, médias), `RendererServiceTest` (fieldset/legend, `aria-invalid`,
  escape simples), `SubmissionServiceTest` (anonimização, consentimento, sem faixas),
  `FormularioCrudServiceTest` (versão no lugar × bump, opções, JSON inválido).
- **Feature** — `tests/Feature/V2/Services/FormularioDinamico/FormularioDinamicoApiTest.php`
  (schema 200/404, submit 201/422, agregado) e `FormularioSlugReusoTest.php` (slug apagado
  reutilizável, slug ativo bloqueado, faixas ordenadas).
- **E2E** — `tests-e2e/tests/cms-v2-formulario-dinamico-crud.spec.js` e
  `tests-e2e/tests/portal-v2-autoteste.spec.js`.
- **Comando** (a partir da raiz `lotep/`):
  `vendor/bin/phpunit tests/Unit/V2/Services/FormularioDinamico tests/Feature/V2/Services/FormularioDinamico`.

## 8. Migração para Laravel Moderno

No V7 não existe módulo equivalente. O aproveitamento é o **motor**, não as tabelas nem as telas.
O dossiê `09-APROVEITAMENTO-V7.md` fixa: portar o modelo de versão imutável + campos por versão +
submissão anônima + scoring/faixas como **service headless** (`src/services/...`) e um **módulo
CMS/portal** que o consome; adaptar renderização para componentes Blade sem HTML em string,
relatórios para SQL agregado, e o token para URL assinada (`URL::temporarySignedRoute`); rejeitar
o `schema` stub, o tipo `file` órfão, os scripts inline (CSP), o HTML em `consent_text`, as
tabelas legadas e o módulo legado.

## 9. Resumo Geral

Formulários Dinâmicos no `lotep` são hoje **um motor em `V2/Services/FormularioDinamico`** com um
**módulo legado morto** ao lado. O motor modela formulários como configuração versionada: um
formulário tem N versões imutáveis, cada versão tem campos com tipo/peso/opções/regras, e cada
submissão referencia a versão exata que respondeu — logo, editar um formulário nunca reescreve o
passado. A submissão passa por consentimento LGPD, validação por tipo, normalização, pontuação e
classificação por faixas (percentuais ou absolutas), e é gravada anonimamente com `ip_hash`
HMAC-SHA256 e `result_token` aleatório. O CMS expõe CRUD, editor de perguntas/faixas, relatórios e
CSV; o portal mostra o formulário e uma confirmação sem diagnóstico; a API entrega schema, submissão
e agregados. Os pontos frágeis são conhecidos: o `schema` jsonb é decorativo, o GIN de `answers` não
é usado, `result_token` não é assinado apesar do comentário, relatórios carregam tudo em memória,
não há i18n e o módulo legado tem uma migration com formatação degenerada (mas sintaxe válida) e
escrita residual com IP cru. A reconstrução no V7 deve portar o **modelo mental** (versões + campos + submissão anônima +
scoring/faixas) e descartar as tabelas e telas legadas.

## 10. Mapa do Dossiê

| Arquivo | Conteúdo |
|---|---|
| `01-ARQUITETURA.md` | fluxo, classes, provider, dependências, dois caminhos |
| `02-MODELAGEM.md` | 8 tabelas, índices, migrations, importador, seed legado |
| `03-ROTAS-E-CONTROLADORES.md` | rotas CMS/portal/API/legado, permissões, validação |
| `04-VIEWS-E-UX.md` | telas do motor, componentes, exportação, acessibilidade |
| `05-REGRAS-E-PORTABILIDADE.md` | versionamento, scoring, faixas, LGPD, acoplamentos e armadilhas |
| `06-MIGRACAO-MODERNA.md` | schema/migration, models, enums, actions, contratos, requests e permissões |
| `07-TESTES.md` | casos existentes e plano de testes da reconstrução |
| `08-GAPS-E-DIVIDAS.md` | bugs, código morto, mapa entre versões, riscos |
| `09-APROVEITAMENTO-V7.md` | portar/adaptar/rejeitar com ADRs do V7 |
| `10-INTERFACES-E-OPERACAO.md` | endpoints, resources, views, fila/cache/storage e riscos da migração |
