# e-Vacina — Migração para Laravel Moderno

> Plano de reconstrução do módulo em **PHP 8.x + Laravel 11/12**.
> O módulo está **desativado** e sem consumidor no portal — a decisão
> de negócio (manter ou descontinuar) deve preceder a reconstrução.

---

## 1. Decisões de equivalência (o que NÃO pode mudar)

| Item | Regra |
|---|---|
| Tela única | `/cms/e-vacina` mostra a configuração corrente, sem listagem |
| Campos | população, URL da fonte de dados, imagem (banner) e o flag "Ativar/Desativar" |
| Importação | um comando/ação busca os dados externos e atualiza os contadores |
| Contadores | 1ª dose, 2ª dose, 1º reforço, 2º reforço |
| Agendamento | a atualização periódica continua mensal (ou mais frequente) |
| Módulo desligável | `modulos.status` continua controlando a visibilidade |

---

## 2. Schema de migração Laravel

```php
Schema::create('evacina_configs', function (Blueprint $table) {
    $table->id();
    $table->string('population')->default(0);           // populacao
    $table->string('source_url', 500)->nullable();       // url
    $table->string('image_path')->nullable();            // path_imagem
    $table->unsignedBigInteger('first_dose')->default(0);
    $table->unsignedBigInteger('second_dose')->default(0);
    $table->unsignedBigInteger('first_booster')->default(0);
    $table->unsignedBigInteger('second_booster')->default(0);
    $table->boolean('is_active')->default(false);        // status
    $table->timestamp('synced_at')->nullable();          // última importação
    $table->timestamps();
});
```

### 2.1 Alternativa: histórico por data

Se houver interesse em manter a série histórica (hoje as linhas antigas
existem na tabela mas não são lidas):

```php
Schema::create('evacina_snapshots', function (Blueprint $table) {
    $table->id();
    $table->foreignId('evacina_config_id')->constrained('evacina_configs')->cascadeOnDelete();
    $table->date('reference_date');
    $table->unsignedBigInteger('first_dose')->default(0);
    $table->unsignedBigInteger('second_dose')->default(0);
    $table->unsignedBigInteger('first_booster')->default(0);
    $table->unsignedBigInteger('second_booster')->default(0);
    $table->timestamps();

    $table->unique(['evacina_config_id', 'reference_date']);
});
```

### 2.2 Migração dos dados

```php
$last = DB::table('evacinas')->orderBy('id', 'desc')->first();

if ($last) {
    DB::table('evacina_configs')->insert([
        'population'   => $last->populacao,
        'source_url'   => $last->url,
        'image_path'   => $last->path_imagem,
        'first_dose'   => $last->primeira_dose ?? 0,
        'second_dose'  => $last->segunda_dose ?? 0,
        'first_booster'=> $last->primeira_dose_reforco ?? 0,
        'second_booster'=> $last->segunda_dose_reforco ?? 0,
        'is_active'    => (bool) $last->status,
        'created_at'   => now(), 'updated_at' => now(),
    ]);
}
```

---

## 3. Modelos

```php
// app/Models/EvacinaConfig.php
final class EvacinaConfig extends Model
{
    use HasFactory;

    protected $fillable = [
        'population', 'source_url', 'image_path',
        'first_dose', 'second_dose', 'first_booster', 'second_booster',
        'is_active', 'synced_at',
    ];

    protected $casts = [
        'population'    => 'float',
        'is_active'     => 'boolean',
        'synced_at'     => 'datetime',
        'first_dose'    => 'integer',
        'second_dose'   => 'integer',
        'first_booster' => 'integer',
        'second_booster'=> 'integer',
    ];
}
```

> `status = null` do legado deve ser migrado para `false` (o
> `filter_request` do legado gravava `null` quando a caixa era
> desmarcada).

---

## 4. Service: a importação

Extrair a importação do comando para um **Service** testável:

```php
// app/Services/Evacina/SheetImporter.php
final class SheetImporter
{
    public const DOSE_COLUMN = 28;   // nomeado, em vez de mágico

    public function __construct(private readonly HttpClient $http) {}

    public function import(EvacinaConfig $config): EvacinaSnapshot
    {
        [$sheetId, $gid] = $this->parseUrl((string) $config->source_url);

        $csv = $this->http->get("https://docs.google.com/spreadsheets/d/{$sheetId}/export", [
            'query' => ['format' => 'csv', 'gid' => $gid],
            'timeout' => 30,
            'headers' => ['User-Agent' => 'Prefeitura/1.0'],
        ])->body();

        $counts = $this->countDoses($csv);

        return EvacinaSnapshot::create([
            'evacina_config_id' => $config->id,
            'reference_date'     => today(),
            ...$counts,
        ]);
    }

    private function parseUrl(string $url): array
    {
        // "spreadsheets/d/<id>/...gid=<gid>"
        preg_match('#spreadsheets/d/([^/]+).*gid=(\d+)#', $url, $m);
        abort_if(empty($m), 422, 'URL da planilha inválida.');

        return [$m[1], $m[2]];
    }
}
```

> A URL vira **um** regex nomeado, com mensagem de erro clara, em vez
> de `explode` encadeado e silencioso.

---

## 5. Form Requests

