# Formulários Dinâmicos — Rotas e Controladores

> **Slug:** `infraestrutura-formulariosdinamicos` · **Versão analisada:** `lotep`
> **Raiz de referência:** `/home/one/projects/maxima/v7/lotep`

---

## 1. Superfícies

| Superfície | Prefixo | Arquivo de rotas | Middleware base |
|---|---|---|---|
| CMS do motor | `/cms/v2/formulario-dinamico` | `V2/Services/FormularioDinamico/Routes/cms.php` | `v2.authenticate`, `v2.permission:formulario-dinamico,read` |
| Portal do motor | `/formularios` | `V2/Services/FormularioDinamico/Routes/web.php` | global (sem auth) |
| API do motor | `/api/v2/formulariodinamico` | `V2/Services/FormularioDinamico/Routes/api.php` | `throttle.api` / `throttle:10,1` |
| CMS legado | `/cms/v2/formularios` | `V2/Modules/Infraestrutura/FormulariosDinamicos/Routes/cms.php` | `v2.authenticate`, `v2.permission:formularios,read` |
| Portal legado | `/formulario` | `V2/Modules/Infraestrutura/FormulariosDinamicos/Routes/web.php` | global |

O prefixo do serviço é derivado do nome do diretório (minúsculo): `FormularioDinamico` →
`formulariodinamico` (`V2/Core/Providers/V2ServiceProvider.php:383` e o bloco de
`mapServiceRoutes`). Já o prefixo do CMS (`cms/v2/formulario-dinamico`) é declarado manualmente.

## 2. Rotas do CMS (motor)

| Método | URI | Nome | Controller@action | Middleware adicional |
|---|---|---|---|---|
| GET | `/cms/v2/formulario-dinamico` | `v2.formulario-dinamico.cms.index` | `Admin\FormularioDinamicoCmsController@index` | — |
| GET | `/cms/v2/formulario-dinamico/create` | `v2.formulario-dinamico.cms.create` | `...@create` | `v2.permission:...,write` |
| POST | `/cms/v2/formulario-dinamico` | `v2.formulario-dinamico.cms.store` | `...@store` | `...,write` |
| GET | `/cms/v2/formulario-dinamico/{slug}/edit` | `v2.formulario-dinamico.cms.edit` | `...@edit` | `...,write` |
| POST | `/cms/v2/formulario-dinamico/{slug}` | `v2.formulario-dinamico.cms.update` | `...@update` | `...,write` |
| POST | `/cms/v2/formulario-dinamico/{slug}/delete` | `v2.formulario-dinamico.cms.destroy` | `...@destroy` | `...,write` |
| GET | `/cms/v2/formulario-dinamico/{slug}/relatorios` | `v2.formulario-dinamico.cms.relatorios` | `...@relatorios` | — |
| GET | `/cms/v2/formulario-dinamico/{slug}/submissoes` | `v2.formulario-dinamico.cms.submissoes` | `...@submissoes` | — |
| GET | `/cms/v2/formulario-dinamico/{slug}/submissoes/{id}` | `v2.formulario-dinamico.cms.show` | `...@show` | `where('id', '[0-9a-f\-]{36}')` |
| GET | `/cms/v2/formulario-dinamico/{slug}/submissoes/export` | `v2.formulario-dinamico.cms.export` | `...@export` | `...,write` |

Fonte: `V2/Services/FormularioDinamico/Routes/cms.php:7-37`.

Todas as actions repetem a checagem `if (!\v2_can('formulario-dinamico', 'read'|'write')) abort(403)`
no início (`FormularioDinamicoCmsController.php:55, 83, 100, 165, 193, 224, 243, 287, 315`). A
duplicação é intencional no legado: protege o controller mesmo quando chamado diretamente (os
testes usam chamada direta).

**Escrita de submissão não existe no CMS**: não há rota para editar/apagar submissão. `export`
exige `write`; `show`, `submissoes` e `relatorios` exigem `read`.

