# ENQUETES — Gaps, Dívidas e Comparativo v5 → v6

> **Versão analisada:** `v6` (o módulo existe **byte a byte idêntico** na `v5`)

---

## 1. Mapa v5 → v6

### 1.1 Resultado da comparação

**Não houve nenhuma evolução do módulo entre a `v5` e a `v6`.** Todos os
arquivos do módulo foram comparados com `diff` e são idênticos:

| Artefato | `v5` | `v6` | Diff |
|---|---|---|---|
| `app/Http/Controllers/Prefecture/SurveyController.php` | ✅ | ✅ | **idêntico** |
| `app/Http/Controllers/Portal/SurveyController.php` | ✅ | ✅ | **idêntico** |
| `app/Http/Model/Prefecture/Survey.php` | ✅ | ✅ | **idêntico** |
| `app/Http/Model/Prefecture/SurveyAnswer.php` | ✅ | ✅ | **idêntico** |
| `app/Http/Model/Prefecture/SurveyResult.php` | ✅ | ✅ | **idêntico** |
| `app/Http/Requests/SurveyRequest.php` | ✅ | ✅ | **idêntico** |
| `app/Repositories/Contracts/ISurveyRepository.php` | ✅ | ✅ | **idêntico** |
| `app/Repositories/Contracts/ISurveyAnswerRepository.php` | ✅ | ✅ | **idêntico** |
| `app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php` | ✅ | ✅ | **idêntico** |
| `app/Repositories/Eloquent/Prefecture/SurveyAnswer/SurveyAnswerRepository.php` | ✅ | ✅ | **idêntico** |
| `database/migrations/2022_01_10_111907_create_surveys_table.php` | ✅ | ✅ | **idêntico** |
| `database/migrations/2022_01_10_121444_create_survey_answers_table.php` | ✅ | ✅ | **idêntico** |
| `database/migrations/2022_01_17_100753_create_survey_results_table.php` | ✅ | ✅ | **idêntico** |
| `database/migrations/2023_04_06_134445_create_view_to_enquetes.php` | ✅ | ✅ | **idêntico** |
| `database/migrations/2023_05_25_093023_update_enquetes_view_data_hour.php` | ✅ | ✅ | **idêntico** |
| `resources/views/prefecture/surveys/` (4 arquivos) | ✅ | ✅ | **idêntico** |
| `resources/views/portal/layouts/surveys/` (3 arquivos) | ✅ | ✅ | **idêntico** |
| `public/assets/js/custom/contents/enquetes/` (2 arquivos) | ✅ | ✅ | **idêntico** |

### 1.2 Diferenças (apenas de numeração de linha nas rotas)

| Rota | `v5/routes/basepro.php` | `v6/routes/basepro.php` |
|---|---|---|
| `POST /cms/ajax/ajax-delete-answer` (`ajax.delete.answer`) | 123 | 133 |
| `POST /cms/ajax/enquete/delete-por-lote` (`ajax.survey.deleteByLote`) | 144 | 155 |
| `Route::resource('enquete')` | 194 | 222 |
| `GET /cms/enquete/{slug}/{id}/delete` | 202 | 230 |
| `GET /cms/datatable/enquete` (`enquete`) | 428 | 417 |

| Rota | `v5/routes/web.php` | `v6/routes/web.php` |
|---|---|---|
| `GET /enquetes` (`enquetes`) | 41 | 32 |
| `GET /enquetes/{slug}` (`enquetes.show`) | 42 | 33 |
| `GET /enquetes/encerrada/{slug}` (`enquetes.closed.show`) | 43 | 34 |
| `POST /send-survey-ajax` (`send.survey.ajax`) | 44 | 35 |

Conteúdo das rotas: **idêntico** (mesmos verbos, URIs, nomes, controllers).

### 1.3 O que isso significa para a reconstrução

- O módulo **não acumulou dívida funcional** por evolução incremental; ele
  simplesmente **nunca foi revisado** desde 2022-01.
- Todos os bugs listados na §3 são **originais de 2022**, não regressões da v6.
- A v7 pode redesenhar o módulo do zero **sem risco de perder funcionalidade
  introduzida na v6**, o que torna a migração deste módulo uma das mais
  simples do sistema.
- O único cuidado: o `slug` da enquete é exibido/divulgado publicamente e
  pode estar impresso ou em links externos — a semântica da URL
  `/enquetes/{slug}` **precisa** ser preservada (ver E9 em
  `06-MIGRACAO-MODERNA.md`).

### 1.4 Itens que existem na v5 e foram removidos na v6 (ou vice-versa)

