# Revisões — Modelagem de Dados

> Schema verificado por leitura direta no Postgres da v6:
> `docker exec v6_6f22e1bd-postgres-1 psql -U postgres -d prefeituras_v5 -c "\d ratings" -c "\d questions" -c "\d send_ratings" -c "\d answer_forms" -c "\d email_revisoes"`
> Todas as tabelas abaixo são de criação exclusiva da v6 (não existiam na v5).

---

## 1. Tabelas

### 1.1 `ratings` — a avaliação (o "formulário")

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `ratings_id_seq` | PK |
| `title` | `varchar(191)` | não | — | "Prefeitura", "Câmara" |
| `version` | `varchar(191)` | sim | — | **Coluna morta**: existe no schema, não está no `$fillable` e não é gravada em nenhum ponto do código |
| `subtitle` | `varchar(191)` | não | — | "Revisão das Prefeituras" |
| `description` | `text` | não | — | texto livre exibido na listagem |
| `slug` | `varchar(191)` | não | — | gerado por `slug_fix($request->title)` |
| `status` | `boolean` | não | — | `true` = revisão vigente (única) |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | |
| `deleted_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

PK `ratings_pkey`. Referenciada por `answer_forms`, `email_revisoes`, `questions`, `send_ratings`.

### 1.2 `questions` — as perguntas

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `description` | `text` | não | — | enunciado |
| `value` | `double precision` | sim | — | **peso** da pergunta (pontuação) |
| `order` | `integer` | sim | — | ordenação manual (drag & drop) |
| `options_categories_id` | `integer` | não | — | FK → `option_categories` |
| `ratings_id` | `integer` | não | — | FK → `ratings` |
| `question_types_id` | `integer` | não | — | FK → `question_types` |
| `question_categories_id` | `integer` | não | — | FK → `question_categories` |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

### 1.3 `question_categories` — categorias (árvore)

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `question_categories_id` | `integer` | sim | — | **auto-FK**: categoria pai (`NULL` = raiz) |
| `title` | `varchar(191)` | não | — | |
| `order` | `integer` | sim | — | ordenação manual |
| `ratings_id` | `integer` | sim | — | FK lógica (sem constraint): a avaliação dona |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

Constraint: `question_categories_question_categories_id_foreign` (auto-referência).
Sem FK para `ratings` — a coluna foi adicionada depois
(`v6/database/migrations/2023_06_22_152508_add_ratings_id.php`).

### 1.4 `question_types` — tipo da pergunta

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | seq |
| `title` | `varchar(80)` | não | — |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — |

Em uso: apenas `"Resposta única"` (o seed "Múltipla escolha" está comentado).

### 1.5 `option_categories` — grupos de alternativas

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | seq |
| `title` | `text` | não | — |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — |

Grupos semeados: `Comum` (Sim/Não/Parcial) e `Satisfação` (escala Insatisfeito → Muito satisfeito).

### 1.6 `options` — alternativas

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | seq |
| `description` | `text` | não | — | Rótulo exibido: "Sim", "Não", "Parcial", "Insatisfeito", "Pouco satisfeito", "Neutro", "Satisfeito", "Muito Satisfeito" |
| `value` | `text` | sim | — | Peso da opção: "1" (Sim), "0" (Não), "0.5" (Parcial), "0", "1", "2", "3", "4" na escala de satisfação |
| `created_at` / `updated_at` / `deleted_at` | `timestamp(0)` | sim | — |

> A pontuação **não** usa `options.value`: ela compara `options.description == 'Sim'`
> (literal em `QuestionAnswersController::pontuacao()` e `EnviosRepository::returnResults()`).

### 1.7 `option_category_has_options` — N:N

| Coluna | Tipo | Null | Default |
|---|---|---|---|
| `id` | `integer` | não | seq |
| `option_categories_id` | `integer` | não | — | FK → `option_categories` |
| `options_id` | `integer` | não | — | FK → `options` |
| `order` | `integer` | sim | — | |
| `created_at` / `updated_at` | `timestamp(0)` | sim | — | **sem `SoftDeletes`** |

### 1.8 `send_ratings` — uma aplicação da avaliação

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `revisor` | `text` | não | — | quem avaliou |
| `data` | `timestamp(0)` | não | — | data/hora da avaliação |
| `status` | `boolean` | não | — | `false` = pendente de envio; `true` = enviada |
| `protocol` | `integer` | não | — | `ano . mes . rand(1000,10000)` ou herdado do envio pai |
| `send_id` | `integer` | sim | — | **auto-FK** → `send_ratings.id` (retificação) |
| `pts_totais` | `double precision` | sim | — | soma dos pesos das perguntas |
| `pts_finais` | `double precision` | sim | — | soma dos pesos das respostas "Sim" |
| `ratings_id` | `integer` | não | — | FK → `ratings` |
| `path` | `text` | sim | — | caminho do PDF no Storage |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

### 1.9 `answer_forms` — a resposta a uma pergunta

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `ratings_id` | `integer` | não | — | FK → `ratings` |
| `questions_id` | `integer` | não | — | FK → `questions` |
| `send_id` | `integer` | não | — | FK → `send_ratings` (o envio) |
| `option_id` | `integer` | sim | — | FK → `options` (alternativa escolhida) |
| `observation` | `text` | sim | — | justificativa livre |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

### 1.10 `email_revisoes` — destinatários do resultado

| Coluna | Tipo | Null | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` | não | seq | PK |
| `title` | `text` | não | — | endereço de e-mail |
| `ratings_id` | `integer` | não | — | FK → `ratings` |
| `deleted_at` / `created_at` / `updated_at` | `timestamp(0)` | sim | — | `SoftDeletes` |