## 3. Rotas do portal (motor)

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/formularios/{slug}` | `v2.formulario-dinamico.portal.show` | `Portal\FormularioDinamicoPortalController@show` | — |
| POST | `/formularios/{slug}` | `v2.formulario-dinamico.portal.store` | `...@store` | `throttle:10,1` |
| GET | `/formularios/{slug}/resultado/{token}` | `v2.formulario-dinamico.portal.resultado` | `...@resultado` | — |

Fonte: `V2/Services/FormularioDinamico/Routes/web.php:5-11`.

O portal não usa `cache.private` nem `auth`; depende do middleware global. O `throttle:10,1` é o
único freio de submissão — por IP, 10 requisições por minuto.

## 4. Rotas da API (motor)

| Método | URI | Nome | Controller@action | Middleware |
|---|---|---|---|---|
| GET | `/api/v2/formulariodinamico/{slug}` | `v2.formulario-dinamico.api.schema` | `Api\FormularioDinamicoApiController@schema` | `throttle.api` |
| POST | `/api/v2/formulariodinamico/{slug}` | `v2.formulario-dinamico.api.submit` | `...@submit` | `throttle:10,1` |
| GET | `/api/v2/formulariodinamico/resultados/{slug}` | `v2.formulario-dinamico.api.resultados` | `...@resultados` | `throttle.api` |

Fonte: `V2/Services/FormularioDinamico/Routes/api.php:5-13`.

A ordem de registro não cria conflito: `/{slug}` casa com **um** segmento, enquanto
`/resultados/{slug}` exige dois. Um formulário cujo slug seja `resultados` continua acessível em
`GET /api/v2/formulariodinamico/resultados` (schema); o agregado usa
`GET /api/v2/formulariodinamico/resultados/{slug}`.

## 5. Rotas do módulo legado

### 5.1 CMS

| Método | URI | Nome | Controller@action |
|---|---|---|---|
| GET | `/cms/v2/formularios` | `v2.formularios.forms.index` | `Admin\FormCmsController@index` |
| GET | `/cms/v2/formularios/create` | `v2.formularios.forms.create` | `...@create` |
| POST | `/cms/v2/formularios` | `v2.formularios.forms.store` | `...@store` |
| GET | `/cms/v2/formularios/{id}/edit` | `v2.formularios.forms.edit` | `...@edit` |
| POST | `/cms/v2/formularios/{id}` | `v2.formularios.forms.update` | `...@update` |
| POST | `/cms/v2/formularios/{id}/delete` | `v2.formularios.forms.destroy` | `...@destroy` |

Fonte: `V2/Modules/Infraestrutura/FormulariosDinamicos/Routes/cms.php:5-11`.

Comportamento efetivo:

- `index`, `create` e `edit` **redirecionam** para `v2.formulario-dinamico.cms.index` com
  `session('info')`.
- `store` ainda cria `V2Form` e audita (`FormCmsController.php:52-66`), mas nenhuma tela envia
  para essa rota.
- `update` e `destroy` ainda alteram `v2_forms`; `destroy` é soft delete.

### 5.2 Portal

| Método | URI | Nome | Controller@action |
|---|---|---|---|
| GET | `/formulario/{slug}` | `v2.formularios.portal.show` | `Portal\FormPortalController@show` |
| POST | `/formulario/{slug}/submit` | `v2.formularios.portal.submit` | `...@submit` |

`show` redireciona para `v2.formulario-dinamico.portal.show`. `submit` **continua gravando** uma
submissão legada em `v2_form_submissions` com `answers = $r->except('_token')` e `ip = $r->ip()`
(`FormPortalController.php:28-32`).

## 6. Permissões

### 6.1 Vocabulário do legado

- Chave de módulo do motor: `formulario-dinamico`.
- Chave do módulo legado: `formularios`.
- `PermissionService` mapeia **as duas** para o mesmo módulo canônico `FormulariosDinamicos`
  (`V2/Core/Services/PermissionService.php:29-30`), então a matriz de permissões tem uma linha só.
- Níveis: `none`, `read`, `write` (`PermissionService::hasPermission` compara o nível armazenado em
  `v2_roles.permissions_v2` jsonb com a ação pedida; `write` implica leitura).
- Bypass: e-mail em `config('v2.support.email')` e perfil `super-admin`.

### 6.2 Efeito prático

Um perfil com `formularios = read` acessa CMS, relatórios e submissões do motor, mas não cria,
edita nem exporta. `write` libera create/edit/delete/export. O portal e a API são públicos.

O `ModuleProvider` legado aponta o menu para `v2.formularios.forms.index`, que redireciona para o
motor — logo o item "Formulários Dinâmicos" do menu é íntegro, mas o `active_pattern =
'cms/v2/formularios*'` não casa mais com a URL real (`cms/v2/formulario-dinamico*`).

## 7. Validação de entrada

### 7.1 CMS (motor) — sem Form Request

`validateFormData()` (`FormularioDinamicoCmsController.php:344-355`) normaliza:

| Campo | Regra efetiva |
|---|---|
| `slug` | trim de string; sem validação de formato/obrigatoriedade (`required` só no HTML) |
| `title` | trim de string; idem |
| `description` | trim; vazio vira `null` no serviço |
| `status` | allowlist `draft|active|archived`; inválido vira `draft` |
| `score_enabled` | cast booleano |

Campos `campos[]` são extraídos com `strInput()` (array vira string vazia) e normalizados em
`FormularioCrudService::syncCampos()`: `name` vazio é descartado, `type` vazio vira `text`,
`order` vazio usa a posição, `weight` vazio vira `0`.

**Não há validação de `name` como identificador único no controller** — a unicidade é garantida
pela constraint `(versao_id, name)`. Nomes duplicados no mesmo payload fazem o `INSERT` estourar
`QueryException`, capturada como erro de slug (`store`/`update` mapeiam qualquer `QueryException`
para `slug`).

### 7.2 API (motor)

| Aspecto | Regra |
|---|---|
| `answers` | deve ser array; se não for, vira `[]` (`FormularioDinamicoApiController.php:84-86`) |
| `consent` | `(bool) $request->input('consent', false)` |
| campos desconhecidos | ignorados na validação e na normalização |
| erros | `ValidationException` → 422 `{message, errors}` |
| sucesso | 201 `{message, id, score, max_score, result_level, result_token}` |

### 7.3 Portal (motor)

`answers` idem; consentimento vem de `lgpd_consent`; erros voltam via `back()->withInput()`
(redirect), com log `formulario_dinamico.validation_failed`
(`FormularioDinamicoPortalController.php:78-103`).

### 7.4 Legado

`DynamicFormRequest` (`V2/Modules/Infraestrutura/FormulariosDinamicos/Requests/DynamicFormRequest.php`):

```php
'authorize' => v2_can('formularios', 'write')
'title' => 'required|string|max:255'
'slug' => 'nullable|string|max:255|unique:v2_forms,slug,' . $id . ',id'
'description' => 'nullable|string'
'is_active' => 'boolean'
'has_result' => 'boolean'
'hide_title' => 'boolean'
'submit_info' => 'nullable|string'
'published_at' => 'nullable|date'
```

O `authorize()` duplica a checagem do controller; `rules()` aceita `id` objeto (route model) ou
string vazia/`'0'` como `NULL` para o `unique`.

## 8. Presenters e saída

- **Portal:** view `v2-service-formulariodinamico::portal.form` (corpo HTML já renderizado em
  `$formBody`) e `portal.result` (título/mensagem, `retakeUrl`).
- **CMS:** views Blade com dados Eloquent; o controller não usa API Resource.
- **API:** arrays montados à mão; `schema` omite `weight`, `options.value` vai como está e
  `consent_required` tem default `true`.
- **CSV:** `ExportService::toCsv` — cabeçalho fixo
  `id,data,score,max_score,result_level,ip_hash,origin,answers`, BOM UTF-8, `answers` como JSON
  serializado. Não inclui `user_agent`, `session_id`, `result_token` nem `consent_at`.

## 9. Respostas de erro

| Situação | Resultado |
|---|---|
| Formulário inexistente/inativo/expirado | portal `abort(404)`; API 404 `{message: "Formulário não encontrado."}` |
| Versão ativa ausente | portal `abort(404)`; API 404 `{message: "Formulário sem versão ativa."}` |
| Token de resultado de outro formulário | `abort(404)` (`FormularioDinamicoPortalController.php:136-139`) |
| Consentimento ausente | 422 com chave `lgpd_consent` (API) / redirect com erro (portal) |
| Sem permissão no CMS | `abort(403)` |
| `scoring_config_json` inválido | volta com erro `config_json` |
| Colisão de slug | volta com erro `slug` |
