# e-Vacina — Regras e Portabilidade

> Regras de negócio não óbvias, acoplamentos e armadilhas do módulo.

---

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

### 1.1 A tela mostra sempre o registro **mais recente**

`index()` faz `Evacina::latest()->first()` — `latest()` ordena por
`created_at`. Como o `store` **cria** uma linha a cada gravação, a
tabela funciona como um log de configurações e a tela mostra a última.

### 1.2 O `update` ignora o id da URL

O `update` busca o registro mais recente e atualiza **ele**, ignorando
o `{e_vacina}` da rota. Na prática, o `PUT /cms/e-vacina/1` do
formulário (id fixo) atualiza o registro mais recente.

### 1.3 O `status` vem da **presença** da checkbox

```php
$request->merge(["status" => isset($request->status_evacina)]);
```

Desmarcado -> `false` no request -> `filter_request` converte para
**`null`** (coluna nullable). Marcado -> `true`.

O comando `reload:evacina` só atualiza os contadores quando
`$last_evacina->status` é verdadeiro — logo, **desmarcar a caixa impede
a atualização dos contadores**.

### 1.4 Os contadores vêm de uma coluna fixa do CSV

O comando conta os valores da **coluna de índice 28** de cada linha do
CSV exportado do Google Sheets:

| Valor na coluna 28 | Contador |
|---|---|
| `Dose` | `primeira_dose` |
| `1ª Dose` | `primeira_dose` |
| `2ª Dose` | `segunda_dose` |
| `Dose Adicional` | `primeira_dose_reforco` |
| `Reforço` | `primeira_dose_reforco` |
| `2ª Reforço` | `segunda_dose_reforco` |

> Não há validação de que a coluna 28 é a correta. Se a planilha mudar
> de layout, os contadores passam a significar outra coisa — **sem erro
> nem aviso**.

### 1.5 O módulo está **desativado** por padrão

`modulos.id = 5`, `status = false`. O item de menu só aparece se o
módulo for ligado em `/cms/modulos` **e** o perfil tiver
`e-vacina.index`.

### 1.6 O "banner" não é um banner do portal

O `imagem_evacina` é gravado em `evacina/images/{id}/banner.png` — um
caminho próprio do módulo, **não** na galeria de conteúdos do portal. O
botão "Criar Banner Personalizado" apenas gera a imagem no cliente.

---

## 2. Acoplamentos

### 2.1 Dependências de saída

| Dependência | Tipo | Onde |
|---|---|---|
| **Google Sheets** (CSV público) | externa | `ReloadEvacina` |
| `IEvacinaRepository` / `EvacinaRepository` | interno | controller |
| `App\Console\Kernel` (scheduler) | interno | agendamento mensal |
| `Storage` (disk `public`) | Laravel | CSV e banner |
| `Artisan` (chamada síncrona) | Laravel | controller |
| `modulos` / `resources` | interno | menu e permissões |

### 2.2 Dependências de entrada

| Consumidor | Dependência | Risco |
|---|---|---|
| Menu do AdminLTE | `route('e-vacina.index')` + `$modulos['e-vacina']` | item some |
| Scheduler | `reload:evacina` | comando renomeado quebra |
| Portal público | **nenhum** | — |

> O módulo é **folha** e, na prática, **órfão**: ninguém consome a
> tabela `evacinas` além do próprio módulo.

---

## 3. Armadilhas

### 3.1 [CRÍTICO] O módulo é público

`Route::resource('e-vacina', ...)` sem `check.logged`, e o
`EvacinaController::__construct()` não chama `parent::__construct()`
(logo, o `check.route.permission` da classe base
`Prefecture\Controller` não é registrado).

Consequência: qualquer visitante pode **ler** a URL da planilha, a
população e o banner, e **gravar** novos valores (basta um token CSRF,
que é emitido para qualquer página que use formulários).

### 3.2 [ALTO] Quatro das sete rotas apontam para métodos inexistentes

`create`, `show`, `edit` e `destroy` **não existem** em
`EvacinaController` -> HTTP 500 em
`GET /cms/e-vacina/create`, `GET /cms/e-vacina/{id}`,
`GET /cms/e-vacina/{id}/edit` e `DELETE /cms/e-vacina/{id}`.

### 3.3 [ALTO] O botão "Registrar" não submete o formulário

Em `v6/resources/views/prefecture/configurations/evacina/edit.blade.php`,
o `<form>` é aberto dentro do `form.blade.php` (included na `.row`) e
**fechado** depois do `box-footer`. O botão `#btn-submit` está, portanto,
**fora** do `<form>` e, sem `type="submit"` nem `form="..."`, o clique
**não submete nada**.