**Nenhum.** O inventário de rotas, models, repositories, migrations, views e
JS é o mesmo nas duas versões.

## 2. Bugs e erros conhecidos

A lista abaixo é a **fonte única de verdade** sobre os defeitos; os
identificadores `A1`..`A30` são referenciados por
`05-REGRAS-E-PORTABILIDADE.md` §3.1 e `07-TESTES.md` §4.

| ID | Severidade | Descrição | Onde |
|---|---|---|---|
| A1 | **Alta** | `SurveyRepository::upInsert()` não retorna o model → o flash de sucesso e o log de auditoria **nunca** são exibidos/gravados em create e update | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:43-52` |
| A2 | **Alta** | `Repository::upInsert()` faz `dd($th)` no `catch` → erro de banco vira dump de exceção na tela | `v6/app/Repositories/Repository.php:66-69` |
| A4 | **Alta** | Voto é aceito em enquete encerrada ou em rascunho | `v6/app/Http/Controllers/Portal/SurveyController.php:91-116` |
| A5 | **Alta** | Voto aceito com `survey_answer_id` de outra enquete (voto cruzado) | idem |
| A6 | **Alta** | Sem unicidade: voto duplicado aceito indefinidamente | `v6/database/migrations/2022_01_17_100753_create_survey_results_table.php` |
| A7 | **Média** | "Você já votou" compara o IP apenas com o **primeiro** voto da enquete | `v6/app/Http/Controllers/Portal/SurveyController.php:57-58` + 6 sidebars |
| A8 | **Média** | Editar enquete com votos recria as opções → apuração zera | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:47-51` |
| A19 | **Alta** | `check.route.permission` não é aplicado (o controller não chama `parent::__construct()`) | `v6/app/Http/Controllers/Prefecture/SurveyController.php:24-28` |
| A20 | **Alta** | `ajax.survey.deleteByLote` sem `check.logged` — exclusão em lote acessível sem autenticação (protegida só por CSRF) | `v6/routes/basepro.php:155` |
| A21 | **Média** | Rotas `ajax.delete.answer` e `enquete/{slug}/{id}/delete` apontam para métodos inexistentes → 500 | `v6/routes/basepro.php:133` e `:230` |
| A3 | **Média** | Desmarcar "Publicar" na edição não despublica | `v6/resources/views/components/inputs/checkbox.blade.php:13` |
| A9 | **Média** | Slug inexistente → HTTP 500 em vez de 404 | `v6/app/Http/Controllers/Portal/SurveyController.php:43-89` |
| A10 | **Baixa** | "Enquete encerrada em" exibe `publication_date` | `v6/resources/views/portal/layouts/surveys/closed.blade.php:17-18` |
| A11 | **Baixa** | `aria-valuenow="40"` fixo nas barras de progresso | `v6/resources/views/portal/layouts/surveys/closed.blade.php:29` |
| A12 | **Baixa** | `/enquetes/{slug}` declarada antes de `/enquetes/encerrada/{slug}` | `v6/routes/web.php:33-34` |
| A13 | **Baixa** | Breadcrumb da tela de resultado aponta para a rota de votação | `v6/routes/breadcrumbs.php:296` |
| A14 | **Baixa** | `categoryIndex()` — método do *Agenda de Eventos* copiado, sem rota | `v6/app/Http/Controllers/Portal/SurveyController.php:26-39` |
| A15 | **Baixa** | Nomenclatura invertida: `answers()` devolve votos, `surveyAnswers()` devolve opções | `v6/app/Http/Model/Prefecture/Survey.php:32-40` |
| A16 | **Baixa** | Relações `SurveyAnswer::surveys()` e `SurveyResult::surveyAnswers()` quebradas | `v6/app/Http/Model/Prefecture/SurveyAnswer.php:26-29`, `v6/app/Http/Model/Prefecture/SurveyResult.php:26-29` |
| A17 | **Média** | `SurveyResult::all()` carrega todos os votos do banco a cada visita ao CMS | `v6/app/Http/Controllers/Prefecture/SurveyController.php:34` |
| A18 | **Média** | N+1 na apuração: 1 query por opção + carga completa em memória | `v6/app/Http/Controllers/Portal/SurveyController.php:68-79` |
| A22 | **Baixa** | `action` do formulário aponta para `contato.enviar` | 6 sidebars + `v6/resources/views/portal/layouts/surveys/show.blade.php:52` |
| A23 | **Baixa** | Função JS global `contact()` declarada em 7 arquivos | idem |
| A24 | **Baixa** | `id="form-survey"` duplicado na página de votação | idem |
| A25 | **Baixa** | `str_limit` (depreciado no Laravel 5.8) | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:33` |
| A26 | **Baixa** | `updateOrCreate` descarta `answers`/`_token` silenciosamente (quebra com `preventSilentlyDiscardingAttributes`) | `v6/app/Http/Model/Prefecture/Survey.php:15-21` |
| A27 | **Baixa** | Interpolação `"$request[slug]%"` de leitura obscura (verificada como correta em PHP 7.2 e 8.4) | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:34` |
| A28 | **Média** | Auditoria gravada com `performedOn(CalendarEvent)` — modelo errado | `v6/app/Http/Controllers/Prefecture/SurveyController.php:24-28` |
| A29 | **Baixa** | Controller do portal herda da base do CMS | `v6/app/Http/Controllers/Portal/SurveyController.php:12` |
| A30 | **Média** | Votos sobrevivem à exclusão da enquete (sem soft delete em `survey_results`) | `v6/app/Http/Model/Prefecture/SurveyResult.php:11` |