## 2. Relacionamentos (Eloquent)

| Model | Relação | Tipo | Chave |
|---|---|---|---|
| `Ratings` | `questions()` | hasMany `Questions` | `ratings_id` → `id` |
| `Ratings` | `sends()` | hasMany `SendRating` | `ratings_id` → `id` |
| `SendRating` | `ratings()` | hasOne `Ratings` | `ratings_id` → `id` |
| `SendRating` | `send()` | hasOne `SendRating` (auto) | `send_id` → `id` |
| `AnswerForm` | `ratings()` / `questions()` / `option()` | hasOne | `ratings_id` / `questions_id` / `option_id` |
| `EmailRevisoes` | `send()` | hasOne `Ratings` | `ratings_id` → `id` (nome enganoso: é a avaliação) |
| `Questions` | `question_type()`, `question_categories()`, `ratings()`, `options_categories()` | hasOne | idem |
| `QuestionCategory` | `parent_id()` | hasOne (auto) | `question_categories_id` → `id` |
| `Options` | `options_category()` | belongsToMany | `option_category_has_options` |
| `OptionCategory` | `options()` | belongsToMany | `option_category_has_options` |

Observações de modelagem:

- Tudo usa `hasOne` onde caberia `belongsTo` — funcional, mas gera SQL com `LIMIT 1` e
  impede `with()` aninhado eficiente.
- `Options` e `OptionCategory` não expõem `withCount`; o N:N é consultado por
  `belongsToMany` sem `->withTimestamps()`, embora a pivot tenha `created_at/updated_at`.
- `QuestionCategory` usa `PresentableTrait` com `RevisoesPresenter`
  (`v6/app/Http/Model/Prefecture/QuestionCategory.php:11-12`).

## 3. Views SQL

Criadas por migrations com `DB::statement` executando `CREATE OR REPLACE VIEW` — não há rollback
real em duas delas.