### 3.4 [ALTO] `update` quebra quando não há nenhum registro

```php
if(!($last_evacina = $this->evacina_repo->getModel()->latest()->first()))
    $this->evacina_repo->error("Evacina", 'update');

if($last_evacina->id && ...)
```

O `error()` apenas dispara um flash e **não** interrompe o fluxo: a
linha seguinte acessa `$last_evacina->id` sobre `null` -> **erro fatal**.

### 3.5 [ALTO] O comando engole todas as exceuções

```php
} catch (\Throwable $th) {
    // Ignorando Erro
}
```

Falha na URL, na planilha, no download, na contagem — **nada** é
registrado. O operador não sabe que os contadores estão desatualizados
há semanas.

### 3.6 [MÉDIO] `file_get_contents` sem timeout e sem User-Agent

O download do CSV usa `file_get_contents` na URL remota. Não há timeout
-> uma planilha lenta trava a requisição (e o comando agendado). Não há
User-Agent -> alguns servers podem recusar.

### 3.7 [MÉDIO] Coluna fixa (28) sem validação

Se a planilha mudar de layout, os contadores passam a contar outra
coisa, sem erro. O número mágico `28` está "hardcoded" em duas linhas
do comando.

### 3.8 [MÉDIO] O CSV é lido de um caminho assumido

`Storage::put('evacina/evacina.csv', $csv)` grava no disk padrão
(`public`, presumivelmente), mas a leitura usa
`storage_path('app/public/evacina/evacina.csv')` — o caminho **assume**
o symlink `public/storage` -> `storage/app/public`. Se o disk padrão
for outro, a leitura falha (e o erro é engolido).

### 3.9 [MÉDIO] `status = false` vira `null`

`filter_request` converte valores falsos em `null`. A coluna é
nullable, então funciona, mas o estado fica ambíguo (`true`/`null`, sem
`false`).

### 3.10 [MÉDIO] `Repository::upInsert` faz `dd($th)`

`v6/app/Repositories/Repository.php:64-71` — a exceção vaza na tela.
Com `populacao = 0` (falsy) virando `null` numa coluna NOT NULL, o
erro é provável.

### 3.11 [BAIXO] `use illuminate\Database\Eloquent\SoftDeletes;`

`v6/app/Http/Model/Prefecture/Evacina.php` — "illuminate" minúsculo.
Funciona (case-insensitive), mas quebra análise estática e é um erro de
estilo.

### 3.12 [BAIXO] `title` como campo oculto com valor fixo

`<input type="hidden" value="e-Vacina" name="title">` — vestígio de um
formulário compartilhado. Não é gravado (não está no `$fillable`
efetivo do repository), mas é enviado.

### 3.13 [BAIXO] Sem auditoria

Nenhuma chamada a `activity()`. Quem alterou os dados de vacinação não
fica registrado.

### 3.14 [BAIXO] Sem flash de feedback

Nem o `store` nem o `update` emitem `flash()` de sucesso. O operador
não recebe confirmação.

### 3.15 [BAIXO] A tabela cresce a cada gravação

O `store` sempre **cria** uma linha (`updateOrCreate(['id' => null])`).
Não há limpeza nem unicidade.

### 3.16 [BAIXO] A URL do Google é guardada com credenciais embutidas

A URL de export é `https://docs.google.com/spreadsheets/d/<id>/export?format=csv&gid=<n>`
— o id da planilha fica no banco. Não é segredo (é a mesma URL usada
para compartilhar), mas é um identificador de um documento que pode ser
privado por engano.

---

## 4. Código duplicado entre v5 e v6

| Arquivo | v5 vs v6 |
|---|---|
| `app/Http/Controllers/Prefecture/EvacinaController.php` | **idêntico** |
| `app/Http/Model/Prefecture/Evacina.php` | **idêntico** |
| `app/Http/Requests/EvacinaRequest.php` | **idêntico** |
| `app/Repositories/Eloquent/Prefecture/Evacina/EvacinaRepository.php` | **idêntico** |
| `app/Console/Commands/ReloadEvacina.php` | **idêntico** |
| `resources/views/prefecture/configurations/evacina/{edit,form}.blade.php` | **idênticos** |
| `database/migrations/2022_09_06_091951_create_evacinas_table.php` | **idêntica** |

O módulo é **byte-a-byte idêntico** entre v5 e v6. A única diferença no
contexto é o registro de módulos (`modulos` id 5, `status = false`),
introduzido na v6.

---

