# Formulários Dinâmicos — Arquitetura

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

---

## 1. Visão em dois caminhos

O diretório do dossiê cobre dois artefatos que compartilham o nome e **não** compartilham o motor:

| Caminho | Estado | Papel |
|---|---|---|
| `V2/Services/FormularioDinamico/**` | **vivo** | motor: versões, campos, submissões, scoring, diagnóstico, exportação, CMS, portal e API |
| `V2/Modules/Infraestrutura/FormulariosDinamicos/**` | **morto para o usuário** | port do builder legado: telas redirecionam; escrita residual em `v2_forms` |

O módulo legado é auto-descoberto como qualquer módulo (`V2/Core/Providers/V2ServiceProvider.php:85-127`),
carrega migrations e rotas, mas o `ModuleProvider` aponta para o motor:

```php
return ['title' => 'Formulários Dinâmicos','description' => 'Criação de formulários e questionários dinâmicos.','icon' => 'bi bi-ui-checks','position' => 16,'active_pattern' => 'cms/v2/formularios*','route' => 'v2.formularios.forms.index','group' => 'Infraestrutura'];
```

(`V2/Modules/Infraestrutura/FormulariosDinamicos/ModuleProvider.php:12`)

O serviço **não** tem `ModuleProvider`: ele é descoberto pelo bloco de `V2/Services` do
`V2ServiceProvider` (`V2/Core/Providers/V2ServiceProvider.php:165-193`), que carrega migrations,
views (`v2-service-formulariodinamico::`) e rotas por convenção.

## 2. Fluxo da requisição — motor

### 2.1 Portal (formulário público)

```
GET /formularios/{slug}
  → Routes/web.php (sem middleware além do global)
  → FormularioDinamicoPortalController@show
      findActive(slug)         [status=active e expires_at nulo/futuro]
      activeVersion(slug)      [versão current_version + campos]
      renderFormPage()
          service->renderForm(versao, old('answers'), erros da sessão)
              RendererService::renderForm
                  para cada campo: service->fieldType(type)->render(field, value)
                      FieldType::render -> view componente -> string HTML
          view('v2-service-formulariodinamico::portal.form')
```

### 2.2 Portal (submissão)

```
POST /formularios/{slug}   [throttle:10,1]
  → FormularioDinamicoPortalController@store
      findActive + activeVersion
      service->submit(answers, versao, {ip, user_agent, session_id, origin=portal, consent=lgpd_consent})
          SubmissionService::submit
              1. consentRequired(scoring_config) e empty(context.consent) → erro lgpd_consent
              2. ValidationService::validate (por campo, via FieldType)
              3. normalizeAnswers (via FieldType::normalize)
              4. ScoringService::scoreAnswers
              5. DiagnosisService::classify
              6. create(V2FormularioDinamicoSubmissao)
              7. V2AuditLogger::info + Log::info
      redirect → /formularios/{slug}/resultado/{result_token}
```

### 2.3 CMS

```
GET/POST /cms/v2/formulario-dinamico/...   [v2.authenticate + v2.permission:formulario-dinamico,{read|write}]
  → FormularioDinamicoCmsController
      index / create / store / edit / update / destroy / relatorios / submissoes / show / export
      store/update usam FormularioCrudService (create/update/destroy)
      relatorios usam FormularioDinamicoService (aggregate/distribution/questionScores)
      export usa FormularioDinamicoService::exportCsv
```

O CMS não usa Form Request: `validateFormData()` e `extractCampos()` normalizam o payload no
próprio controller (`FormularioDinamicoCmsController.php:344-403`). A autorização aparece duas
vezes — middleware de rota e checagem `v2_can()` no início de cada action.

### 2.4 API

```
GET  /api/v2/formulariodinamico/{slug}           [throttle.api]
POST /api/v2/formulariodinamico/{slug}           [throttle:10,1]
GET  /api/v2/formulariodinamico/resultados/{slug}[throttle.api]
  → FormularioDinamicoApiController@schema|submit|resultados
```