| View | Migration | Origem | Colunas |
|---|---|---|---|
| `envios` | `v6/database/migrations/2023_04_12_143016_create_view_to_agenda_de_envios.php` (redefinida por `2023_05_29_133831_envios_revisoes_formato_24_horas.php`) | `send_ratings` ⋈ `ratings` LEFT JOIN `send_ratings` (pai) | `value`, `id`, `status`, `revisor`, `revisao`, `protocolo`, `data_envio`, `status_name` (fixo "Enviado"), `retificado` |
| `formulario` | `v6/database/migrations/2023_04_13_111410_create_to_view_formulario.php` | `ratings` | `value`, `id`, `formulario_name`, `descricao`, `status_name` ("Ativo"/"Inativo"), `status` |
| `respostas` | `v6/database/migrations/2023_03_28_135323_create_view_to_respostas.php` (redefinida por `2023_05_29_133353_respostas_revisoes_formato_24_horas.php`) | `send_ratings` ⋈ `ratings` | `value`, `status_name_respostas` ("Em espera"), `revisor`, `revisao`, `data_resposta`, `protocolo`, `retificado`, `id` |
| `listagem_clientes` | `v6/database/migrations/2023_06_29_154137_create_view_to_lista_revisoes.php` | `send_ratings` ⋈ `ratings` LEFT JOIN pai | `value`, `id`, `revisao` (concat título + data) |

Filtros aplicados nas views (não no Eloquent):

- `envios` e `respostas` exigem `sr.deleted_at IS NULL`.
- `envios` filtra `sr.status IS true` (só enviados); `respostas` filtra `sr.status IS NOT true`
  (só pendentes).
- `listagem_clientes` exige `r.status IS true` (só a avaliação vigente).

## 4. Cronologia das migrations

| Data | Migration | Efeito |
|---|---|---|
| 2022-05-17 08:35 | `2022_05_17_083539_create_ratings_table.php` | cria `ratings` (com `version` nullable) |
| 2022-05-17 09:01 | `2022_05_17_090121_create_options_table.php` | cria `options` |
| 2022-05-17 09:05 | `2022_05_17_090509_create_question_types_table.php` | cria `question_types` |
| 2022-05-17 09:21 | `2022_05_17_092101_create_question_categories_table.php` | cria `question_categories` (auto-FK) |
| 2022-05-17 09:24 | `2022_05_17_092429_create_option_categories_table.php` | cria `option_categories` |
| 2022-05-17 09:27 | `2022_05_17_092728_create_option_category_has_options_table.php` | cria a pivot |
| 2022-05-18 08:48 | `2022_05_18_084825_create_questions_table.php` | cria `questions` |
| 2022-05-18 10:36 | `2022_05_18_103602_create_send_ratings_table.php` | cria `send_ratings` (com `send_id` auto-FK) |
| 2022-05-19 09:47 | `2022_05_19_094729_create_answer_forms_table.php` | cria `answer_forms` |
| 2022-07-11 08:24 | `2022_07_11_082454_create_email_revisoes_table.php` | cria `email_revisoes` |
| 2023-03-28 13:53 | `2023_03_28_135323_create_view_to_respostas.php` | cria a view `respostas` |
| 2023-03-28 14:22 | `2023_03_28_142219_alter_order_to_questions_categories.php` | adiciona `question_categories.order` |
| 2023-04-12 14:30 | `2023_04_12_143016_create_view_to_agenda_de_envios.php` | cria a view `envios` |
| 2023-04-13 11:14 | `2023_04_13_111410_create_to_view_formulario.php` | cria a view `formulario` |
| 2023-05-29 13:33 / 13:38 | `2023_05_29_133353_respostas_revisoes_formato_24_horas.php`, `2023_05_29_133831_envios_revisoes_formato_24_horas.php` | formato de data 24h nas views |
| 2023-06-22 15:25 | `2023_06_22_152508_add_ratings_id.php` | adiciona `question_categories.ratings_id` |
| 2023-06-29 15:41 | `2023_06_29_154137_create_view_to_lista_revisoes.php` | cria a view `listagem_clientes` |
| 2023-06-30 11:45 | `2023_06_30_114520_add_collumns_to_send_rating.php` | adiciona `send_ratings.path` |
| 2024-11-01 15:02 | `2024_11_01_150206_create_modulos_table.php` | registra o módulo `revisoes` (id 34) |

