# ENQUETES — Modelagem de Dados

> **Versão analisada:** `v6`
> Banco: PostgreSQL 10 (schema `public`), base `prefeituras_v5`.
> Estrutura verificada com `\d` (leitura) no container `v6_6f22e1bd-postgres-1`.

---

## 1. Tabelas do módulo

### 1.1 `surveys` — a enquete

Origem: `v6/database/migrations/2022_01_10_111907_create_surveys_table.php`

| Coluna | Tipo (PostgreSQL) | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('surveys_id_seq')` | PK |
| `question` | `text` | não | — | Texto da pergunta |
| `final_date` | `timestamp(0) without time zone` | não | — | Data/hora de encerramento (expiração) |
| `publication_date` | `timestamp(0) without time zone` | sim | `NULL` | Data/hora de início da publicação |
| `status` | `boolean` | sim | `NULL` | `true` = publicada; `NULL` = nunca publicada |
| `slug` | `character varying(191)` | sim | `NULL` | URL pública (`/enquetes/{slug}`); sem índice único |
| `created_at` | `timestamp(0) without time zone` | sim | `NULL` | |
| `updated_at` | `timestamp(0) without time zone` | sim | `NULL` | |
| `deleted_at` | `timestamp(0) without time zone` | sim | `NULL` | Soft delete |

Índices: apenas `surveys_pkey`.
Referências: `survey_answers.survey_id`, `survey_results.survey_id`.

**Sem índice em `slug`:** a rota pública busca por `where('slug', $slug)`, que
resulta em *sequential scan* na tabela `surveys`.

### 1.2 `survey_answers` — as opções de resposta

Origem: `v6/database/migrations/2022_01_10_121444_create_survey_answers_table.php`

| Coluna | Tipo (PostgreSQL) | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('survey_answers_id_seq')` | PK |
| `answer` | `character varying(300)` | não | — | Texto da opção |
| `survey_id` | `integer` | não | — | FK → `surveys(id)` |
| `created_at` | `timestamp(0) without time zone` | sim | `NULL` | |
| `updated_at` | `timestamp(0) without time zone` | sim | `NULL` | |
| `deleted_at` | `timestamp(0) without time zone` | sim | `NULL` | Soft delete |

FK: `survey_answers_survey_id_foreign FOREIGN KEY (survey_id) REFERENCES surveys(id)`.
Referenciada por: `survey_results.survey_answer_id`.

### 1.3 `survey_results` — os votos

Origem: `v6/database/migrations/2022_01_17_100753_create_survey_results_table.php`

| Coluna | Tipo (PostgreSQL) | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | `nextval('survey_results_id_seq')` | PK |
| `survey_id` | `integer` | não | — | FK → `surveys(id)` |
| `survey_answer_id` | `integer` | não | — | FK → `survey_answers(id)` |
| `ip` | `character varying(191)` | sim | `NULL` | `$_SERVER['REMOTE_ADDR']` |
| `browser` | `character varying(500)` | sim | `NULL` | `$_SERVER['HTTP_USER_AGENT']` |
| `created_at` | `timestamp(0) without time zone` | sim | `NULL` | |
| `updated_at` | `timestamp(0) without time zone` | sim | `NULL` | |

FKs: `survey_results_survey_id_foreign` e `survey_results_survey_answer_id_foreign`.

**Sem `deleted_at`** — ao contrário de `surveys` e `survey_answers`. Os votos
são imutáveis e sobrevivem à exclusão (soft) da enquete.

**Sem restrição de unicidade** em `(survey_id, ip)` ou
`(survey_id, survey_answer_id)`: nada no banco impede votos duplicados, nem
votos simultâneos em opções diferentes da mesma enquete.

## 2. Relacionamentos

```
surveys (1) ──────< (N) survey_answers        FK survey_answers.survey_id
   │                        │
   │                        └──────< (N) survey_results   FK survey_results.survey_answer_id
   └───────────────────────────────< (N) survey_results   FK survey_results.survey_id
```

| Relação | Eloquent | Implementação |
|---|---|---|
| `Survey::surveyAnswers()` | `hasMany(SurveyAnswer::class, 'survey_id', 'id')->orderBy('id', 'ASC')` | `v6/app/Http/Model/Prefecture/Survey.php:32-35` |
| `Survey::answers()` | `hasMany(SurveyResult::class, 'survey_id', 'id')` | `v6/app/Http/Model/Prefecture/Survey.php:37-40` |
| `SurveyAnswer::surveys()` | `hasMany(Survey::class, 'id', 'survey_id')` — **relacionamento invertido e incorreto** (uma resposta "tem" várias enquetes) | `v6/app/Http/Model/Prefecture/SurveyAnswer.php:26-29` |
| `SurveyResult::surveyAnswers()` | `hasMany(SurveyAnswer::class, 'survey_id', 'id')` — usa `survey_id` do model de resultado, que **não existe** em `survey_results`; relação quebrada | `v6/app/Http/Model/Prefecture/SurveyResult.php:26-29` |

