# LGPD (Barra de Privacidade) — Migração para Laravel Moderno

> **Alvo:** PHP 8.x + Laravel 11/12 ("v7")
> **Origem:** `v6` (Laravel 5.5.50), módulo `lgpd`

---

## 1. Decisão de modelagem (a mais importante)

No legado, a LGPD é um conjunto de **linhas genéricas** em `configurations`.
Isso é frágil (sem tipo, sem validação, `key` sem índice único, leitura
espalhada em providers e views). A v7 deve dar ao módulo uma entidade própria,
mantendo uma camada de compatibilidade para leitura das chaves antigas.

### 1.1 Nova tabela

```php
// migration: create_privacy_bars_table.php (ilustrativo)
Schema::create('privacy_bars', function (Blueprint $table) {
    $table->id();
    $table->foreignId('prefecture_id')->constrained()->cascadeOnDelete();
    $table->boolean('enabled')->default(false);
    $table->longText('text')->nullable();          // HTML sanitizado
    $table->unsignedSmallInteger('expiry_days')->default(7);
    $table->string('button_text_color', 32)->default('#ffffff');
    $table->string('cookie_name', 64)->default('acceptCookies');
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
});
```

### 1.2 Tabela de consentimento (novo, resolve a lacuna jurídica)

```php
// migration: create_privacy_bar_consents_table.php (ilustrativo)
Schema::create('privacy_bar_consents', function (Blueprint $table) {
    $table->id();
    $table->foreignId('privacy_bar_id')->constrained()->cascadeOnDelete();
    $table->string('consent_key', 64)->index();     // uuid do cookie
    $table->string('ip_address', 45)->nullable();
    $table->string('user_agent', 512)->nullable();
    $table->string('text_version', 64)->nullable(); // hash do texto exibido
    $table->timestamp('accepted_at');
    $table->timestamps();
});
```

Isso responde à lacuna apontada em `08-GAPS-E-DIVIDAS.md`: o aceite passa a
ser **prova de consentimento** auditável, e não apenas um cookie.

### 1.3 Migração dos dados existentes

```php
// migration: migrate_privacy_bar_configs.php (ilustrativo)
DB::transaction(function () {
    $rows = DB::table('configurations')
        ->join('category_configurations', 'category_configurations.id', '=', 'configurations.category_configurations_id')
        ->where('category_configurations.slug', 'barra-privacidade')
        ->whereNull('configurations.deleted_at')
        ->pluck('value', 'key');

    DB::table('privacy_bars')->insert([
        'prefecture_id' => 1,
        'enabled' => (int) ($rows['barra-privacidade-exibir'] ?? 0) === 1,
        'text' => $rows['barra-privacidade'] ?? null,
        'expiry_days' => (int) ($rows['barra-privacidade-expiracao'] ?? 7),
        'button_text_color' => $rows['barra-privacidade-botao'] ?? '#ffffff',
        'created_at' => now(), 'updated_at' => now(),
    ]);
});
```

**Decisão de equivalência:** preservar `modulos.status` do módulo `lgpd` como
um campo `enabled` da tabela `privacy_bars`? Recomendação: **não**. Na v7 o
módulo deixa de existir como entidade; o portal decide exibir a barra pelo
próprio `privacy_bars.enabled`. Documentar a mudança de default (o legado
nascia inativo) para o negócio.

## 2. Modelos

```php
// app/Models/PrivacyBar.php (ilustrativo)
final class PrivacyBar extends Model
{
    use HasFactory;

    protected $fillable = [
        'prefecture_id', 'enabled', 'text', 'expiry_days',
        'button_text_color', 'cookie_name', 'published_at',
    ];

    protected function casts(): array
    {
        return [
            'enabled' => 'boolean',
            'expiry_days' => 'integer',
            'published_at' => 'datetime',
        ];
    }

    public function consents(): HasMany
    {
        return $this->hasMany(PrivacyBarConsent::class);
    }
}
```

```php
// app/Models/PrivacyBarConsent.php (ilustrativo)
final class PrivacyBarConsent extends Model
{
    protected $fillable = [
        'privacy_bar_id', 'consent_key', 'ip_address',
        'user_agent', 'text_version', 'accepted_at',
    ];

    protected function casts(): array
    {
        return ['accepted_at' => 'datetime'];
    }
}
```

