# 08 — Gaps, Bugs, Riscos e Comparativo v5 → v6

## 1. Bugs confirmados no código

### B1 — `required_if` aponta para um campo inexistente (`style` em vez de `ask_type_id`)

`v6/app/Http/Requests/Prefecture/DynamicForms/Questionnaire/SaveRequest.php`:

```php
'categories.*.asks.*.config.min' => ["required_if:categories.*.asks.*.style,$range", 'numeric'],
'categories.*.asks.*.config.max' => ["required_if:categories.*.asks.*.style,$range", 'numeric'],
'categories.*.asks.*.config.options' => ["required_if:categories.*.asks.*.style,$select", 'array'],
'categories.*.asks.*.config.options' => ["required_if:categories.*.asks.*.style,$multiSelect", 'array'],
'categories.*.asks.*.config.options' => ["required_if:categories.*.asks.*.style,$radio", 'array'],
```

O payload manda `ask_type_id`, nunca `style`; logo, nenhuma dessas regras é aplicada. Además, a chave `categories.*.asks.*.config.options` aparece **três vezes** no array `rules` — a última ocorrência sobrescreve as anteriores no PHP, o que faria a validação usar apenas a condição de RADIO (se a chave fosse corrigida). Efeito prático: um questionário com pergunta do tipo SELECT pode ser salvo **sem opções**, quebrando a renderização do portal.

### B2 — `published_at` gravado em formato 12 horas

`v6/app/Http/Model/Prefecture/DynamicForms/Questionnaire.php`:

```php
$this->attributes['published_at'] = \Carbon\Carbon::create($year, $month, $day, $hour, $minute)->format('Y-m-d h:i:s');
```

`h` é hora de 12 horas; o accessor de leitura usa `H:i` (24 horas). Um questionário publicado às 18:00 é gravado como 06:00 e exibido como 06:00.

### B3 — `AskType` e `AskStyle` com `$fillable` incorreto

```php
class AskType extends Model
{
    protected $fillable = [
        'ask_id',
    ];
    public function question() { return $this->belongsTo(Ask::class); }
}
```

`ask_id` não existe em `ask_types`/`ask_styles`; os campos reais (`name`, `description`, `icon`) não são fillable. Por isso os seeds usam `DB::table()->updateOrInsert()` em vez do model, e `AskType::query()->get()` funciona (leitura). Um `AskType::create(['name' => 'TEXT'])` falharia silenciosamente (atributo ignorado, pois `name` não está no `$fillable`) ou com erro de coluna.

### B4 — `ON DELETE CASCADE` em `ask_style_id` nullable

`v6/database/migrations/2024_03_27_190914_create_asks_table.php`:

```php
$table->unsignedBigInteger('ask_style_id')->nullable();
$table->foreign('ask_style_id')->references('id')->on('ask_styles')->onDelete('cascade');
```

Apagar uma linha de `ask_styles` (o catálogo é editável no banco) apaga **todas as perguntas** que a utilizavam — e, em cascata, suas respostas (via `filled_questionnaires`? não: `answers` é JSON, então as respostas ficam órfãs com `ask_id` inexistente). Risco de perda de dados.

### B5 — Arquivos órfãos no storage

`AskFileController@save` grava o arquivo e cria `ask_files` **antes** de o cidadão enviar o questionário (`filled_questionnaire_id` fica `null`). Se o cidadão abandonar o formulário, o arquivo e a linha ficam para sempre; não há job de limpeza. Ao excluir o questionário (soft delete), os arquivos também permanecem.

### B6 — `answers` JSON sem validação estrutural

`SaveFilledQuestionnaireController` monta o array sem checar tipos:

```php
$data[] = [
    'ask_id' => $ask->id,
    'value' => $request->get($ask->identifier)
];
```

Não há validação de que o `value` corresponde ao tipo da pergunta, nem limite de tamanho, nem `required` server-side. Um POST direto grava valores arbitrários.

