# Newsletters — Gaps, Riscos e Dívidas

> **Módulo:** `newsletters` (tabela `modulos`, id 53)
> **Versão analisada:** `v6` (idêntico à `v5`)

---

## 1. Bugs e erros conhecidos

### B1 — Exclusão em lote apaga linhas da tabela errada (**severidade alta**)

`v6/resources/views/prefecture/contents/newsletter/index.blade.php` monta o botão de
exclusão em lote com a rota de **conteúdos**:

```blade
@component('prefecture.components.deleteByLoteSubmitButton', ['route' => 'ajax.generic.content.deleteAll'])
```

Essa rota é `v6/routes/basepro.php:141`:

```php
Route::post('conteudo/deleteAll', 'Prefecture\ContentController@genericDeleteAll')->name('ajax.generic.content.deleteAll');
```

e `Prefecture\ContentController@genericDeleteAll`
(`v6/app/Http/Controllers/Prefecture/ContentController.php:70-78`) executa:

```php
return $this->contentRepository->destroyAll($request->toDelete);
```

`ContentRepositoryDefaultImpl` opera sobre `App\Prefecture\Content`, cujo model declara
`protected $table = 'contents'` e usa `SoftDeletes`
(`v6/app/Http/Model/Prefecture/Content.php:13-18`). Portanto, ao clicar em
"Excluir Registro(s)" na tela de newsletters, o sistema **soft-deleta linhas da tabela
`contents`** cujos `id`s coincidem com os ids das assinaturas selecionadas — ou seja,
apaga conteúdo institucional (notícias, páginas, secretarias) de forma silenciosa.

**Correção sugerida (v7):** criar `POST /cms/newsletter/bulk-delete` tratado por um
controller próprio do módulo, com policy `newsletter.delete`.

### B2 — Exportação CSV ignora o soft delete (**severidade média**)

`NewsLetterController@export` consulta `DB::table('newsletters')->get()`
(`v6/app/Http/Controllers/Prefecture/NewsLetterController.php:96`), enquanto a listagem
do CMS usa a view `newsletter`, que já filtra `deleted_at IS NULL`. O arquivo entregue
ao operador contém assinaturas que já foram excluídas — inclusive dados pessoais de
quem pediu para ser removido.

### B3 — `storePortal` sem valor de `active` provoca HTTP 500 (**severidade média**)

`storePortal` grava `'active' => $request->active` sem validação nem default. A coluna
`active` é `boolean NOT NULL` sem default no banco. Um POST direto para
`/newsletter/store-portal` sem o campo `active` gera `QueryException` e resposta 500
(página de erro). Os formulários do portal sempre enviam o campo, mas qualquer outro
cliente (ou um template de e-mail marketing futuro) quebra.

**Correção sugerida (v7):** `'active' => $request->boolean('active')` ou
`default(false)` na coluna.

### B4 — `storePortal` grava `name` sem validar (**severidade baixa**)

`name` é anulável no banco, então o INSERT funciona; o registro fica com nome vazio e a
DataTable exibe a célula em branco. A rota do CMS, em contraste, exige `name`.

### B5 — A rota pública herda `check.route.permission` (**severidade alta**)

`v6/routes/web.php:229` não declara nenhum middleware, mas a ação está em
`Prefecture\NewsLetterController`, que **não** define construtor e portanto herda o
construtor de `App\Http\Controllers\Prefecture\Controller`
(`v6/app/Http/Controllers/Prefecture/Controller.php:24-27`), onde está registrado
`$this->middleware('check.route.permission')`.

O Laravel 5.5 agrega o middleware do controller à rota
(`v6/vendor/laravel/framework/src/Illuminate/Routing/Route.php:748-760` chama
`controllerMiddleware()` no `gatherMiddleware()`). Com isso, `CheckRoutePermission`
(`v6/app/Http/Middleware/CheckRoutePermission.php:31-34`) executa para o POST público e,
para um visitante **não autenticado**, devolve `redirect()->route('login')`.

Efeito: o formulário público de assinatura **exige login no CMS** para funcionar. Se o
comportamento desejado é assinatura anônima, o bug é grave; se o formulário público
nunca funcionou, o módulo está inoperante no portal.

> **Ponto a confirmar com o negócio:** a assinatura anônima é requisito? Se sim, a rota
> pública precisa ficar fora do namespace `Prefecture` (ou o controller precisa
> sobrescrever o construtor e remover o middleware).

### B6 — `newsletter.store` e `newsletter.update` não exigem permissão (**severidade média**)

