# ENQUETES — Regras de Negócio e Portabilidade

> **Versão analisada:** `v6` (idêntica à `v5`)
> Este arquivo lista as regras **não óbvias**, os acoplamentos e as
> **armadilhas** que precisam ser tratadas na reconstrução.

---

## 1. Regras de negócio (observáveis)

| # | Regra | Onde está implementada |
|---|---|---|
| R1 | Só aparece no portal a enquete com `status = true` **e** `publication_date <= agora`. | `v6/app/Http/Controllers/Portal/SurveyController.php:18` |
| R2 | A lista do portal ordena por `final_date desc` (encerramento mais distante primeiro). | `v6/app/Http/Controllers/Portal/SurveyController.php:18` |
| R3 | Ao abrir uma enquete **já vencida**, o portal redireciona para a tela de resultado (`enquetes/encerrada/{slug}`). | `v6/app/Http/Controllers/Portal/SurveyController.php:49-51` |
| R4 | Ao abrir a tela de resultado de uma enquete **ainda vigente**, redireciona para a tela de votação. | `v6/app/Http/Controllers/Portal/SurveyController.php:82-84` |
| R5 | "Você já votou" é decidido pelo IP do visitante igual ao IP do **primeiro** registro de `survey_results` da enquete. | `v6/app/Http/Controllers/Portal/SurveyController.php:57-58` e as 6 sidebars |
| R6 | O percentual de uma opção é `votos_da_opção / total_de_votos_da_enquete * 100`, com 2 casas decimais. | `v6/app/Http/Controllers/Portal/SurveyController.php:70-79` e `:119-124` |
| R7 | `0/0` (enquete sem votos) devolve `0`, evitando divisão por zero. | `v6/app/Http/Controllers/Portal/SurveyController.php:120-122` |
| R8 | A enquete só pode ser editada pela interface enquanto **não** houver nenhum `survey_results` vinculado. | `v6/app/Http/Controllers/Prefecture/SurveyController.php:105-106` e `v6/resources/views/prefecture/surveys/edit.blade.php:19-24` |
| R9 | Ao editar uma enquete, **todas** as opções de resposta são soft-deleted e recriadas (novos `id`). | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:47-51` |
| R10 | O `slug` é derivado da pergunta (`str_limit(question, 50)` + `slugfy`), com sufixo `-{n}` se já houver slug com o mesmo prefixo. | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:33-38` |
| R11 | O `slug` é recalculado a cada `update` → alterar o texto da pergunta **muda a URL pública**. | idem |
| R12 | Excluir uma enquete (`destroy`) remove também suas opções; excluir em lote (`destroyAll`) **não** remove as opções. | `v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:54-58` vs. `v6/app/Repositories/Repository.php:96-109` |
| R13 | A exclusão (individual ou em lote) é **soft delete**; os votos em `survey_results` permanecem no banco. | `v6/app/Http/Model/Prefecture/Survey.php:12` (`use SoftDeletes`) |
| R14 | O `status` do CMS ("Publicar") é enviado como a string `"on"` quando marcado e **ausente** quando desmarcado. | `v6/resources/views/components/inputs/checkbox.blade.php:13` |
| R15 | A sidebar do portal mostra apenas **uma** enquete: a mais recente entre as ativas (`created_at desc`). | 6 sidebars `enquetes.blade.php` |
| R16 | A listagem do CMS mostra também os rascunhos (`status` nulo/false), porque a view SQL `enquetes` não filtra por `status`. | `v6/database/migrations/2023_05_25_093023_update_enquetes_view_data_hour.php` |
| R17 | Toda visita a uma tela do CMS de enquetes incrementa `menu_orders.clicks` do slug `enquete`. | `v6/app/Http/Controllers/Prefecture/SurveyController.php:43,61,112` e `v6/app/Helpers/functions.php:865-872` |

## 2. Acoplamentos

### 2.1 Dependências do módulo Enquetes