### B7 — Obrigatoriedade e máscaras validadas apenas no navegador

Toda a validação de "campo obrigatório", CPF, CNPJ e data está em `v6/public/assets/js/custom/portal/dynamic-form/form-utils.js` (`validate()`, `cpfValidation()`, `cnpjValidation()`, `dateValidation()`). Não há equivalente no servidor.

### B8 — `identifier` frágil

```php
$model->identifier = sprintf("%s%s%s", time(), rand(pow(10, 8), pow(10, 9)-1), uniqid());
```

`uniqid()` é derivado do tempo com microsegundos e é previsível; `rand()` não é criptográfico. A unicidade só é garantida pelo índice único, e um conflito geraria exceção 500 (não há retry).

### B9 — Exclusão em lote de respostas não valida vínculo

`DeleteByLoteFilledQuestionnaireController` apaga por `id` sem verificar se a resposta pertence a um questionário que o operador pode ver.

### B10 — Mensagem de exportação diz "CSV" mas o arquivo é XLSX

```php
return response()->json([
    'success' => true,
    'message' => 'CSV exportado com sucesso!',
    'file_path' => '/storage/' . $filePath,
]);
```

`$filePath` termina em `.xlsx` e o `ResponsesExport` usa `maatwebsite/excel`.

### B11 — `mkdir` do diretório de exportação

```php
if (!Storage::exists($directoryPath)) {
    mkdir(storage_path('app/public/' . $directoryPath), 0755);
}
```

Se `storage/app/public` não existir (ou o link simbólico `public/storage` não for criado), o `mkdir` falha e a exportação retorna 500.

### B12 — `showIndividualResponses` carrega todas as respostas

O método carrega `filledQuestionnaires` (todas as respostas do questionário) e seleciona a nth em memória:

```php
$filledQuestionnaires = $questionnaire->filledQuestionnaires[$index - 1];
```

Com milhares de respostas isso consome muita memória; o índice é **1-based** enquanto o `currentResponse` devolvido é `index + 1` (off-by-one na exibição).

### B13 — Perguntas órfãs após soft delete

`Questionnaire` usa `SoftDeletes`, mas `asks.ask_category_id` e `asks.questionnaire_id` têm `ON DELETE CASCADE` referenciando a tabela — que **não** é apagada no soft delete. Resultado: as perguntas e categorias permanecem no banco, invisíveis para a listagem (que filtra `deleted_at is null`) e ocupando ids.

### B14 — Ações do CMS gravam mensagens de sucesso em controller que retorna JSON

`SaveQuestionnaireController` chama `flash('Questionário salvo com sucesso.')->success();` **e** devolve JSON 201. Como a chamada vem de XHR, a mensagem do flash fica na sessão e aparece na próxima tela do CMS.

### B15 — `require_if` no singular de `config.options.*` não cobre `MULTI_FILE`

O `config` de arquivo é `{isImg}` e não é validado no servidor; um `isImg` não booleano é aceito e é comparado como truthy no Blade (`data_get($config, 'isImg', false)`).

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| `AskCategory\SaveRequest` valida `parentId` | o controller usa `parent_id`; o request valida `parentId` (camelCase) — a validação do pai nunca é aplicada ao campo realmente usado. |
| `FilledQuestionnaire\SaveRequest` (CMS) | existe, mas o controller `create()` retorna questionários com `status = 1` e `whereHas('asks')`; a view `filled-questionnaire/create.blade.php` **não existe** na pasta `v6/resources/views/prefecture/dynamics-forms/filled-questionnaire/` (só `index`, `content`, `responses`) — a rota `dynamic_forms.filled-questionnaire.create` aponta para uma view inexistente. |
| `SaveFilledQuestionnaireController::saveFilledQuestionnaireOnAksFile` | é `public static` e poderia ser reutilizada; só o CMS a chama. |
| `ShowFilledQuestionnaireController` vs `FilledQuestionnaireController@showResponses` | duplicam a mesma consulta; um devolve Resource, o outro view + JSON. |
| `AskType`/`AskStyle` model | relação `question()` (belongsTo Ask) está invertida/sem uso — quem tem a pergunta é `Ask`. |
| Seed `QuestionTypesSeeder` (`v6/database/seeds/QuestionTypesSeeder.php`) | pertence ao módulo de **enquetes** (tabela `question_types`), não a este. |
| Exclusão de `ask_files` junto com a resposta | não existe: apagar a resposta deixa os arquivos em disco e as linhas `ask_files` (o cascade só ocorre se a linha for removida). |

