# Revisões — Regras de Negócio e Portabilidade

---

## 1. Regras de negócio não óbvias

### 1.1 Exclusividade da avaliação ativa

Só pode existir **uma** avaliação com `status = true`. O fluxo é sempre:

```php
// v6/app/Http/Controllers/Prefecture/RatingsController.php:96-103
if (isset($request->status)) {
    $this->reverseAllStates();   // desativa todas as que estavam ativas
    $status = true;
} else {
    $status = false;
}
```

`reverseAllStates()` (`RatingsController.php:249-256`) faz `Ratings::where('status', true)->get()`
e atualiza cada um para `false` — um UPDATE por linha. `setStatus()`
(`RatingsController.php:228-247`) inverte o estado da avaliação e, ao ativar, desativa as demais.

**Consequência**: a listagem de relatórios (`listagem_clientes`) e a "Listagem de Revisões
(cliente)" só enxergam a avaliação vigente.

### 1.2 A pontuação só reconhece "Sim"

Duas implementações independentes computam a nota, e **ambas** comparam
`$resposta->option->description == 'Sim'`:

| Onde | Arquivo |
|---|---|
| Ao gravar a resposta | `QuestionAnswersController::pontuacao()` (`v6/app/Http/Controllers/Prefecture/QuestionAnswersController.php:398-417`) |
| Ao gerar relatório/PDF | `EnviosRepository::returnResults()` (`v6/app/Repositories/Eloquent/Prefecture/Ratings/Envios/EnviosRepository.php:275-305`) |

Consequências:

- "Não" e "Parcial" valem **zero** — a coluna `options.value` ("0.5" para Parcial) é **ignorada**.
- Se a alternativa for renomeada (ex.: "Sim" → "SIM"), a pontuação zera silenciosamente.
- `pts_totais` = soma dos pesos de **todas** as perguntas da avaliação (independentemente de
  haver resposta); `pts_finais` = soma dos pesos das perguntas respondidas "Sim".
- O percentual do PDF é `round(100 * ($valorFinal / $valorTotal))`
  (`EnviosRepository.php:46`) — **divisão por zero** quando `pts_totais = 0` (avaliação sem
  perguntas ou perguntas sem peso). Em PHP 7.2 isso gera aviso e `INF`/`NAN`; **em PHP 8 lança
  `DivisionByZeroError`** — risco real de migração.

### 1.3 Protocolo

`$protocol = $ano . $mes . rand(1000, 10000);` (`QuestionAnswersController@store`) — sequência
numérica "AAAAMM" + aleatório, gravada em um `integer`. Em retificações o protocolo do envio
pai é reaproveitado (`$rating_pai->protocol`).

### 1.4 Retificação

`send_ratings.send_id` é uma auto-FK: um novo envio aponta para o envio que ele retifica. Ao
criar a retificação, o `protocol` é herdado e `id_pai` gravado em `send_id`. A view SQL
`envios`/`respostas` exibe "Retificado de {protocolo do pai}".

> Na v5 existia `QuestionAnswersController::update()`, que criava um **novo** envio
> (retificação) a partir do formulário de edição, e a view
> `v5/resources/views/prefecture/configurations/revisoes/respostas/create.blade.php` postava
> para `route('respostas.update', $content->id)` com `method_field('PUT')`.
> **O método foi removido na v6** (o `diff` entre
> `v5/app/Http/Controllers/Prefecture/QuestionAnswersController.php` e
> `v6/app/Http/Controllers/Prefecture/QuestionAnswersController.php` remove as linhas
> 182-246 da v5) e a view da v6 passou a postar **sempre** para `respostas.store`.
> A retificação **continua funcionando**: o formulário da v6 envia o `content_id` do envio
> original (`v6/resources/views/prefecture/configurations/revisoes/respostas/form.blade.php:61`)
> e o `store()` usa esse campo para herdar o `protocol` e gravar `send_id`.
> O que sobra é a **rota morta** `PUT /cms/respostas/{id}` (`respostas.update`), ainda
> registrada pelo `Route::resource` e apontando para um método inexistente.