### 2.1 Achados adicionais verificados na apuração

| Achado | Impacto |
|---|---|
| A view SQL `enquetes` não filtra por `status` | A listagem do CMS mostra rascunhos — comportamento **desejado**, mas não documentado |
| A view SQL ordena por `created_at` no `row_number()` e por `publication_date` no `ORDER BY` | A coluna `value` (numeração) não acompanha a ordem exibida |
| `surveys.slug` não tem índice | *Sequential scan* em `where('slug', ?)` na rota mais acessada do portal |
| A tela do portal não usa paginação | Todas as enquetes publicadas são carregadas de uma vez |
| O `modulos.status` de `enquetes` só afeta a **sidebar** | A rota `/enquetes` continua acessível com o módulo desligado |
| O layout `default` não serve às telas de enquete | 404 em todas elas se `configurations.layout = 'default'` |
| `surveys.status` pode ficar `NULL` | `where('status','1')` não encontra enquetes nunca publicadas — coerente com o portal, mas o `NULL` é indistinguível de "não publicado" |

## 3. Funcionalidades incompletas ou mortas

| Item | Situação | Recomendação |
|---|---|---|
| `ajax.delete.answer` (`v6/routes/basepro.php:133`) | Rota morta (método inexistente) | **Remover** no cutover |
| `enquete/{slug}/{id}/delete` (`v6/routes/basepro.php:230`) | Rota morta + `GET` destrutivo | **Remover** no cutover |
| `enquete.show` (gerada pelo `Route::resource`) | `SurveyController` **não implementa** `show()` | **Remover** da resource ou implementar redirecionamento para `edit` |
| `Portal\SurveyController@categoryIndex()` | Método copiado do *Agenda de Eventos*, sem rota | **Remover** |
| `SurveyAnswer::surveys()` e `SurveyResult::surveyAnswers()` | Relações quebradas, não usadas | **Remover** |
| `Survey::answers()` | Retorna votos, nome enganoso | **Renomear** para `results()` |
| Import `use App\Prefecture\Survey; use App\Prefecture\SurveyResult;` em `v6/app/Http/Controllers/Portal/PortalController.php:20-21` | Imports mortos | **Remover** |
| `use Image;` em `v6/app/Http/Controllers/Prefecture/SurveyController.php:13` | Import morto (`intervention/image`) | **Remover** |
| `$results = SurveyResult::all();` em `index()` | Carregado e não usado | **Remover** |
| Exclusão em lote que não remove as opções | Deixa `survey_answers` órfãs (soft-deleted apenas as enquetes) | **Corrigir** para apagar as opções em lote |
| Ausência de qualquer relatório/exportação de resultados para o cidadão | — | **Avaliar** com o negócio |
| Ausência de resultado para enquetes ainda abertas | O portal só mostra percentual após o encerramento | Decisão de produto; registrar |
| Ausência de "votos por dia" / série temporal | — | Fora do escopo do legado |

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