```php
final class UpdateEvacinaConfigRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', EvacinaConfig::class);
    }

    public function rules(): array
    {
        return [
            'population' => ['required', 'numeric', 'min:1'],
            'source_url' => ['required', 'url', 'max:500'],
            'image'      => ['nullable', 'image', 'mimes:jpeg,png,jpg', 'max:4096'],
        ];
    }

    protected function prepareForValidation(): void
    {
        $this->merge(['is_active' => $this->boolean('is_active')]);
    }
}
```

> `is_active` passa a ser um **booleano explícito** (o legado derivava
> da presença da checkbox e gravava `null`).

---

## 6. Policies

```php
final class EvacinaConfigPolicy
{
    public function view(User $u, EvacinaConfig $c): bool
    {
        return $u->hasCapability('e-vacina.index');
    }

    public function update(User $u, EvacinaConfig $c): bool
    {
        return $u->hasCapability('e-vacina.edit');
    }

    public function refresh(User $u, EvacinaConfig $c): bool
    {
        return $u->hasCapability('e-vacina.edit');
    }
}
```

E o recurso (que **já existe** na v6 com o slug `e-vacina`):

| recurso | ação | descrição |
|---|---|---|
| `e-vacina` | `index` | Visualizar o painel de vacinação |
| `e-vacina` | `create` | Cadastrar configuração de vacinação |
| `e-vacina` | `edit` | Editar configuração de vacinação |
| `e-vacina` | `refresh` | Atualizar os dados de vacinação |

---

## 7. Endpoints REST/JSON sugeridos

| Método | URI | Descrição | Autorização |
|---|---|---|---|
| GET | `/api/e-vacina` | configuração corrente + contadores | `e-vacina.index` |
| PUT | `/api/e-vacina` | salva a configuração | `e-vacina.edit` |
| POST | `/api/e-vacina/refresh` | dispara a importação agora | `e-vacina.refresh` |
| GET | `/api/e-vacina/snapshots` | série histórica (se adotada) | `e-vacina.index` |

**Todas** as rotas exigem autenticação (`auth` + Policy) — corrigindo a
falha de segurança do legado.

---

## 8. API Resources

```php
final class EvacinaConfigResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'population'     => $this->population,
            'source_url'     => $this->source_url,
            'image_url'      => $this->image_path
                ? Storage::disk('public')->url($this->image_path) : null,
            'first_dose'     => $this->first_dose,
            'second_dose'    => $this->second_dose,
            'first_booster'  => $this->first_booster,
            'second_booster' => $this->second_booster,
            'is_active'      => $this->is_active,
            'synced_at'      => $this->synced_at?->toIso8601String(),
        ];
    }
}
```

---

## 9. Views Blade modernas

| Tela legada | v7 |
|---|---|
| `evacina/edit.blade.php` + `form.blade.php` | `<x-evacina-form :config="..." />` — **um** componente, com o `<form>` **envolvendo** o botão de submit |
| botão "Criar Banner Personalizado" | manter ou remover (depende de o portal consumir a imagem) |
| pré-visualização com `FileReader` | `<x-image-picker>` (Livewire) com preview nativo |

Correção obrigatória do bug do botão:

```blade
<x-form wire:submit="save">
    ...
    <x-button type="submit">Registrar</x-button>
</x-form>
```

---

## 10. Fila, cache e storage

| Recurso | Proposta v7 |
|---|---|
| Fila | `RefreshEvacinaData` como **job** (`ShouldQueue`), disparado após salvar e pelo scheduler. A requisição não espera o download do CSV. |
| Scheduler | `Schedule::job(new RefreshEvacinaData)->monthlyOn(1, '03:00')` (em `routes/console.php` ou `bootstrap/app.php`) |
| Cache | Nenhum (o dado é pequeno) |
| Storage | Disk `public`, caminho `evacina/images/{id}/banner.png` (manter) ou `evacina/banners/{uuid}.png` |
| CSV intermediário | Deixar de gravar o CSV em disco: usar `League\Csv` em memória |
| Logs | `Log::warning` em toda falha de importação; notificar o operador se a sincronização falhar |

---

## 11. Riscos e decisões de equivalência funcional

| Risco | Impacto | Mitigação |
|---|---|---|
| Exigir autenticação | quem usava a rota pública perde o acesso | é uma correção de segurança; comunicar |
| Trocar `float` por `decimal`/`bigint` na população | arredondamento | `decimal(12,2)` |
| `status` nullable -> `NOT NULL` | registros legados com `null` | backfill `false` |
| Migrar a tabela `evacinas` (com nomes em português) | reescrita dos models | `rename`/mapear em uma migration explícita |
| Deixar de gravar o CSV em disco | nada consome o arquivo | verificar que nenhum outro módulo usa `evacina/evacina.csv` |
| `is_active` explícito vs derivado da checkbox | mesmo comportamento | manter o checkbox na UI, enviar `is_active` |
| Coluna 28 do CSV | continua sendo um número mágico | parametrizar em `SheetImporter::DOSE_COLUMN` e **logar** o cabeçalho da coluna detectada |
| Descontinuar o módulo | o dado escrito deixa de ter destino | decisão de negócio antes de codificar |
