# Formulários Dinâmicos — Interfaces, Operação e Riscos da Migração

> **Slug:** `infraestrutura-formulariosdinamicos` · **Versão analisada:** `lotep`
> **Complemento de:** `06-MIGRACAO-MODERNA.md` (schema, models, enums, actions e contratos)
> **Raiz de referência:** `/home/one/projects/maxima/v7/lotep`

---

## 1. Endpoints sugeridos

### Portal (módulo `Formularios`, `Routes/portal.php`)

| Método | URI | Nome |
|---|---|---|
| GET | `/formularios/{slug}` | `portal.formularios.show` |
| POST | `/formularios/{slug}` | `portal.formularios.store` |
| GET | `/formularios/{slug}/resultado/{submissao}` (`signed`) | `portal.formularios.resultado` |

A GET e a POST usam `cache.private` (formulário tem CSRF); a POST usa `throttle:formularios` com
`RateLimiter::for` (ex.: 10/min por IP), seguindo a ADR-021.

### CMS (`Routes/cms.php`)

| Método | URI | Nome | Permissão |
|---|---|---|---|
| GET | `/cms/formularios` | `cms.formularios.index` | `formularios.ver` |
| GET | `/cms/formularios/novo` | `cms.formularios.create` | `formularios.editar` |
| POST | `/cms/formularios` | `cms.formularios.store` | `formularios.editar` |
| GET | `/cms/formularios/{formulario}/editar` | `cms.formularios.edit` | `formularios.editar` |
| PUT | `/cms/formularios/{formulario}` | `cms.formularios.update` | `formularios.editar` |
| POST | `/cms/formularios/{formulario}/publicar` | `cms.formularios.publish` | `formularios.aprovar` |
| POST | `/cms/formularios/{formulario}/arquivar` | `cms.formularios.archive` | `formularios.arquivar` |
| GET | `/cms/formularios/{formulario}/relatorios` | `cms.formularios.reports` | `formularios.ver` |
| GET | `/cms/formularios/{formulario}/submissoes` | `cms.formularios.submissions` | `formularios.ver` |
| GET | `/cms/formularios/{formulario}/submissoes/{submissao}` | `cms.formularios.submission` | `formularios.ver` |
| GET | `/cms/formularios/{formulario}/submissoes.csv` | `cms.formularios.export` | `formularios.ver` |

### API (`Routes/api.php`)

| Método | URI | Nome |
|---|---|---|
| GET | `/api/formularios/{slug}` | `api.formularios.schema` |
| POST | `/api/formularios/{slug}` | `api.formularios.submit` |
| GET | `/api/formularios/{formulario}/resultados` | `api.formularios.results` |

Rotas sem closure (ADR-008), controllers invocáveis, nomes com token `formularios` (ADR-004).

## 2. API Resources

`schema`: `{slug, titulo, descricao, versao, consentimento_obrigatorio, campos[]}` — campos com
`tipo`, `nome`, `rotulo`, `ajuda`, `obrigatorio`, `ordem`, `opcoes` e **sem `peso`** (peso é
controle interno; expor permite engenharia reversa da pontuação).

`submissao`: `{id, pontuacao, pontuacao_maxima, resultado_url}`. O legado devolvia
`result_level`; no V7 o portal não revela faixa — a API de submissão também não deve devolver
`faixa`, apenas o link assinado. Se um consumidor de módulo precisar do diagnóstico, há um endpoint
interno autenticado ou a action retorna o DTO.

`resultados`: `{total, media_pontuacao, por_faixa, serie_diaria}` — sempre agregado, nunca
submissão individual.

## 3. Views Blade modernas

- Componentes anônimos por tipo: `<x-formularios::campo-texto>`, `campo-escolha`,
  `campo-multipla`, `campo-faixa`, `campo-satisfacao`, `campo-arquivo`.
- O service expõe um **renderer** que devolve um array de DTOs de campo; o módulo itera com
  `@foreach` e `<x-dynamic-component>`. **Nada de string HTML** atravessando camadas.
- Faixas/relatórios: componentes com `<progress>`/SVG server-side; JS só para a barra interativa do
  editor, em módulo Vite (`resources/js/formularios-editor.js`), sem `onclick`/`<script>` inline.
- Layout do portal: shell V7 (`layout`), com `h1`, breadcrumb e `<form method="post">` do CMS
  (`@csrf`, `@method`).
- Strings via `__('formularios::...')`; conteúdo do formulário (título, rótulos, mensagens) em
  jsonb multilíngue quando o módulo exigir — usar `LocaleCast` (ADR-012).

## 4. Fila, cache e storage

| Item | Decisão |
|---|---|
| Cache de página | formulário é `cache.private` (CSRF, ADR-022); página de resultado idem |
| Cache de schema | opcional: `cache()->remember("formularios:schema:{$slug}", 300, ...)`, invalidado ao publicar versão |
| Fila | exportação CSV acima de um limiar (ex.: 5.000 linhas) vira job + download assinado; submissão é síncrona |
| Storage | anexos usam o service Media (ADR-026/030); a resposta guarda `media_id`, nunca o binário |
| Auditoria | `AuditWriter` em criar/editar/publicar/arquivar/exportar e em leitura de submissão (ADR-035) |
| Telemetria | span `formularios.submissao` com `formulario`, `versao`, `origem`, `duracao_ms`; sem PII (ADR-024) |
| Retenção | `RetentionTarget` para submissões expiradas: anonimiza `respostas`/`user_agent`/`ip_hash` e mantém agregados (ADR-019) |

## 5. Riscos e decisões de equivalência

| Tema | O que não pode mudar | Como o V7 cumpre |
|---|---|---|
| Histórico | submissão continua ligada à versão exata | FK `versao_id` imutável; edição só sem submissões |
| Diagnóstico | faixa calculada com as mesmas fronteiras e truncamento | manter `floor` na estratégia percentual; teste de fronteira obrigatório |
| Portal | não revelar pontuação/faixa | resource só com link; teste garantindo ausência de `pontuacao`/`faixa` |
| LGPD | sem IP cru; consentimento registrado | `ip_hash` com pepper versionado; `consentimento_em` |
| Slug | reutilização de slug apagado | renomear o antigo para `slug--{hash}` e manter histórico |
| Sem faixas | sem diagnóstico | `resultado = null`, confirmação padrão |
| Migração de dados | `answers` da V1 pode conter PII (ex.: "Qual seu nome?") | inventariar antes; anonimizar campos pessoais no importador ou marcar o formulário como legado |

**Riscos assumidos:**

- `respostas` jsonb pode conter texto livre com dado pessoal. O V7 deve classificar o formulário
  (`metadados.classificacao = publico|interno|pessoal|sensivel`) e aplicar retenção/cifra quando
  pessoal. Não há como decidir isso por campo sem intervenção editorial.
- A agregação em SQL precisa de `jsonb_each`/`jsonb_array_elements`; a query deve ser testada em
  PG 18 e limitada por `formulario_id`. Evitar agregar em PHP como o legado.
- A assinatura de URL usa `APP_KEY`; rotacionar a chave invalida links antigos — documentar que o
  link de resultado é efêmero (30 dias) e o dado permanece.
- O editor de faixas é a parte mais interativa do módulo: se o JS não for reescrito como módulo,
  a tentação de copiar o script inline quebra o CSP (ADR-022) e o ETag.