## 5. Seeds

| Seed | O que popula |
|---|---|
| `v6/database/seeds/PrefeituraRevSeeder.php` (1621 linhas) | Cria a avaliação "Prefeitura" (slug `prefeitura`, `status = true`) e suas 21 categorias ("Itens Necessários - Turmalina - TCE/PB", "Despesas", "Receitas", "Licitações", "Contratos", "Convênios", "Pessoal", "Leis", "LGPD", "Usabilidade", "Série Histórica", "Outros", "Demonstrativos fiscais", "COVID-19" e suas variações) com dezenas de perguntas ponderadas (`ratings_id = 1`, `options_categories_id = 1`, `question_types_id = 1`) |
| `v6/database/seeds/CamaraRevSeeder.php` | Cria a avaliação "Câmara" (slug `camara`, `status = false`) e as categorias "Conteúdo", "Procedimentos licitatórios Orientação", "Contratos", "Transferência Recebida", "Despesas", "Pessoal", "Série Histórica", "Usabilidade", "Outros" (`ratings_id = 2`) |
| `v6/database/seeds/QuestionTypesSeeder.php` | Limpa `question_types` e insere "Resposta única" ("Múltipla escolha" está comentado) |
| `v6/database/seeds/OptionsSeeder.php` | Limpa `options` e insere Sim/Não/Parcial + escala de satisfação de "Insatisfeito" a "Muito Satisfeito" |
| `v6/database/seeds/OptionCategoriesSeeder.php` | Limpa `option_categories` e insere "Comum" e "Satisfação" |
| `v6/database/seeds/OptionsCategoryHasOptionSeeder.php` | Limpa a pivot e liga as opções às categorias (ids 1 e 2 fixos) |
| `v6/database/seeds/StatusesTableSeeder.php` | Popula `statuses` — **não é usado pelo módulo** (o status das revisões é booleano) |

Os dois seeders de revisão são chamados em
`v6/database/seeds/DatabaseSeeder.php:58-59` (`PrefeituraRevSeeder`, `CamaraRevSeeder`).

> **Risco de dados**: os seeders usam **ids fixos** (`ratings_id = 1`, `ratings_id = 2`,
> `question_categories_id` 1..34, `options_categories_id = 1`). Rodá-los em uma base já
> populada (ou fora de ordem) quebra as referências. Não há `truncate` nem transação.

## 6. Registro do módulo no CMS

| Tabela | Registro |
|---|---|
| `modulos` | id 34, `name = "Revisões"`, `slug = "revisoes"`, `status = 0` (`v6/database/migrations/2024_11_01_150206_create_modulos_table.php:227-232`) |
| `resources` | id 43 "Revisões" (slug `revisoes`) e id 44 "Listagem de Revisões (Cliente)" (slug `revisoes-cliente`), ambos `modulo_id = 34` |
| `menu_orders` | id 28, `slug = "revisoes"`, `modulo_id = 34` (contador de cliques via `menuClicks('revisoes')` / `menuClicks('revisoes-cliente')`) |

## 7. Permissões

O módulo usa o padrão do CMS: `has_access(['revisoes.create'])` nas views e o middleware
`check.route.permission` comparando o **nome da rota** com as permissões do usuário
(`v6/app/Http/Middleware/CheckRoutePermission.php:41-92`).

No banco de referência a tabela `permissions` possui apenas slugs genéricos
(`index`, `create`, `edit`, `delete`, `inactive`, `inactivate`, `restore`, `show`,
`publish`) e **nenhuma linha** para `revisoes.*`; na prática, `has_access(['revisoes.create'])`
só retorna `true` para `super-admin` (que faz curto-circuito em
`v6/app/Library/Helper/appHelpers.php:33-35`). Ver `08-GAPS-E-DIVIDAS.md`.