> As duas últimas relações **não são usadas** por nenhum controller e estão
> incorretas. Devem ser removidas na v7.

### 2.1 `$fillable` e casts

| Model | `$fillable` | Soft delete |
|---|---|---|
| `Survey` | `question`, `final_date`, `slug`, `status`, `publication_date` | sim |
| `SurveyAnswer` | `answer`, `survey_id` | sim |
| `SurveyResult` | `survey_id`, `survey_answer_id`, `ip`, `browser` | **não** |

Nenhum model define `$casts`. Como `status` é `boolean` no PostgreSQL e o
formulário envia a string `"on"` (checkbox), a conversão é feita pelo próprio
PostgreSQL (que aceita `'on'`/`'off'` como literais booleanos).

## 3. View SQL `enquetes`

Criada em `v6/database/migrations/2023_04_06_134445_create_view_to_enquetes.php`
e **substituída** em `v6/database/migrations/2023_05_25_093023_update_enquetes_view_data_hour.php`.

Definição vigente (SQL literal da migration):

```sql
CREATE OR REPLACE VIEW enquetes
as select
    row_number() OVER (ORDER BY s.created_at desc) as value,
    s.question as pergunta,
    (select count(id) from survey_results sr where survey_id = s.id) as qtd_respostas,
    to_char(s.final_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text) as data_encerramento,
    s.id as id,
    to_char(s.publication_date::timestamp with time zone, 'DD/MM/YYYY às HH24:MI'::text) as data_publicacao
from surveys s
where
    s.deleted_at is null
order by s.publication_date desc;
```

| Coluna da view | Origem | Uso |
|---|---|---|
| `value` | `row_number()` sobre `created_at desc` | índice de linha (não usado pelo JS do módulo) |
| `pergunta` | `surveys.question` | coluna "Pergunta" do DataTable |
| `qtd_respostas` | subquery `count(survey_results)` por enquete | coluna "Qtd. Resposta" |
| `data_encerramento` | `to_char(final_date)` | coluna "Data de encerramento" |
| `data_publicacao` | `to_char(publication_date)` | coluna "Data de publicação" (adicionada em 2023-05-25) |
| `id` | `surveys.id` | usado pelo JS para montar os botões de edição/exclusão |

**O que a view expõe e o que esconde:**

- Expõe a contagem de respostas de cada enquete (inclusive de enquetes já
  encerradas) e as duas datas já formatadas em `dd/mm/aaaa às hh:mm`.
- **Não expõe** o `status` (publicada/rascunho) nem o `slug`. A listagem do CMS
  mostra apenas enquetes **não excluídas** (`deleted_at is null`), incluindo
  rascunhos — o operador vê e edita rascunhos, mas o público não os vê.
- O `row_number()` ordena por `created_at`, enquanto o `ORDER BY` final ordena
  por `publication_date`: a numeração interna não segue a ordem exibida.
- A view **não** aplica filtro de `status`, logo o DataTable do CMS lista
  rascunhos junto com as enquetes publicadas.
- A conversão `::timestamp with time zone` converte a coluna `timestamp without
  time zone` usando o fuso do servidor; o resultado é formatado como texto, o
  que neutraliza o deslocamento, mas torna a coluna impossível de ordenar
  numericamente.
- Não há materialized view nem índice próprio: cada chamada do DataTable
  recalcula a subquery de contagem sobre `survey_results`.

## 4. Migrations

| Migration | Data | Efeito |
|---|---|---|
| `v6/database/migrations/2022_01_10_111907_create_surveys_table.php` | 2022-01-10 | Cria `surveys` |
| `v6/database/migrations/2022_01_10_121444_create_survey_answers_table.php` | 2022-01-10 | Cria `survey_answers` + FK |
| `v6/database/migrations/2022_01_17_100753_create_survey_results_table.php` | 2022-01-17 | Cria `survey_results` + FKs |
| `v6/database/migrations/2023_04_06_134445_create_view_to_enquetes.php` | 2023-04-06 | Cria a view `enquetes` |
| `v6/database/migrations/2023_05_25_093023_update_enquetes_view_data_hour.php` | 2023-05-25 | `CREATE OR REPLACE VIEW` com `data_publicacao` e ordenação por `publication_date` |

Não existe migration posterior que adicione índices, unicidade ou campos de
auditoria de voto às tabelas do módulo.