## 3. Itens da v5 que não existem na v6 (e vice-versa)

- **PHP**: nenhum. `diff -r` entre todos os diretórios do módulo em `v5/` e `v6/` retorna vazio.
- **Views**: `v6/resources/views/prefecture/dynamics-forms/questionnaire/form.blade.php` só difere no *cache-busting* (`structure.js?v=0.0.7`/`listeners.js?v=0.0.6` na v6 contra `?v=0.0.8`/`?v=0.0.7` na v5) e as telas de resultado do portal (`modelo1..modelo6/dynamics-forms/filled-questionnaire/show.blade.php`) foram reorganizadas com `container` e botão de compartilhamento.
- **Rotas**: as rotas do módulo são as mesmas; a v6 adicionou rotas de outros módulos (`tv-camara/delete-por-lote`, `ajust-vereadores-mandato`, `/modulos`, etc.).

## 4. Mapa v5 → v6

| Item | v5 | v6 | Situação |
|---|---|---|---|
| `app/Http/Model/Prefecture/DynamicForms/*` (7 models) | ✅ | ✅ | **idêntico** |
| `app/Enum/Prefecture/DynamicForms/*` (2 enums) | ✅ | ✅ | **idêntico** |
| `app/Http/Controllers/Prefecture/DynamicForms/**` (2 + 11 actions) | ✅ | ✅ | **idêntico** |
| `app/Http/Controllers/Portal/DynamicForms/**` (3 controllers) | ✅ | ✅ | **idêntico** |
| `app/Http/Requests/{Prefecture,Portal}/DynamicForms/**` (11 requests) | ✅ | ✅ | **idêntico** |
| `app/Http/Resources/Prefecture/DynamicForms/**` (11 resources) | ✅ | ✅ | **idêntico** |
| `app/Exports/DynamicForms/FilledQuestionnaire/ResponsesExport.php` | ✅ | ✅ | **idêntico** |
| `database/seeds/DynamicForms/{AskTypeSeeder,AskStyleSeeder}.php` | ✅ | ✅ | **idêntico** |
| `database/seeds/AddSatisfationLevelOptionOnAskType.php` | ❌ | ✅ | **novo na v6** (tipo 10) |
| `resources/views/prefecture/dynamics-forms/**` | ✅ | ✅ | 1 view com cache-busting divergente |
| `resources/views/portal/layouts/modelo1..6/dynamics-forms/**` | ✅ | ✅ | telas de resultado reescritas na v6 |
| `public/assets/js/custom/portal/dynamic-form/**` | ✅ | ✅ | idêntico |
| `public/assets/js/custom/contents/formularios-dinamicos/**` | ✅ | ✅ | idêntico |
| `routes/basepro.php` — bloco `formularios-dinamicos` (linhas 483-511 na v5) | ✅ | ✅ (linhas 471-509) | idêntico |
| `routes/basepro.php` — bloco `formularios` (linhas 571-576 na v5) | ✅ | ✅ (linhas 533-540) | idêntico |
| `app/Helpers/functions.php::setQuestionnaireFieldStyle` | ✅ | ✅ | idêntico |
| Migrations do módulo | ❌ | ✅ | **v6 só** — o schema nasce na v6 (2024-03-27 em diante) |