Em `CheckRoutePermission::hasAccess()` o array `$resourceRoutes`
(`['edit' => 'update', 'create' => 'store', 'delete' => 'destroy']`) é usado como lista
de **valores** no teste `in_array($value, $resourceRoutes)`. Como o nome da rota é
dividido por `.`, qualquer rota cujo segundo segmento seja `store`, `update` ou
`destroy` é liberada para **qualquer usuário autenticado**.

Para este módulo: `newsletter.store` (segmento `store`) e `newsletter.update`
(segmento `update`) ficam abertas; já `newsletter.create`, `newsletter.edit`,
`newsletter.delete` e `newsletter.export` exigem a permissão correspondente.
Esse comportamento é do middleware compartilhado (não exclusivo do módulo) e precisa ser
reavaliado na v7 com policies.

### B7 — `show()` aponta para uma view inexistente (**severidade baixa**)

`v6/app/Http/Controllers/Prefecture/NewsLetterController.php:142`:
`return view('newsletters.show', compact('newsletter'));`. Não existe
`resources/views/newsletters/show.blade.php` e não há rota para a ação. Código morto que
vai quebrar se alguém registrar a rota.

### B8 — `destroy()` responde 500 para id inexistente (**severidade baixa**)

`findOrFail` lança `ModelNotFoundException`, que é capturada pelo
`catch (\Exception $e)` e convertida em `{erro: 'Erro ao deletar inscrição!'}` com HTTP
500. O esperado seria 404. O mesmo vale para `edit()` e `update()`, que não têm
tratamento de exceção (nesse caso o erro é 404 nativo do `findOrFail`).

### B9 — `index()` carrega a base inteira sem utilizar (**severidade baixa**)

`Newsletters::all()` é executado em toda visita a `/cms/newsletter` e o resultado é
passado para a view, que não o utiliza (a listagem vem do AJAX
`/cms/datatable/newsletter`). É uma leitura completa da tabela a cada visita.

### B10 — CSV injection no arquivo exportado (**severidade média**)

`fputcsv` grava os valores sem neutralizar fórmulas. Um `name` ou `email` começando com
`=`, `+`, `-` ou `@` é executado como fórmula pelo Excel/LibreOffice ao abrir o arquivo,
o que pode disparar requisições externas (exfiltração de dados) no computador do
operador. O `name` vem de um formulário público sem validação de conteúdo.

**Correção sugerida (v7):** prefixar valores que comecem com esses caracteres por `'`
ou usar `league/csv` com `escape`/formatter adequado.

## 2. Funcionalidades incompletas ou mortas

| Item | Situação |
|---|---|
| Envio de e-mails | **Não existe** no repositório. A tabela, os campos `active` e `sent_at` e o nome "Newsletter" sugerem um disparo de correspondência que nunca foi implementado. O `sent_at` é apenas a data da assinatura. |
| `NewsLetterController@show` | Ação morta (view inexistente, sem rota). |
| Coluna `ativo` da view `newsletter` | Calculada e exposta no JSON, mas não exibida em nenhuma coluna da DataTable do CMS. |
| Formulário do portal no layout `default` | O layout `default` não tem newsletter: em municípios que o utilizem, não há ponto de entrada público. |
| Undo/restauração | Não há restauração de assinaturas excluídas (o model tem `deleted_at`, mas nenhuma rota de `restore`). |

## 3. Itens da v5 e da v6

| Item | v5 | v6 |
|---|---|---|
| Controller, model, views, migrations, rotas | presentes | presentes, **idênticos** |
| Exportação CSV | presente (mesmo código) | presente |
| View SQL `newsletter` | presente (mesmo SQL) | presente |
| Registro na tabela `modulos` | **inexistente** (a tabela `modulos` não existe na v5) | `modulos.id = 53`, slug `newsletters` |
| Painel de módulos (`/cms/modulos`) | inexistente | presente (`Admin\ModulosController`) |
| Seed da configuração | sobrescrevia `value = 0` no `update()` | não sobrescreve mais o `value` |
| Segunda trava de exibição (modulo + configuração) | apenas a configuração | configuração **e** `modulos.status` |

Nada foi **removido** da v5 para a v6 neste módulo.

## 4. Riscos de dados (migração de schema)

| Risco | Detalhe |
|---|---|
| `UNIQUE` em `email` com soft delete | assinaturas excluídas bloqueiam o re-cadastro. Ao migrar, considerar um índice parcial `UNIQUE (email) WHERE deleted_at IS NULL` + migração de duplicados, **ou** manter como está (comportamento atual) |
| `active` `NOT NULL` sem default | inserções vindas de fontes não validadas quebram; adotar `default(false)` na migração |
| `sent_at` `NOT NULL` | se a v7 passar a permitir cadastro manual sem data, é preciso default `now()` |
| `name` anulável | coerente com o banco, mas inconsistente com a validação do CMS; padronizar |
| Tabela `newsletters` × view `newsletter` | nomes quase idênticos em caixa baixa; risco alto de confusão em migração de scripts e consultas manuais |
| Slug `newsletters` × `newsletter` | o registro em `modulos` e a linha em `menu_orders` usam slugs diferentes; conferir o `modulo_id` em qualquer carga de dados |