## 5. Seeds

**O módulo Enquetes não possui seeds.** Os seeds abaixo são do módulo
*Revisões* (ratings) e **não** devem ser confundidas com as tabelas das
Enquetes:

| Seed | Tabela | Módulo real |
|---|---|---|
| `v6/database/seeds/QuestionTypesSeeder.php` | `question_types` | Revisões |
| `v6/database/seeds/OptionsSeeder.php` | `options` | Revisões |
| `v6/database/seeds/OptionCategoriesSeeder.php` | `option_categories` | Revisões |
| `v6/database/seeds/OptionsCategoryHasOptionSeeder.php` | `option_category_has_options` | Revisões |

### 5.1 Tabelas do módulo *Revisões* (contexto, NÃO pertencem às Enquetes)

Estas tabelas foram listadas na tarefa inicial por analogia de nome, mas o
código mostra que elas servem ao módulo *Revisões*
(`v6/app/Http/Controllers/Prefecture/RatingsController.php`,
`v6/app/Http/Model/Prefecture/Questions.php`,
`v6/app/Http/Model/Prefecture/Options.php`).

| Tabela | Colunas (via `\d`) | Papel no módulo *Revisões* |
|---|---|---|
| `question_types` | `id`, `title varchar(80) not null`, `deleted_at`, `created_at`, `updated_at` | Tipo da pergunta (ex.: "Resposta única") |
| `questions` | `id`, `description text not null`, `value double precision`, `order integer`, `options_categories_id int not null`, `ratings_id int not null`, `question_types_id int not null`, `question_categories_id int not null`, `deleted_at`, `created_at`, `updated_at` | Pergunta do formulário de avaliação |
| `options` | `id`, `description text not null`, `value text`, `created_at`, `updated_at`, `deleted_at` | Opção de resposta (Sim / Não / Parcial / Insatisfeito / Pouco satisfeito / Neutro / Satisfeito / Muito Satisfeito) |
| `option_categories` | `id`, `title text not null`, `deleted_at`, `created_at`, `updated_at` | Agrupador de opções |
| `option_category_has_options` | `id`, `option_categories_id int not null`, `options_id int not null`, `order integer`, `created_at`, `updated_at` | Pivot categoria ↔ opção |

FKs confirmadas: `questions.options_categories_id → option_categories.id`,
`questions.question_types_id → question_types.id`,
`questions.question_categories_id → question_categories.id`,
`questions.ratings_id → ratings.id`,
`option_category_has_options.option_categories_id → option_categories.id`,
`option_category_has_options.options_id → options.id`.

**Nenhuma dessas tabelas é lida ou escrita por `Survey`, `SurveyAnswer`,
`SurveyResult`, `SurveyRepository`, `SurveyAnswerRepository` ou pelos
controllers de enquete.** A lista de opções de uma enquete é exclusivamente a
tabela `survey_answers`.

## 6. Registro do módulo no CMS

| Tabela | Coluna | Valor |
|---|---|---|
| `modulos` | `slug` / `name` | `enquetes` / `Enquetes` (`id = 38`) |
| `resources` | `slug` / `name` / `modulo_id` | `enquete` / `Enquetes` / `38` (`id = 53`) |
| `menu_orders` | `slug` / `modulo_id` / `clicks` | `enquete` / `38` / `0` (`id = 5`) |

Origem dos seeds: `v6/database/seeds/ResourcesTableSeeder.php:459-466`,
`v6/database/seeds/MenuOrderSeeder.php:24-30`.

O status do módulo (ligado/desligado) é cacheado em `Cache::rememberForever('modulos')`
(`v6/app/Providers/ModulosServiceProvider.php:22-24`) e injetado como
`$modulos` nas views do portal — é o que habilita/desabilita a enquete na
sidebar.

## 7. Ambiguidade de nomes a registrar

| Nome | Onde aparece | Significado real |
|---|---|---|
| `enquete` | slug de `resources` e `menu_orders`; prefixo das rotas CMS | Recurso do CMS |
| `enquetes` | slug de `modulos`; URI e view `enquetes`; nomes de rota `enquetes*` | Módulo do portal |
| `answers` | `Survey::answers()` | Retorna `SurveyResult` (votos), não `SurveyAnswer` (opções) — nome enganoso |
| `surveyAnswers` | `Survey::surveyAnswers()` | Retorna `SurveyAnswer` (opções) — nome confuso, pois "answers" no portal significa voto |
| `survey_answers` | tabela | Opções de resposta da enquete |
| `survey_results` | tabela | Votos registrados |
| `porcetagem` | atributo dinâmico na view de resultado | Grafia incorreta de "porcentagem", replicada do código original |