### 1.5 A data do envio é sobrescrita no envio do e-mail

```php
// v6/app/Http/Controllers/Prefecture/QuestionAnswersController.php:308-309
$send->update(['status' => true]);
$send->update(['data' => new DateTime()]);
```

Ao disparar o e-mail, a **data original da avaliação é substituída pela data/hora do envio do
e-mail**. A coluna `data` deixa de representar "quando a avaliação foi feita" e passa a
representar "quando o relatório foi distribuído". A view `respostas` (`data_resposta`) usa
`sr.data`, e a view `envios` usa `sr.created_at` — logo as duas telas discordam entre si.

### 1.6 Regras de exclusão (proteção de integridade referencial)

| Ação | Regra | Onde |
|---|---|---|
| Excluir alternativa | bloqueado se a alternativa pertence a alguma categoria | `OptionsController@delete_options` |
| Excluir categoria de opções | bloqueado se há pergunta usando a categoria | `OptionsController@delete_options_categories` |
| Excluir opção de dentro de uma categoria | bloqueado se há pergunta usando a categoria | `OptionsController@delete_options_of_category` |
| Excluir categoria de pergunta | bloqueado se há pergunta usando a categoria ou se a categoria tem filhas | `OptionsController@delete_categories` |

As regras são verificadas **por query**, não por constraint de FK: apagar a categoria deixa as
perguntas órfãs (a FK impede o `DELETE` do banco, mas a verificação happens antes).

### 1.7 Importação / exportação do questionário

**Exportação** (`RatingsController@baixar_revisao`):

```php
$arquivo = "cache/revisao_$name.json";   // gravado em public/
```

O JSON tem a forma `{title, version, subtitle, description, slug, 0: [perguntas]}`
(as perguntas são empurradas no índice 0 do array PHP, o que serializa como chave `"0"`).

**Importação** (`RatingsController@importar_revisao`):

| Entidade | Critério de reaproveitamento |
|---|---|
| Avaliação | sempre nova, com `status = false` |
| Categoria de pergunta | por `title` **dentro da avaliação nova** (cria se não existir) |
| Tipo de pergunta | por `title` **global** (cria se não existir) |
| Categoria de opções | por `title` **global** (se já existir, **as opções do JSON são descartadas**) |
| Opções | criadas **apenas** quando a categoria de opções é nova |

Não há transação: uma falha no meio deixa avaliação, tipos e opções órfãos.

### 1.8 Cópia de avaliação

`copiar_revisao` duplica a avaliação (título + " (copia)", `status = false`) e as perguntas
(campos `value`, `description`, `question_types_id`, `options_categories_id`,
`question_categories_id`). **Não copia** as categorias de pergunta nem as opções — a cópia
fica com perguntas apontando para as categorias da avaliação original.

### 1.9 Exclusão em lote

`deleteByLote` registra no activity log ("Exclusão de revisões") com a lista de ids e delega
para `IRatingsRepository::destroyAll()`. Como o `ratings` tem `SoftDeletes`, a remoção é lógica.

## 2. Acoplamentos

| Acoplamento | Direção | Observação |
|---|---|---|
| `Configuration` (`titulo_site`, `nome_prefeito`, `logo`, `favicon`, `cor_1`, `estado`) | módulo → configuração | Uso direto do model, sem cache |
| `Supplier` (`fornecedor_ativo`) | módulo → fornecedores | Aparece no PDF |
| `Admin\User` + `Sentinel::getUser()` | módulo → usuários | Nome do revisor e remetente do e-mail |
| `Admin\Controller` (base de `RatingsClientsController`) | módulo → base do CMS | Traz `getTemplateActived()` etc. |
| `AppTrait` (via `Repository`) | módulo → traits | `createIMGraph()`, `filter_request()`, `toBRDataFormat()` |
| `menu_orders` | módulo → métricas | `menuClicks('revisoes')` / `menuClicks('revisoes-cliente')` |
| `modulos` / `resources` | módulo → registro | Exibição no menu do CMS |
| Formulários Dinâmicos | **nenhum** | Apenas homonímia de "formulário" |

