# Newsletters — Arquitetura

> **Módulo:** `newsletters` (tabela `modulos`, id 53)
> **Escopo:** captura pública de assinaturas (portal) + gestão da lista (CMS)

---

## 1. Fluxo da requisição

### 1.1 Assinatura pelo portal (público)

```
POST /newsletter/store-portal        (v6/routes/web.php:229, nome newsletter.storeportal)
  └─ grupo 'web' (RouteServiceProvider::mapWebRoutes) → middleware web + csrf
     └─ Preface\NewsLetterController@storePortal   (v6/app/Http/Controllers/Prefecture/NewsLetterController.php:45)
        ├─ $request->validate(['email' => 'required|email|unique:newsletters,email'])
        ├─ Newsletters::create([
        │      'name'       => $request->name,      ← sem validação
        │      'email'      => $request->email,
        │      'ip_address' => $request->ip(),
        │      'sent_at'    => Carbon::now(),
        │      'active'     => $request->active,    ← sem validação, coluna NOT NULL
        │  ])
        └─ redirect()->back()->with('success', 'Inscrição realizada com sucesso!')
```

Observações do fluxo:

- A rota vive em `v6/routes/web.php`, dentro do grupo `web` (sessão + CSRF). Não há
  middleware `check.logged`: o endpoint é público por definição.
- O controller herda de `App\Http\Controllers\Prefecture\Controller`, cujo construtor
  (`v6/app/Http/Controllers/Prefecture/Controller.php:24-27`) aplica
  `$this->middleware('check.route.permission')`. Como `NewsLetterController` **não**
  declara construtor, o middleware também é aplicado à rota pública — o que na prática
  significa que **um visitante não logado é redirecionado para `login`** ao tentar
  assinar, pois `CheckRoutePermission` cai no bloco `if (!Sentinel::check())` e faz
  `redirect()->route('login')` (`v6/app/Http/Middleware/CheckRoutePermission.php:31-34`).
  Este é um ponto de atenção de alto impacto funcional (ver `08-GAPS-E-DIVIDAS.md`).

### 1.2 Listagem no CMS

```
GET /cms/newsletter                    (v6/routes/basepro.php:81, nome newsletter.index)
  └─ grupo ['prefix' => 'cms', 'before' => 'csrf'] + middleware web
     └─ Preface\NewsLetterController@index
        ├─ Newsletters::all()                       ← consulta carregada e NÃO usada pela view
        └─ view('prefecture.contents.newsletter.index')
           └─ <table id="tabela_newsletter">       (só o esqueleto da tabela)
              └─ JS assets/js/custom/contents/newsletter/datatables.js
                 └─ runDatatables('newsletter', colunas, true, 'ASC', null, true)
                    └─ AJAX GET /cms/datatable/newsletter
                       └─ Preface\DatatableController@get_newsletter
                          ├─ DB::table('newsletter')->get()   ← VIEW SQL, não a tabela
                          ├─ struct_views($data, 'newsletter') → has_access('newsletter.edit' / 'newsletter.delete')
                          └─ Yajra DataTables::of($newsletter)->make(true)  → JSON
```

### 1.3 Exclusão (CMS)

```
DELETE /cms/newsletter/delete/{id}     (v6/routes/basepro.php:83, nome newsletter.delete)
  └─ Preface\NewsLetterController@destroy
     ├─ Newsletters::findOrFail($id)   ← ModelNotFoundException se o id não existir
     ├─ $newsletter->delete()          ← soft delete
     └─ JSON {success: 'Inscrição deletada com sucesso!'}  |  500 {erro: 'Erro ao deletar inscrição!'}
```

Chamada pelo JavaScript: `addDelete()` em `v6/public/assets/js/custom/utilities/table.js:466-489`
lê o atributo `target` do botão (definido por `createBTN`, `table.js:227-260`) e executa
`myFetch(url, 'DELETE')` (`v6/public/assets/js/custom/utilities/functions.js:64`).

### 1.4 Exportação (CMS)

```
GET /cms/newsletter/export             (v6/routes/basepro.php:82, nome newsletter.export)
  └─ Preface\NewsLetterController@export
     ├─ DB::table('newsletters')->get()   ← tabela crua: ignora o soft delete
     ├─ vazio → redirect()->back()->with('error', 'Nenhuma inscrição encontrada para exportar.')
     └─ response()->stream($callback)  → BOM UTF-8 + cabeçalho + uma linha por registro
```

## 2. Classes envolvidas