## 3. Services

```php
// app/Services/PrivacyBarService.php (ilustrativo)
final class PrivacyBarService
{
    public function __construct(private readonly PrivacyBarRepository $repository) {}

    public function current(): ?PrivacyBar
    {
        return $this->repository->current();
    }

    public function registerConsent(PrivacyBar $bar, Request $request): PrivacyBarConsent
    {
        return $this->repository->createConsent($bar, [
            'consent_key' => (string) Str::uuid(),
            'ip_address' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'text_version' => hash('sha256', (string) $bar->text),
            'accepted_at' => now(),
        ]);
    }
}
```

Cache sugerido: `Cache::remember('privacy-bar', 300, fn () => PrivacyBar::first())`,
invalidado no `saved`/`deleted` do model (evento de Eloquent) ou via
`Cache::forget` no controller de atualização.

## 4. Form Requests

```php
// app/Http/Requests/UpdatePrivacyBarRequest.php (ilustrativo)
final class UpdatePrivacyBarRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('update', PrivacyBar::class);
    }

    public function rules(): array
    {
        return [
            'enabled' => ['required', 'boolean'],
            'text' => ['nullable', 'string', 'max:5000'],
            'expiry_days' => ['required', 'integer', 'min:1', 'max:365'],
            'button_text_color' => ['required', 'string', 'regex:/^#[0-9a-fA-F]{6}$/'],
        ];
    }
}
```

Ganhos sobre o legado: validação real de cor (hex) e de prazo (inteiro 1..365),
hoje inexistentes; `text` limitado; sanitização do HTML na aplicação
(`strip_tags` com allowlist, ou `Purifier`).

## 5. Policies / autorização

```php
// app/Policies/PrivacyBarPolicy.php (ilustrativo)
final class PrivacyBarPolicy
{
    public function viewAny(User $user): bool
    {
        return $user->hasPermissionTo('lgpd.view');
    }

    public function update(User $user): bool
    {
        return $user->hasPermissionTo('lgpd.update');
    }
}
```

| Contexto | Permissão legado | Permissão v7 sugerida |
|---|---|---|
| Ler a tela do CMS | `barra-privacidade.edit` | `lgpd.view` |
| Salvar | `configuracoes.edit` | `lgpd.update` |
| Ver no menu | `barra-privacidade.index` + `modulos.status` | `lgpd.view` |

Isso elimina a assimetria A5 do legado (leitura e gravação com permissões
diferentes).

## 6. Endpoints REST/JSON sugeridos

| Método | URI | Nome | Descrição |
|---|---|---|---|
| GET | `/api/privacy-bar` | `api.privacy-bar.show` | Estado público: `{enabled, text, expiry_days, cookie_name}`. |
| POST | `/api/privacy-bar/consent` | `api.privacy-bar.consent` | Registra o aceite; grava o cookie com `Secure`/`SameSite=Lax`/`HttpOnly`. |
| GET | `/admin/privacy-bar` | `admin.privacy-bar.edit` | Tela do CMS (formulário). |
| PUT | `/admin/privacy-bar` | `admin.privacy-bar.update` | Salva as configurações. |

Exemplo de resposta pública:

```json
{
  "enabled": true,
  "text": "<p>Ao continuar navegando no nosso portal, você concorda com a nossa Política de Privacidade.</p>",
  "expiry_days": 7,
  "cookie_name": "acceptCookies"
}
```

## 7. Recursos de API

```php
// app/Http/Resources/PrivacyBarResource.php (ilustrativo)
final class PrivacyBarResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'enabled' => $this->enabled,
            'text' => $this->text,
            'expiry_days' => $this->expiry_days,
            'cookie_name' => $this->cookie_name,
            'button_text_color' => $this->button_text_color,
        ];
    }
}
```

## 8. Views Blade modernas