## 3. Armadilhas técnicas (confirmadas no código)

| # | Armadilha | Evidência | Impacto |
|---|---|---|---|
| A1 | **Divisão por zero na nota** | `EnviosRepository::generatePDF()`: `round(100 * ($valorFinal / $valorTotal))` | PHP 8 lança `DivisionByZeroError`; hoje gera `INF` no PDF |
| A2 | **Divergência de namespace nos models** | `v6/app/Http/Model/Prefecture/SendRating.php:3` declara `namespace App\Prefecture;`, mas 4 arquivos importam `App\Http\Model\Prefecture\SendRating` (`RatingsController`, `RatingsClientsController`, `QuestionAnswersController`, `EnviosRepository`) e o `vendor/composer/autoload_classmap.php:277` ainda mapeia o nome antigo | Num `composer dump-autoload` limpo, `App\Http\Model\Prefecture\SendRating` deixa de existir → *class not found*. O mesmo vale para `EmailRevisoes` (classe mapeada em `autoload_classmap.php:256` com namespace antigo, arquivo hoje com `App\Prefecture`) |
| A3 | **`version` fora do `$fillable` e nunca gravada** | `Ratings::$fillable` não inclui `version`; `RatingsController@baixa_revisao:303` lê `$rating->version` | A coluna é sempre `NULL` e o JSON exportado carrega `"version": null` |
| A4 | **Rota `respostas.update` sem método** | `v6/routes/basepro.php:282-285` registra o resource completo; `QuestionAnswersController` não implementa `update()` na v6 (o método existia na v5) | `PUT /cms/respostas/{id}` → `BadMethodCallException`. A retificação em si continua funcionando via `respostas.store` com `content_id` |
| A5 | **Rotas do resource `revisoes/relatorios` sem métodos** | `v6/routes/basepro.php:221`; `RatingsClientsController` só tem `index`, `show`, `edit` | `create`, `store`, `update`, `destroy` quebram |
| A6 | **`revisoes.show` sem método** | resource `revisoes` gera `show`; `RatingsController` não tem `show()` | `GET /cms/revisoes/{id}` quebra |
| A7 | **Nome de rota duplicado** | `respostas.index` é registrado duas vezes (resource na linha 282 e rota explícita na linha 291) | A rota explícita sobrescreve a do resource; `QuestionAnswersController` não tem `index()`, mas funciona por causa da sobrescrita |
| A8 | **`/cms/listagem/revisoes/graficos/{id}` fora do resource** | `v6/routes/basepro.php:225` | Rota "órfã", sem nome e fora do padrão `revisoes.*`; o controller renderiza `prefecture.configurations.revisoes.listagem.send_estatistica` (delegue de uma linha) |
| A9 | **Escrita de arquivo no `public/`** | `baixa_revisao` grava `public/cache/revisao_*.json` | Artefato não versionado no diretório público; em `public/` somente-leitura quebra |
| A10 | **Uso de superglobais** | `enviar_email` lê `$_SERVER["REMOTE_ADDR"]` e `$_SERVER['HTTP_USER_AGENT']` | Depende do servidor; em testes/CLI quebra |
| A11 | **Paginação inexistente** | `Questions::where('ratings_id', $id)->get()` em `createDeEnvios`, `edit`, `relatorios_get` | Carrega todas as perguntas da avaliação em memória (a avaliação "Câmara" tem ~80 perguntas) |
| A12 | **N+1 no relatório** | `EnviosRepository::returnResults()` faz `AnswerForm::where('send_id', $id)->where('questions_id', $pergunta->id)->with('option')->first()` **dentro** de um loop sobre perguntas | Uma query por pergunta por relatório |
| A13 | **Atualização linha a linha** | `reverseAllStates()` e `question_order()`/`categories_order()` fazem um UPDATE por item | Many queries para reordenar |
| A14 | **Parsing de string como protocolo de dados** | As respostas chegam como `"<question_id>=> <option_id>"` e são quebradas por `explodeInPosition()` (`QuestionAnswersController@explodeInPosition`) | Acoplamento forte entre o HTML do formulário e o controller; `str_replace(' ', '')` remove espaços indevidamente |
| A15 | **Datas por `IntlDateFormatter` + `explode(',')`** | `formatarDataPorExtenso($content->data)` e depois `explode(',', formatarDataPorExtenso($content->data))[1]` (`v6/app/Http/Controllers/Prefecture/QuestionAnswersController.php:332` e `v6/app/Http/Controllers/Prefecture/RatingsClientsController.php:50`) | Frágil: depende do formato do helper (`v6/app/Helpers/functions.php:900`), que internamente faz `explode(" ", $formatter->format(date_create($data)))` e devolve "dia da semana, dia do mês de mês do ano" |
| A16 | **`Status`/`StatusCategory` não usados** | Tabelas `statuses`/`status_categories` semeadas, mas o módulo usa booleanos | Confusão conceitual: o "status" do módulo não é o `statuses` do CMS |
| A17 | **Atributo de grupo `'before' => 'csrf'`** | `v6/routes/basepro.php:35` | Não é chave reconhecida pelo Router do Laravel 5.5; o CSRF é garantido pelo grupo `web` (`v6/app/Providers/RouteServiceProvider.php:84-88`) — o atributo é redundante |
| A18 | **Recurso `respostas` com `check.logged` mas sem `check.route.permission`** | `QuestionAnswersController` não declara o middleware | Qualquer usuário logado (não só quem tem a permissão `revisoes.*`) acessa as telas de resposta |
| A19 | **`formulario` view usada para duas listagens** | `DatatableController@get_revisoes_formulario` e `@get_revisoes_envios` passam a permissão `revisoes` para `struct_views` | `get_listagem_revisoes` passa `pagina-estatica` (copy/paste) — os botões da listagem do cliente usam a permissão do módulo errado |
| A20 | **Import sem transação** | `importar_revisao` insere em várias tabelas | Falha parcial deixa dados inconsistentes |