| Risco | Impacto | Mitigação |
|---|---|---|
| `survey_results` não tem `deleted_at` | Excluir a enquete mantém votos órfãos e não auditáveis | Introduzir `deleted_at` ou `ON DELETE CASCADE` + operação de "excluir definitivamente" auditada |
| `survey_answers` soft-deleted e recriadas a cada edição | Enquetes já editadas **perderam a ligação** entre votos e opções legítimas | Não há recuperação possível a partir do banco; confirmar com o negócio se o histórico importa. Bloquear edição futura por Policy |
| `surveys.status` com `NULL` | Precisa de backfill para `false` antes de `NOT NULL DEFAULT false` | `UPDATE surveys SET status = false WHERE status IS NULL;` |
| `timestamp without time zone` → `timestampTz` | Deslocamento de horário se o servidor e o banco estiverem em fusos diferentes | Converter com `AT TIME ZONE 'America/Sao_Paulo'` e validar uma amostra |
| `slug` sem unicidade | Slugs duplicados impedem o índice único | Detectar duplicatas: `SELECT slug, count(*) FROM surveys GROUP BY slug HAVING count(*) > 1;` e resolver antes do cutover |
| Apuração histórica feita sobre as **opções atuais** | Enquetes editadas exibem percentuais zerados | Recalcular a apuração a partir de `survey_results.survey_answer_id` (os ids antigos ainda existem em `survey_answers`, com `deleted_at` preenchido) — a view `enquetes` já conta por `survey_id`, então é possível montar um relatório de consistência |
| `ip` e `browser` são dados pessoais (LGPD) | Retenção e exposição | Definir política de retenção; mascarar/truncar `browser`; restringir a leitura a perfil de auditoria |
| `enquetes` (view) é consultada por `DB::table()` | A view não é portável (SQL PostgreSQL) | Substituir por query Eloquent agregada |

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

1. **Uma enquete pode receber mais de um voto por pessoa?** Hoje o legado aceita
   ilimitado. Se a resposta for "não", é preciso decidir a estratégia de
   identificação (IP puro? cookie? cadastro?) — ver R2 em
   `06-MIGRACAO-MODERNA.md`.
2. **O que acontece com os votos em enquetes encerradas?** O legado aceita;
   a recomendação é recusar com 422.
3. **É permitido despublicar uma enquete que já recebeu votos?** O legado não
   consegue nem despublicar (A3).
4. **O resultado deve ser público antes do encerramento?** Hoje só depois.
5. **Há enquetes com mais de uma pergunta?** O modelo atual (uma pergunta com N
   opções) não comporta. Se houver demanda, avaliar o reaproveitamento do motor
   de formulários dinâmicos (`App\Prefecture\DynamicForms\*`), que já suporta
   múltiplas perguntas, categorias, tipos e upload de arquivo.
6. **Quantos votos constituem uma enquete válida para publicação do resultado?**
   Não há regra de sigilo/limiar no legado.
7. **Quem pode ver os IPs dos voters?** O CMS atual **não** expõe os IPs em
   nenhuma tela — o `ip` e o `browser` são gravados mas nunca exibidos. Isso
   é intencional?
8. **Precisa-se de histórico de alterações da enquete?** O legado só tem o log
   de atividade, e ele está gravado sobre o modelo errado (A28).
9. **Deve haver expiração automática (job) que marque a enquete como encerrada?**
   Hoje o encerramento é implícito, por comparação de `final_date`.

## 6. Checklist de saneamento para o cutover

- [ ] Remover as 2 rotas mortas e a `enquete.show` da resource.
- [ ] Remover `categoryIndex()`, `SurveyAnswer::surveys()`,
      `SurveyResult::surveyAnswers()`, imports mortos e `SurveyResult::all()`.
- [ ] Fazer `SurveyRepository::upInsert()` retornar o model (A1).
- [ ] Substituir o `dd($th)` do repository base por exceção de domínio (A2).
- [ ] Aplicar `check.route.permission` (ou Policy) às rotas de enquete e
      `check.logged` à rota de exclusão em lote (A19, A20).
- [ ] Validar `survey_answer_id` contra `survey_id` no voto (A5).
- [ ] Definir e implementar a regra de voto único (A6, A7).
- [ ] Recusar voto em enquete fora da janela (A4).
- [ ] Devolver 404 (não 500) para slug inexistente (A9).
- [ ] Corrigir a data exibida na tela de resultado (A10) e o `aria-valuenow` (A11).
- [ ] Trocar o `action` do formulário de votação (A22) e eliminar a duplicação
      da função `contact()` e do `id="form-survey"` (A23, A24).
- [ ] Gravar a auditoria sobre o modelo correto (A28).
- [ ] Apagar as opções das enquetes excluídas em lote.
- [ ] Substituir `str_limit` por `Str::limit` (A25) e reescrever a interpolação
      do slug (A27).
- [ ] Decidir o destino da view SQL `enquetes` (remover ou reescrever).
- [ ] Definir retenção e acesso aos dados `ip` / `browser` (LGPD).
- [ ] Escrever a suíte de testes descrita em `07-TESTES.md`.