## 5. Riscos de segurança e de privacidade

| Risco | Detalhe |
|---|---|
| IP do assinante exposto | `ip_address` aparece na listagem do CMS e no CSV exportado, sem restrição por permissão |
| Exportação sem trilha de auditoria | a rota `newsletter.export` não registra log de atividade (`activity()`), diferentemente de outros fluxos do CMS |
| Endpoint público sem rate limit | permite o cadastro em massa de assinaturas na base |
| Endpoint público com middleware de CMS | ver B5 — o comportamento real diverge do desenho |
| Ausência de consentimento registrado | a tabela não guarda data/hora de consentimento separada nem origem (`sent_at` e `ip_address` são proxies) |

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

1. A assinatura da newsletter deve ser **anônima**? Se sim, B5 é um bug bloqueante.
2. Deve existir envio periódico de e-mail aos assinantes? Se sim, o módulo está incompleto
   e falta definir template, periodicidade, opt-out e base legal (LGPD).
3. O operador deve poder **reativar** assinaturas (`active = true`) em massa? Hoje só
   linha a linha.
4. A exclusão em lote deveria existir? Se não, o botão deve ser removido (B1).
5. O e-mail de um assinante removido pode ser reinscrito? (restrição `UNIQUE` global)
6. O IP deve continuar sendo coletado e exibido? (LGPD: dado pessoal desnecessário para
   a finalidade de newsletter)
7. O layout `default` deve ganhar o formulário de newsletter?

## 7. Mapa v5 → v6

| Item v5 | Item v6 | Tipo de mudança |
|---|---|---|
| `v5/app/Http/Controllers/Prefecture/NewsLetterController.php` | `v6/app/Http/Controllers/Prefecture/NewsLetterController.php` | **idêntico** |
| `v5/app/Http/Model/Prefecture/Newsletters.php` | `v6/app/Http/Model/Prefecture/Newsletters.php` | **idêntico** |
| `v5/database/migrations/2024_10_28_161745_create_newsletters_table.php` | `v6/database/migrations/2024_10_28_161745_create_newsletters_table.php` | **idêntico** |
| `v5/database/migrations/2024_10_29_154058_create_view_newsletter.php` | `v6/database/migrations/2024_10_29_154058_create_view_newsletter.php` | **idêntico** |
| `v5/database/migrations/2024_10_30_161709_inserting_menu_orders_newsletter.php` | `v6/database/migrations/2024_10_30_161709_inserting_menu_orders_newsletter.php` | **idêntico** |
| `v5/resources/views/prefecture/contents/newsletter/index.blade.php` | `v6/resources/views/prefecture/contents/newsletter/index.blade.php` | **idêntico** |
| `v5/resources/views/prefecture/contents/newsletter/create.blade.php` | `v6/resources/views/prefecture/contents/newsletter/create.blade.php` | **idêntico** |
| `v5/resources/views/prefecture/contents/newsletter/edit.blade.php` | `v6/resources/views/prefecture/contents/newsletter/edit.blade.php` | **idêntico** |
| `v5/resources/views/prefecture/contents/newsletter/form.blade.php` | `v6/resources/views/prefecture/contents/newsletter/form.blade.php` | apenas 2 linhas com ajuste de espaço |
| `v5/routes/basepro.php:72-78` | `v6/routes/basepro.php:81-87` | mesmas 7 rotas CMS |
| `v5/routes/web.php` (rota `newsletter.storeportal`) | `v6/routes/web.php:229` | mesma rota pública |
| `v5/database/seeds/UpdateConfigurationOrderSeederNewsletter.php` | `v6/database/seeds/UpdateConfigurationOrderSeederNewsletter.php` | **mudança real**: `'key'` e `'value' => 0` removidos do `update()` |
| — | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | **novo**: registro `modulos.id = 53` |
| — | `v6/app/Providers/ModulosServiceProvider.php` | **novo**: cache e injeção de `$modulos` nas views do portal |
| — | `v6/app/Http/Controllers/Admin/ModulosController.php` | **novo**: painel de habilitação de módulos |
| `v5/app/Http/Controllers/Prefecture/DatatableController.php` (`get_newsletter`) | `v6/app/Http/Controllers/Prefecture/DatatableController.php:63-67` | idêntico |