| Depende de | Forma | Risco na v7 |
|---|---|---|
| `modulos` (slug `enquetes`) | `$modulos['enquetes']` habilita a sidebar | Baixo — manter como flag de feature |
| `menu_orders` (slug `enquete`) | `menuClicks()` incrementa contador | Baixo |
| `resources` (slug `enquete`) + papéis Sentinel | permissões `enquete.*` | Médio — migrar para Policies |
| `configurations` (chave `layout`) | escolhe o template do portal | Alto — em v7 o layout deve ser uma view única com componentes |
| `App\Http\Controllers\Admin\Controller` | `getTemplateActived()` herdado pelo controller do portal | **Médio — herança invertida; deve ser eliminada** |
| `spatie/laravel-activitylog` | auditoria com `performedOn($surveyLog)` | **Alto — `$surveyLog` é um `CalendarEvent`**; a auditoria fica gravada no modelo errado |
| `laracasts/flash` | mensagens de feedback | Baixo — substituir por session flash do Laravel |
| `yajra/laravel-datatables` | DataTable server-side | Médio |

### 2.2 Dependências do módulo Enquetes **em relação a outros módulos**

O módulo **não é usado por nenhum outro módulo**. Nenhum outro controller,
model ou view referencia `Survey`, `SurveyAnswer` ou `SurveyResult`, exceto:

- `v6/app/Http/Controllers/Portal/PortalController.php:20-21` — importa
  `Survey` e `SurveyResult` mas **não os utiliza** (imports mortos).
- A sidebar do portal (parte do layout, não do módulo).

### 2.3 Relação com o motor de formulários dinâmicos — **VEREDITO**

**Não existe integração.** Os dois módulos são total e completamente
independentes. Evidências verificadas:

| Verificação | Resultado |
|---|---|
| `grep -rn "Survey\|survey\|enquete"` em `v6/app/Http/Model/Prefecture/DynamicForms/` | **0 ocorrências** |
| `grep -rn "Survey\|survey\|enquete"` em `v6/app/Http/Controllers/Prefecture/DynamicForms/` | **0 ocorrências** |
| Tabelas usadas por Enquetes (`surveys`, `survey_answers`, `survey_results`) | Não aparecem em nenhuma migration de `DynamicForms` |
| Tabelas usadas por DynamicForms (`questionnaires`, `asks`, `ask_categories`, `ask_types`, `ask_styles`, `ask_files`, `filled_questionnaires`) | Não aparecem em nenhuma migration de Enquetes |
| Rotas | `v6/routes/basepro.php:471-492` (CMS `formularios-dinamicos/*`) e `v6/routes/web.php` não têm nenhuma rota de enquete; `v6/routes/basepro.php:222` e `v6/routes/web.php:32-35` não têm nenhuma rota de formulário dinâmico |

Comparação objetiva dos dois motores:

| Aspecto | Enquetes | Formulários dinâmicos |
|---|---|---|
| Tabelas | `surveys`, `survey_answers`, `survey_results` | `questionnaires`, `asks`, `ask_categories`, `ask_types`, `ask_styles`, `ask_files`, `filled_questionnaires` |
| Modelos | `App\Prefecture\Survey`, `SurveyAnswer`, `SurveyResult` | `App\Prefecture\DynamicForms\Questionnaire`, `Ask`, `AskCategory`, `AskType`, `AskStyle`, `AskFile`, `FilledQuestionnaire` |
| Tipo de pergunta | Uma pergunta, N opções de escolha única | Árvore de categorias, perguntas com tipo/estilo/placeholder, upload de arquivo |
| Resposta | Uma linha por voto em `survey_results` | Uma linha por preenchimento em `filled_questionnaires` (coluna `answers` em JSON, via cast `array`) |
| URL pública | `/enquetes/{slug}` | `/formularios/{slug}` (`v6/routes/web.php:534`) |
| Apuração | Percentual por opção, calculada em PHP | Resultado controlado por `has_result` no questionário |