## 4. Código duplicado ou copiado

| Duplicação | Onde |
|---|---|
| Cálculo de pontuação | `QuestionAnswersController::pontuacao()` × `EnviosRepository::returnResults()` |
| Cálculo de gráficos | `EnviosRepository::generatePDF()` monta doughnut por categoria; `QuestionAnswersController@relatorios_get` e `RatingsClientsController@edit` montam a mesma tela (mesma view) |
| Extração da árvore de categorias | `RatingsController@childrens()`/`@loopfilhos()` e `RevisoesPresenter::pegarFilhos()` fazem a mesma travessia recursiva |
| Telas de estatística | `envios/send_estatistica.blade.php` e `listagem/send_estatistica.blade.php` (delegate) |
| `getEnvios` | `RatingsClientsController@getEnvios` e `QuestionAnswersController@getEnvios` delegam ao mesmo repository |

## 5. Divergências v5 → v6 (resumo)

| Aspecto | v5 | v6 |
|---|---|---|
| `RatingsController`, `RatingsClientsController`, `QuestionController`, `OptionsController` | idênticos (`diff` sem diferenças) | idênticos |
| `QuestionAnswersController` | tinha `update()` (retificação) | **`update()` removido** |
| Rotas | `respostas.update` nomeado explicitamente no `names()` do resource | nome removido do array `names()` (mas a rota continua existindo com o nome default) |
| Models | mesmo conteúdo de `Ratings`/`SendRating` | idêntico |
| Módulos do CMS | — | módulo `revisoes` registrado na tabela `modulos` (id 34) a partir de 2024-11-01 |

Detalhamento completo em `08-GAPS-E-DIVIDAS.md`.