## 5. Onde a v6 divergiu (e por quê)

| Mudança | Efeito |
|---|---|
| Registro de módulos (id 5, `status = false`) | o módulo passa a poder ser **ligado/desligado**; o item de menu só aparece quando ligado |
| `resources` ganhou `modulo_id` | o recurso `e-vacina` (id 46) passa a ser filtrado por módulo ativo na matriz de permissões |
| Preset `padrao` do formulário de módulos inclui `e-vacina` | ao escolher a estrutura "padrão", o módulo é marcado como **ativo** |

---

## 6. Riscos de migração de schema

| Risco | Detalhe | Mitigação |
|---|---|---|
| `status` nullable com valores `true`/`null` | estado ambíguo | `NOT NULL DEFAULT false` + cast |
| Sem índice e com `latest()->first()` | full scan a cada leitura | índice em `created_at` ou `id` |
| Tabela com uma linha por gravação | crescimento e confusão | modelo singleton (`is_current`) ou tabela de configuração com chave |
| `path_imagem` relativo ao disk | quebra se o symlink mudar | `Storage::url()` |
| Contadores dependentes de coluna fixa (28) | silenciosamente errados | versionar o "layout" da planilha ou migrar para uma API |
| Dependência externa (planilha) | fora do ar -> contadores congelados | timeout + log + aviso na UI |
| Comando sem logging | falhas invisíveis | `Log::warning` em vez de catch vazio |
| Módulo sem consumidor no portal | dado escrito e nunca lido | decidir o futuro do módulo |

---

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

1. **O e-Vacina ainda é relevante?** A pandemia de COVID-19 acabou; o
   módulo está desativado por padrão e nada consome a tabela na v6.
   Manter, migrar para um painel genérico de "indicadores", ou remover?
2. **A planilha do Google é a fonte oficial?** Se a prefeitura
   usar um sistema próprio, a importação precisa ser reescrita.
3. **A coluna 28 é estável?** Quem mantém a planilha?
4. **Os dados devem ser públicos?** Não há consumidor no portal, mas
   a demanda por transparência sanitária foi real.
5. **O que acontece se a planilha for movida ou renomeada?** O link
   fica invalidado e o módulo para de funcionar em silêncio.
6. **Deve haver histórico de contagens por data?** Hoje só o último
   valor é preservado (as linhas antigas existem, mas não são lidas).

---

## 8. Checklist de correção (priorizado)

| Prioridade | Item | Arquivo |
|---|---|---|
| P0 | Exigir autenticação em todas as rotas (`check.logged` + `check.route.permission`) | `v6/routes/basepro.php:201` + `v6/app/Http/Controllers/Prefecture/EvacinaController.php:25-28` |
| P0 | Corrigir o botão "Registrar" (dentro do `<form>` ou com `form="id"`) | `v6/resources/views/prefecture/configurations/evacina/edit.blade.php` |
| P0 | Corrigir o `update` para não quebrar sem registro | `v6/app/Http/Controllers/Prefecture/EvacinaController.php:58-64` |
| P1 | Registrar as falhas do comando (em vez de `catch` vazio) | `v6/app/Console/Commands/ReloadEvacina.php` |
| P1 | Adicionar timeout e User-Agent no download do CSV | mesmo arquivo |
| P1 | Remover as 4 rotas geradas sem método (`Route::resource` -> rotas explícitas) | `v6/routes/basepro.php:201` |
| P1 | Corrigir `Repository::upInsert` (não usar `dd($th)`) | `v6/app/Repositories/Repository.php:64-71` |
| P2 | `status` NOT NULL DEFAULT false + cast boolean | nova migration + `v6/app/Http/Model/Prefecture/Evacina.php` |
| P2 | Validar `url` como URL e `populacao` como numérica | `v6/app/Http/Requests/EvacinaRequest.php` |
| P2 | Validar `imagem_evacina` (image, tamanho) | mesmo arquivo |
| P2 | Corrigir o import `illuminate` minúsculo | `v6/app/Http/Model/Prefecture/Evacina.php` |
| P2 | Parametrizar o índice da coluna do CSV (constante nomeada) | `v6/app/Console/Commands/ReloadEvacina.php` |
| P2 | Índice em `created_at` | nova migration |
| P3 | Emitir flash de sucesso/erro no `store`/`update` | `v6/app/Http/Controllers/Prefecture/EvacinaController.php` |
| P3 | Remover o `input hidden` `title` | `v6/resources/views/prefecture/configurations/evacina/form.blade.php` |
| P3 | Registrar auditoria (`activity()`) | mesmo controller |