**Conclusão:** o módulo Enquetes **não** reaproveita o motor de formulários
dinâmicos (nem o contrário). Um questionário dinâmico **não** pode ser
publicado como enquete. Na v7 há duas opções: (a) manter o motor de enquetes
como um caso especial do motor de formulários (recomendado, elimina duplicação
do formulário de votação e da apuração), ou (b) mantê-los separados e
documentando a duplicação.

## 3. Armadilhas

### 3.1 Correção funcional (bugs que mudam o resultado)

| ID | Armadilha | Impacto |
|---|---|---|
| A1 | `SurveyRepository::upInsert()` **não retorna o model** (`v6/app/Repositories/Eloquent/Prefecture/Survey/SurveyRepository.php:43-52`), mas o controller testa `if ($survey = $this->repository->upInsert($request))` (`v6/app/Http/Controllers/Prefecture/SurveyController.php:75` e `:127`) | Flash "Cadastrado com sucesso"/"Editado com sucesso" **nunca** aparece e **nenhum log de auditoria** é gravado em create/update. O redirecionamento ocorre normalmente, então o operador acredita que salvou. |
| A2 | `Repository::upInsert()` captura `\Throwable` e chama `dd($th)` (`v6/app/Repositories/Repository.php:66-69`) | Qualquer erro de banco vira um **dump de exceção** na tela, em vez de página de erro; o `try/catch` do controller nunca é acionado. |
| A3 | `status` ("Publicar") não é enviado quando desmarcado (checkbox sem valor padrão) e `updateOrCreate` só atualiza atributos presentes | **Desmarcar "Publicar" na edição não despublica a enquete.** Para despublicar é preciso editar direto no banco. |
| A4 | `sendSurvey` não verifica `status`, `publication_date` nem `final_date` (`v6/app/Http/Controllers/Portal/SurveyController.php:91-116`) | **Votos são aceitos em enquetes encerradas ou rascunho**, desde que se POSTe `survey_answer_id`. |
| A5 | `sendSurvey` não valida que `survey_answer_id` pertence a `survey_id` | É possível registrar voto em uma opção de **outra** enquete, contaminando a apuração. |
| A6 | `survey_results` não tem restrição de unicidade | Voto duplicado é aceito quantas vezes o visitante clicar (o botão não é desabilitado durante o envio). |
| A7 | A regra de "já votou" usa `SurveyResult::where('survey_id',...)->first()` (o **primeiro** voto da enquete) | Se o primeiro voto veio de um IP compartilhado (escritório, proxy), **todos** os usuários daquele IP são bloqueados; se o primeiro voto veio de outro IP, o visitante pode votar várias vezes. |
| A8 | Ao editar uma enquete que já tem votos (via HTTP, já que R8 é só de UI), as opções são recriadas com novos `id` (R9) | Os `survey_results` antigos passam a apontar para opções soft-deleted: a apuração passa a exibir **0% em todas as opções** e o total de votos permanece. |
| A9 | `show()` e `showClosed()` não verificam se a enquete existe (`$survey->final_date` sobre `null`) | Um slug inexistente (ou um slug antigo, depois da edição) provoca **erro 500** em vez de 404. |
| A10 | `closed.blade.php:17-18` mostra "Enquete encerrada em" com **`publication_date`** | A data exibida como encerramento é a de publicação. |
| A11 | `aria-valuenow="40"` fixo nas barras de progresso (`v6/resources/views/portal/layouts/surveys/closed.blade.php:29`) | Valor de acessibilidade incorreto. |
| A12 | `Route::get('/enquetes/{slug}')` é declarada **antes** de `/enquetes/encerrada/{slug}` (`v6/routes/web.php:33-34`) | O slug literal `encerrada` é capturado pela rota de votação; a tela de resultado só é alcançada por redirecionamento. |
| A13 | O breadcrumb `surveyAnswers` aponta sempre para `enquetes.show` (`v6/routes/breadcrumbs.php:296`) | Na tela de resultado, o breadcrumb leva de volta à votação (que por sua vez redireciona). |
| A14 | `Portal\SurveyController@categoryIndex()` (`v6/app/Http/Controllers/Portal/SurveyController.php:26-39`) | Método copiado do *Agenda de Eventos*, sem rota — código morto. |
| A15 | `Survey::answers()` retorna `SurveyResult` e `Survey::surveyAnswers()` retorna `SurveyAnswer` | Nomenclatura invertida em relação ao uso semântico de "answer" no portal (lá, answer = voto). Alto risco de erro em código novo. |
| A16 | `SurveyAnswer::surveys()` e `SurveyResult::surveyAnswers()` | Relações **quebradas/invertidas**, não usadas por ninguém; devem ser removidas. |
| A17 | `SurveyController@index` executa `SurveyResult::all()` (`v6/app/Http/Controllers/Prefecture/SurveyController.php:34`) e passa à view, que **não usa** | Carrega **todos os votos de todas as enquetes** na memória a cada visita ao CMS. |
| A18 | `showClosed()` faz `SurveyResult::where(...)->get()` para **cada** opção (`v6/app/Http/Controllers/Portal/SurveyController.php:68,72`) | N+1 queries e carregamento em memória de todos os votos de todas as enquetes já votadas. |
| A19 | `SurveyController` **não** chama `parent::__construct()` | O middleware `check.route.permission` (registrado na base do CMS) **não é aplicado** às rotas de enquete. |
| A20 | A rota `ajax.survey.deleteByLote` (`v6/routes/basepro.php:155`) está no grupo `ajax`, **sem** `check.logged` | A rota de exclusão em lote de enquetes (e de outros recursos) fica acessível **sem autenticação**, protegida apenas pelo token CSRF — que pode ser obtido em página pública. |
| A21 | Rotas `ajax.delete.answer` (`v6/routes/basepro.php:133`) e `enquete/{slug}/{id}/delete` (`v6/routes/basepro.php:230`) apontam para métodos **inexistentes** (`ajaxDeleteAnswer`, `deleteAnswer`) | HTTP 500 (`BadMethodCallException`) se alguém chamar essas rotas. A rota de delete ainda usa **GET** para uma ação destrutiva. |
| A22 | `Form::open(['route' => 'contato.enviar'])` nos formulários de votação (7 arquivos) | O `action` HTML aponta para `/cms/contato/enviar`. Funciona apenas porque o JS sobrescreve a URL; se o JS falhar, o navegador posta na rota de contato. |
| A23 | A função JS global `contact()` é declarada em 7 arquivos do módulo | Colisão com a página de contato do portal; a última definição vence. |
| A24 | `id="form-survey"` duplicado (sidebar + tela de votação) na mesma página | DOM inválido; `$('#form-survey')[0]` depende da ordem de renderização. |
| A25 | `Str::slug` + `str_limit` (helper depreciado no Laravel 5.8) | `str_limit` não existe no Laravel moderno → precisa de `Str::limit`. |
| A26 | `Survey::$fillable` não inclui `answers`, e `structureUpInsert` devolve o request inteiro | Funciona por acaso: o `updateOrCreate` descarta silenciosamente `answers` e `_token` por não estarem em `$fillable`. Com `Model::preventSilentlyDiscardingAttributes()` (Laravel 10+) isso passa a **lançar exceção**. |
| A27 | `SurveyRepository::structureUpInsert()` usa `"$request[slug]%"` (interpolação simples com chave de array sem aspas) | Comportamento verificado como correto em PHP 7.2.34 e PHP 8.4.26, mas é sintaxe de leitura obscura; deve ser reescrita como `$request['slug']`. |
| A28 | A auditoria usa `performedOn($this->surveyLog)`, sendo `$surveyLog` um `CalendarEvent` (`v6/app/Http/Controllers/Prefecture/SurveyController.php:24-28`) | Os logs de "Listou enquetes"/"Cadastro de enquete" ficam gravados sobre o modelo `CalendarEvent`. |
| A29 | `Portal\SurveyController` herda de `App\Http\Controllers\Admin\Controller` | Herança invertida: o controller público depende da base do CMS apenas por `getTemplateActived()`. |
| A30 | `survey_results` não tem soft delete, mas `surveys` tem | Excluir a enquete mantém os votos no banco (órfãos); o operador não consegue mais auditar quem votou em quê. |