| Antes (v6) | Depois (v7) |
|---|---|
| `v6/resources/views/portal/layouts/barra-privacidade/barra-privacidade.blade.php` | `<x-portal.privacy-bar />` (componente anônimo) |
| `v6/resources/views/portal/layouts/barra-privacidade/head.blade.php` | CSS em `resources/css/portal/privacy-bar.css` (Vite), sem `@inject` e sem query por renderização |
| `v6/resources/views/portal/layouts/barra-privacidade/scripts.blade.php` | Alpine.js no componente (`x-data`, `x-show`) |
| `v6/resources/views/prefecture/configurations/barra-privacidade/edit.blade.php` + `form.blade.php` | `resources/views/admin/privacy-bar/edit.blade.php` com `<x-form-field>` |

Exemplo do componente (ilustrativo):

```blade
@if ($bar?->enabled)
    <div x-data="{ accepted: false }" class="privacy-bar" x-show="! accepted">
        <div class="container">
            {!! $bar->text !!}
            <button type="button" class="btn btn-success privacy-bar__accept"
                    @click="accept">
                Ok, entendi!
            </button>
        </div>
    </div>
@endif
```

## 9. Fila, cache e storage

| Aspecto | Decisão |
|---|---|
| Cache | `Cache::remember('privacy-bar', 300, fn () => PrivacyBar::first())`; invalidado ao salvar. |
| Fila | O registro do consentimento pode ser enfileirado (`ShouldQueue`) para não bloquear a resposta; o cookie, porém, deve ser emitido **sincronamente**. |
| Storage | Nenhum arquivo do módulo (o texto é HTML no banco). |
| Sessão | Não usada (o legado também não usa). |

## 10. Cookies e cabeçalhos (v7)

```php
$response = $response->withCookie(cookie(
    name: $bar->cookie_name,          // 'acceptCookies' (equivalência funcional)
    value: $consent->consent_key,
    minutes: $bar->expiry_days * 24 * 60,
    secure: true,
    httpOnly: true,                    // improvement: o valor não precisa ser lido pelo JS
    sameSite: 'lax',
    path: '/',
));
```

> **Equivalência vs melhoria:** manter o nome `acceptCookies` e a
> semântica em dias. `HttpOnly` impede o JS de ler o cookie — se o JS ainda
> precisar consultar o estado de aceite, usar um endpoint
> `GET /api/privacy-bar/consent-status` ou um atributo `data-accepted` no HTML.

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

| # | Risco | Mitigação |
|---|---|---|
| M1 | Quebrar o aceite já registrado nos navegadores dos cidadãos (cookie `acceptCookies`). | Manter nome e escopo do cookie; em caso de troca do formato, considerar prazo de convivência de 30 dias lendo o cookie antigo. |
| M2 | Perder o texto HTML customizado por município na migração de dados. | Ler de `configurations` na migração e versionar o HTML original (sanitizado). |
| M3 | Mudar o default de "módulo inativo" para ativo e exibir a barra sem texto em ambientes migrados. | Migrar `enabled` a partir de `barra-privacidade-exibir` **e** do `modulos.status`; nunca exibir barra sem texto. |
| M4 | Ajustar o texto da política e quebrar a prova de consentimento anterior. | Guardar `text_version` (hash) no consentimento; comparar na auditoria. |
| M5 | Sanitizar o HTML e quebrar formatação rich text legítima (links, `<strong>`). | Allowlist de tags (`p`, `a`, `strong`, `em`, `ul`, `ol`, `li`, `br`) e forbidding de `script`/`on*`. |
| M6 | Performance: o head fazia 5 queries por página. | Unificar em uma consulta (ou cache) via View Composer único. |
| M7 | Regressão no menu do CMS (item "LGPD" sumindo). | O item passa a depender apenas da policy `lgpd.view`, sem `modulos.status`. |
| M8 | `GET /getPrivacyBar` ser usado por scripts de terceiros/municípios. | Manter a rota pública como alias do novo endpoint durante a transição. |
| M9 | Loss de granularidade: o modelo de "um cookie booleano" não é consentimento granular por finalidade. | Registrar em `privacy_bar_consents` a versão do texto e o evento; planejar um Consent Management Platform no escopo do privacy office. |
| M10 | Divergência de nomenclatura: `modulos.slug = 'lgpd'` é referenciado pelo cache e por links. | Manter o slug no menu e nos logs durante a transição; marcar como legado. |