> Observação importante: as migrations do módulo **só existem na v6** (`v6/database/migrations/2024_03_27_190858_*` em diante). A v5 depende de um banco já migrado; quem recriar o ambiente do zero a partir da v5 precisa das migrations da v6 (ou de um dump do schema).

## 5. Riscos de dados (migração de schema)

| Risco | Detalhe |
|---|---|
| `answers` JSON → normalizar | Requer parser que trate `value` escalar e array; valores não textuais (ex.: `true` de checkbox) precisam de cast. |
| `config` → colunas | `options` pode estar ausente ou com formato diferente conforme o tipo; `min`/`max` podem vir como string (`Input` do JS). |
| `slug` sem unique | Na v7, decidir entre restaurar o índice único (recomendado) ou manter a duplicidade e tratar no `findOrFail` do portal (hoje o portal usa `->first()`, pegando o primeiro em caso de duplicidade). |
| `identifier` → UUID | Quebra o `name` dos inputs já salvos no HTML em cache e as URLs que referenciam `ask.identifier` (`/formularios/ask-file` manda o identifier). |
| `ask_style_id` CASCADE | Antes de mexer, recriar a FK como `ON DELETE SET NULL`. |
| Soft delete + FK cascade | Soft delete de questionário não limpa `asks`/`ask_categories`; decidir se a v7 mantém esse comportamento (visibilidade) ou apaga de verdade. |
| `filled_questionnaires.answers` × `ask_files` | Ao remover perguntas, as respostas mantendo `ask_id` órfão devem ser tratadas (hoje o save impede a remoção; a exclusão em lote de respostas apaga o registro e o cascade leva os `ask_files`, mas **não** os arquivos em disco). |

## 6. Perguntas em aberto para o negócio

1. O resultado público deve continuar parcial (só gráficos de escolha) ou expor também texto/arquivo a visitantes? Hoje depende de `has_result` **e** de login.
2. Deve haver um prazo de expiração/anonimização das respostas (LGPD)? O sistema guarda o IP de quem respondeu (`filled_questionnaires.ip`).
3. A exclusão de respostas deve apagar fisicamente os arquivos anexados?
4. Um questionário em rascunho (sem `status`) deve ser acessível por link? Hoje o portal responde 404.
5. É aceitável a prefeitura reutilizar o mesmo `slug` em questionários diferentes (o unique foi removido)? O portal resolve pelo `first()`.
6. Deve existir limite de respostas por questionário / proteção contra spam além do reCAPTCHA?
7. O "Link Permanente" exibido no CMS (`route('portal.formularios.edit', '')`) é sempre no domínio principal — ainda há previsão de prefeituras com subdomínio ou múltiplos portais?

## 7. Checklist de correções para a v7

- [ ] Corrigir as regras condicionais do `config` (`ask_type_id`) e remover as chaves duplicadas.
- [ ] Corrigir `format('Y-m-d h:i:s')` → `H:i:s`.
- [ ] Trocar `rand()/uniqid()` por `Str::uuid()`.
- [ ] FK de `ask_style_id` com `nullOnDelete()`.
- [ ] Validar `required`, tipos e CPF/CNPJ/data no servidor.
- [ ] Normalizar `answers` e `config` (tabelas) com importador.
- [ ] Job de limpeza de `ask_files` órfãos e de arquivos em disco.
- [ ] Exportação assíncrona + limpeza de arquivos antigos de export.
- [ ] Corrigir a view ausente `filled-questionnaire/create.blade.php` (ou remover a rota).
- [ ] Corrigir o off-by-one de `currentResponse` no painel individual.
- [ ] Restaurar o índice único de `slug` (ou documentar a decisão).
- [ ] Trocar `str_slug()` por `Str::slug()`.
- [ ] Tipos de retorno das rotas do portal: `AskTypesEnum` → enum PHP.