### 3.2 Consultas e SQL

| Tipo | Onde | Observação |
|---|---|---|
| SQL hardcoded | `DB::statement(...)` nas migrations `2023_04_06_134445` e `2023_05_25_093023` | `CREATE OR REPLACE VIEW enquetes` — SQL PostgreSQL-specific (`row_number()`, `to_char`, `::timestamp with time zone`) |
| Leitura da view via Query Builder | `v6/app/Http/Controllers/Prefecture/DatatableController.php:148` — `DB::table('enquetes')->get()` | Carrega **toda** a view em memória antes de o Yajra paginar; a subquery de contagem roda para todas as enquetes |
| N+1 | `v6/app/Http/Controllers/Portal/SurveyController.php:72` | 1 query por opção da enquete |
| Query dentro de loop (Blade) | as 6 sidebars `enquetes.blade.php` | 2 queries a cada renderização de sidebar |
| `$_SERVER` direto | `v6/app/Http/Controllers/Portal/SurveyController.php:104-105` e nas sidebars | `REMOTE_ADDR` e `HTTP_USER_AGENT` lidos diretamente, sem `Request::ip()` / `Request::userAgent()` |
| `$_SERVER` em Blade | 6 sidebars | mesma leitura dentro do `@php` da view |
| Sem índice | `surveys.slug` | a rota pública busca por slug sem índice |
| Sem paginação | `Portal\SurveyController@index` | `->get()` sem `paginate()` |