A API **não** monta HTML: `schema` serializa os campos sem `weight`; `submit` delega ao mesmo
`service->submit()` do portal e devolve JSON; `resultados` devolve
`{total, avg_score, by_level, daily}`.

## 3. Diagrama de classes (motor)

```
FormularioDinamicoService  (fachada)
├── RendererService ──────────► FieldTypeInterface (8 tipos registrados)
│                                   └── render() → Blade component → string
├── ValidationService ────────► FieldTypeInterface::validate()
├── SubmissionService ────────► ValidationService, ScoringService,
│                               DiagnosisService, V2AuditLogger
│     └── implementa SubmissionRepositoryInterface
├── ExportService ────────────► ScoringService
│     └── CSV, aggregate, distribution, questionScores
└── config('formulario-dinamico.field_types'|default_max_range|ip_hash_algo|retention_days)

FormularioCrudService ────────► V2AuditLogger
  └── create/update/destroy, resolveCurrentVersion, bumpVersion, syncCampos

DiagnosisService
├── PercentBandStrategy (max_percent)
└── AbsoluteBandStrategy (max_points)
```

## 4. Classes e papéis

| Classe | Caminho | Papel |
|---|---|---|
| `FormularioDinamicoService` | `V2/Services/FormularioDinamico/FormularioDinamicoService.php` | fachada: resolve tipos, busca ativo/versão, render, validate, submit, export, aggregate |
| `FormularioCrudService` | `.../Services/FormularioCrudService.php` | CRUD de metadados/versões/campos, regra de bump, auditoria |
| `ValidationService` | `.../Services/ValidationService.php` | regras efetivas por campo (`rules` + `required` + `options`) e validação |
| `SubmissionService` | `.../Services/SubmissionService.php` | consentimento, normalização, scoring, diagnóstico, hash, persistência, auditoria |
| `ScoringService` | `.../Services/ScoringService.php` | pontuação e máximo por campo |
| `DiagnosisService` | `.../Services/DiagnosisService.php` | escolha de estratégia e anexo de `disclaimer` |
| `PercentBandStrategy` | `.../Services/Strategies/PercentBandStrategy.php` | faixa por percentual truncado (`floor` a 4 casas) |
| `AbsoluteBandStrategy` | `.../Services/Strategies/AbsoluteBandStrategy.php` | faixa por pontuação bruta |
| `ExportService` | `.../Services/ExportService.php` | CSV (BOM UTF-8), agregado, distribuição, média por pergunta |
| `RendererService` | `.../Services/RendererService.php` | itera campos e concatena HTML; injeta `_errors` |
| `FormularioDinamicoCmsController` | `.../Controllers/Admin/...` | CMS do motor |
| `FormularioDinamicoPortalController` | `.../Controllers/Portal/...` | portal genérico |
| `FormularioDinamicoApiController` | `.../Controllers/Api/...` | schema/submit/resultados |
| `FormularioDinamicoServiceProvider` | `.../Providers/...` | singletons, contratos, views, guardrail de tipos |
| `V2FormularioDinamico` | `.../Models/V2FormularioDinamico.php` | formulário + `scopeActive` + relações |
| `V2FormularioDinamicoVersao` | `.../Models/V2FormularioDinamicoVersao.php` | versão imutável + campos/submissões |
| `V2FormularioDinamicoCampo` | `.../Models/V2FormularioDinamicoCampo.php` | campo com casts `options`/`rules` array e `weight` float |
| `V2FormularioDinamicoSubmissao` | `.../Models/V2FormularioDinamicoSubmissao.php` | submissão anônima com `answers`/`result_data` array |

## 5. Provider e container

`FormularioDinamicoServiceProvider::register()` (`FormularioDinamicoServiceProvider.php:29-87`):

- `mergeConfigFrom(__DIR__ . '/../../../../config/formulario-dinamico.php', 'formulario-dinamico')`
  — o arquivo fica em `lotep/config/formulario-dinamico.php`.
- singletons: `RendererService`, `ValidationService`, `ScoringService`, `DiagnosisService`,
  `SubmissionService`, `ExportService`, `FormularioDinamicoService`.