| Classe / arquivo | Papel |
|---|---|
| `v6/app/Http/Model/Prefecture/Newsletters.php` | Model Eloquent da assinatura. `SoftDeletes`, `$fillable = ['name','email','ip_address','sent_at','active']`, `$timestamps = true`. Sem `protected $table` — o Laravel deriva `newsletters` do nome plural da classe. |
| `v6/app/Http/Controllers/Prefecture/NewsLetterController.php` | Controller único do módulo: `index`, `create`, `store`, `storePortal`, `edit`, `update`, `export`, `show`, `destroy`. Validação inline, sem Form Request. |
| `v6/app/Http/Controllers/Prefecture/Controller.php` | Controller base do CMS; construtor registra o middleware `check.route.permission`. |
| `v6/app/Http/Controllers/Prefecture/DatatableController.php` | Serve a listagem paginada em JSON: `get_newsletter()` (linha 63) lê `DB::table('newsletter')` e `struct_views()` (linha 504) injeta as flags de permissão. |
| `v6/app/Http/Middleware/CheckRoutePermission.php` | Autorização por nome de rota × permissões do Sentinel; `super-admin` passa direto. |
| `v6/app/Library/Helper/appHelpers.php` | `has_access()` — controla botões de criar/excluir/edit nas views do CMS. |
| `v6/app/Helpers/functions.php` | `getStatusDisplayNewsletters()` (linha 583) lê `configurations.key = 'newsletters'`; `menuClicks()` (linha 865) incrementa o contador de cliques do menu. |
| `v6/app/Providers/ModulosServiceProvider.php` | Publica a coleção `$modulos` (slug → status) em cache permanente e a injeta nas views do portal via `view()->composer`. |
| `v6/app/Providers/MenuServiceProvider.php` | Monta o menu lateral do CMS; o `case 'newsletter'` (linha ~236) cria o item "Newsletter" apontando para `/cms/newsletter`. |
| `v6/app/Http/Model/Prefecture/MenuOrder.php` | Model de `menu_orders`; `belongsTo(Modulos::class, 'modulo_id')`. |
| `v6/app/Http/Model/Admin/Modulos.php` | Model de `modulos` (`slug`, `status`), ligado ao menu pelo `modulo_id`. |
| `v6/app/Http/Model/Prefecture/Configuration.php` | Model de `configurations`, usado por `getStatusDisplayNewsletters()`. |

**Não existem** para este módulo: repository, service, Form Request, API Resource,
policy, factory ou seed de dados de exemplo.

## 3. Diagrama de fluxo

```mermaid
graph TD
    A[Cidadão no portal<br/>layout modelo1..modelo6] -->|POST /newsletter/store-portal| B[NewsLetterController@storePortal]
    B --> C{validate email}
    C -->|falha| A
    C -->|ok| D[Newsletters::create]
    D --> E[(tabela newsletters)]
    A -->|GET /cms/newsletter| F[NewsLetterController@index]
    F --> G[view prefecture.contents.newsletter.index]
    G --> H[JS datatables.js]
    H -->|GET /cms/datatable/newsletter| I[DatatableController@get_newsletter]
    I --> J[(view SQL newsletter)]
    I --> K[struct_views: has_access]
    K --> H
    F -->|POST /cms/newsletter/store| L[NewsLetterController@store]
    L --> E
    F -->|PUT /cms/newsletter/update/id| M[NewsLetterController@update]
    M --> E
    F -->|DELETE /cms/newsletter/delete/id| N[NewsLetterController@destroy]
    N --> E
    F -->|GET /cms/newsletter/export| O[NewsLetterController@export]
    O --> P[(tabela newsletters crua)]
    O --> Q[CSV stream + BOM UTF-8]
```

## 4. Dependências externas

| Pacote | Uso no módulo |
|---|---|
| `nesbot/carbon` | `Carbon::now()` para `sent_at` e `Carbon::parse()` na formatação da data no CSV. |
| `yajra/laravel-datatables-oracle` | `DataTables::of($newsletter)->make(true)` em `DatatableController@get_newsletter`. |
| `laravelcollective/html` | `Form::open`, `Form::model`, `Form::email`, `Form::text` nas views do CMS. |
| `cartalyst/sentinel` | Sessão, papéis (`super-admin`) e permissões (`has_access`, `CheckRoutePermission`). |
| `symfony/http-foundation` | `StreamedResponse` (importado em `NewsLetterController`, embora não seja usado como type hint). |
| `jeroennoten/laravel-adminlte` | Menu lateral do CMS (evento `BuildingMenu` consumido em `MenuServiceProvider`). |

## 5. Onde o módulo se encaixa na plataforma

- **Menu do CMS:** `MenuServiceProvider` itera `menu_orders` e, para `slug = 'newsletter'`,
  adiciona "Newsletter" (`/cms/newsletter`, ícone `book`). O item só aparece se
  `has_access('newsletter.index')` **e** o módulo estiver habilitado (`menu.modulo.status`).
- **Portal:** `ModulosServiceProvider` compartilha `$modulos` (coleção slug → status) com
  as views `portal.layouts.modelo{1..6}.home.index` e `portal.layouts.modelo{1..6}.layout.sidebar.layout`,
  entre outras. O status vem da tabela `modulos` e é cacheado com `Cache::rememberForever('modulos')`,
  invalidado por `Cache::pull('modulos')` em `Admin\ModulosController@update`.
- **Segundo interruptor:** `configurations.key = 'newsletters'` (semeado por
  `v6/database/seeds/UpdateConfigurationOrderSeederNewsletter.php`) é lido por
  `getStatusDisplayNewsletters()` e decide se o bloco é renderizado (`== 0`) ou omitido (`== 1`).