### 3.3 Código duplicado / copiado

| O quê | Onde | Observação |
|---|---|---|
| Controller de enquetes **idêntico** entre v5 e v6 | `v5/app/Http/Controllers/Prefecture/SurveyController.php` = `v6/app/Http/Controllers/Prefecture/SurveyController.php` | `diff` sem diferenças |
| Controller do portal **idêntico** entre v5 e v6 | `v5/app/Http/Controllers/Portal/SurveyController.php` = `v6/app/Http/Controllers/Portal/SurveyController.php` | idem |
| Models, Request, Repositories, migrations, views e JS **idênticos** entre v5 e v6 | ver `08-GAPS-E-DIVIDAS.md` | nenhuma evolução do módulo em 2 anos |
| 6 sidebars de enquetes quase idênticas | `modelo1`..`modelo6` | mesmo PHP/JS replicado 6 vezes (só muda o HTML externo) |
| `calcularPorcentagem` duplica a global `calcPorcentagem()` | `v6/app/Http/Controllers/Portal/SurveyController.php:119` e `v6/app/Helpers/functions.php:874` | mesma fórmula, dois nomes, uma com grafia errada |
| `categoryIndex` copiado do *Agenda de Eventos* | `v6/app/Http/Controllers/Portal/SurveyController.php:26-39` | código morto |

### 3.4 Onde a v6 divergiu da v5

**Nenhuma divergência funcional.** Todos os arquivos do módulo foram
comparados arquivo a arquivo entre `v5/` e `v6/` e são **byte a byte
idênticos** (controllers, models, request, repositories, migrations, views do
CMS e do portal, e os dois arquivos JS). As únicas diferenças são **numeração
de linhas** nas rotas:

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

E em `v5/routes/web.php:41-44` as 4 rotas do portal estão nas linhas 41-44
(contra 32-35 na v6) — mesma conteúdo.

**Motivo da estabilidade:** o módulo foi construído em janeiro de 2022
(migrations de 2022-01-10 e 2022-01-17) e nunca foi revisado. A view SQL foi
atualizada uma única vez (2023-05-25) para incluir a data de publicação.