- binds: `DiagnosisStrategyInterface → PercentBandStrategy`,
  `SubmissionRepositoryInterface → SubmissionService`.

`boot()` carrega views do namespace `v2-service-formulariodinamico` e, **fora de produção**,
valida que toda classe em `field_types` existe. Não roda migration nem registra comando.

O provider está listado manualmente em `config/app.php:221` — é a exceção ao auto-registro de
services, que ocorre em `V2ServiceProvider.php:165-193` para migrations/views/rotas.

## 6. Dependências

- **Internas:** `V2\Core\Controllers\BaseController`, `V2\Core\Services\V2AuditLogger`,
  `V2\Core\Logging\CmsActionLogger`, helper `v2_can()`, helper `v2_uuid()`.
- **Framework:** Eloquent, `Illuminate\Validation\ValidationException`, Blade, `fputcsv`.
- **Banco:** PostgreSQL — `jsonb`, `GIN jsonb_path_ops`, `to_char`, `ilike`, `COALESCE`.
- **Externas:** nenhuma biblioteca de formulário, máscara, gráfico ou PDF. Gráficos do relatório
  são **SVG/CSS gerados em Blade**, sem Chart.js.
- **Consumidores:** `V2/Modules/Servicos/AutoTesteJogoResponsavel/Services/AutoTesteInstrumentoService.php`
  cria formulário/versão/campos programaticamente; `V2/Modules/Support/MigracaoLegada/Services/FormulariosDinamicosLegacyImporter.php`
  importa a V1.

## 7. Fluxo do módulo legado

```
GET /cms/v2/formularios              → FormCmsController@index    → redirect v2.formulario-dinamico.cms.index
GET /cms/v2/formularios/create       → FormCmsController@create   → redirect v2.formulario-dinamico.cms.index
POST /cms/v2/formularios             → FormCmsController@store    → V2Form::create + audit + redirect
GET /cms/v2/formularios/{id}/edit    → FormCmsController@edit     → redirect v2.formulario-dinamico.cms.index
POST /cms/v2/formularios/{id}        → FormCmsController@update   → V2Form::update + audit + redirect
POST /cms/v2/formularios/{id}/delete → FormCmsController@destroy  → soft delete + audit + redirect

GET  /formulario/{slug}              → FormPortalController@show  → redirect v2.formulario-dinamico.portal.show
POST /formulario/{slug}/submit       → FormPortalController@submit→ V2FormSubmission::create (IP cru)
```

O comentário do controller é explícito: *"O port antigo (Fase 2) não possui Views — o namespace
`v2-formularios::` nunca foi registrado"* (`FormCmsController.php:24-28`). A view namespace é
registrada apenas no teste, por `view()->addNamespace('v2-formularios', __DIR__.'/stubs/views')`.

## 8. Pontos de atenção arquiteturais

| Ponto | Evidência |
|---|---|
| Serviço e módulo têm o mesmo domínio e nomes diferentes (`formulario-dinamico` × `formularios`) | rotas e `PermissionService.php:29-30` mapeiam os dois aliases para `FormulariosDinamicos` |
| O `schema` jsonb não descreve o formulário | `FormularioCrudService.php:55` grava `['campos' => count($campos)]` |
| A versão é imutável apenas por convenção de escrita | `syncCampos` faz `forceDelete()` dos campos e recria (`FormularioCrudService.php:198`); nada no banco impede editar versão com submissões |
| Renderização é string, não composição de componentes | `RendererService::renderForm` concatena retornos de `FieldType::render` |
| Rotas do portal e do fallback V1 podem competir | `tests/Feature/V2/Services/FormularioDinamico/FormularioDinamicoApiTest.php:19-22` documenta que o fallback V1 precede rotas V2 de portal em `V2_IS_DEFAULT=true` |
| Módulo legado carrega migration com formatação degenerada (`Blueprint$t`), mas sintaxe válida | `V2/Modules/Infraestrutura/FormulariosDinamicos/Migrations/2026_07_17_000004_create_v2_form_submissions_table.php:17` |
