# Newsletters — Modelagem de Dados

> **Módulo:** `newsletters` (tabela `modulos`, id 53)
> **Banco:** PostgreSQL 10 (`prefeituras_v5`)

---

## 1. Tabela `newsletters`

Criada por `v6/database/migrations/2024_10_28_161745_create_newsletters_table.php`
(classe `CreateNewslettersTable`). O arquivo é **idêntico** ao da v5.

```php
Schema::create('newsletters', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->nullable();
    $table->string('email')->unique();
    $table->string('ip_address');
    $table->timestamp('sent_at');
    $table->boolean('active');
    $table->timestamps();
    $table->softDeletes();
});
```

### Colunas (estado real no banco)

| Coluna | Tipo | Nulo | Default | Observação |
|---|---|---|---|---|
| `id` | `integer` (serial) | não | `newsletters_id_seq` | PK |
| `name` | `character varying(191)` | **sim** | — | Nome do assinante; opcional no banco |
| `email` | `character varying(191)` | não | — | `UNIQUE` (`newsletters_email_unique`) |
| `ip_address` | `character varying(191)` | não | — | IP do cliente no momento da assinatura |
| `sent_at` | `timestamp(0) without time zone` | não | — | Data/hora da assinatura (não é data de envio) |
| `active` | `boolean` | não | — | "Ativar E-mail" |
| `created_at` | `timestamp(0) without time zone` | sim | — | `timestamps = true` no model |
| `updated_at` | `timestamp(0) without time zone` | sim | — | idem |
| `deleted_at` | `timestamp(0) without time zone` | sim | — | soft delete |

Índices: `newsletters_pkey` (PK) e `newsletters_email_unique` (UNIQUE em `email`).

Observações:

- `name` é anulável no banco, mas o CMS exige `name` (`required`); a rota pública não exige.
- `active` é `NOT NULL` e **não tem default**: uma inserção sem o campo quebra o INSERT.
- A unicidade do e-mail é garantida **no banco**, portanto assinaturas em soft delete
  continuam bloqueando o re-cadastro do mesmo endereço (a validação `unique` do Laravel
  também consulta a tabela crua).
- O nome da coluna `sent_at` é enganoso: nada é enviado por e-mail no sistema; o valor
  é o instante da assinatura.

## 2. View SQL `newsletter`

Criada por `v6/database/migrations/2024_10_29_154058_create_view_newsletter.php`
(classe `CreateViewNewsletter`), também idêntica à v5.

```sql
CREATE VIEW public.newsletter AS
SELECT row_number() OVER (ORDER BY nl.created_at DESC) AS value,
    nl.name AS nome,
    nl.email AS email,
    nl.ip_address AS ip,
    nl.sent_at AS enviado,
    nl.active AS ativo,
    nl.id
FROM newsletters nl
WHERE nl.deleted_at IS NULL
ORDER BY nl.created_at DESC;
```

| Coluna da view | Origem | Uso |
|---|---|---|
| `value` | `row_number()` sobre `created_at DESC` | numeração sequencial exibida na 1ª coluna da DataTable |
| `nome` | `nl.name` | coluna "Nome" |
| `email` | `nl.email` | coluna "E-mail" |
| `ip` | `nl.ip_address` | coluna "IP" |
| `enviado` | `nl.sent_at` | coluna "Data de inscrição" |
| `ativo` | `nl.active` | não exibido na tabela (só no CSV de exportação) |
| `id` | `nl.id` | chave das ações (editar/excluir) |

Pontos importantes:

- É esta view — e não a tabela — que alimenta a listagem do CMS
  (`DatatableController@get_newsletter` usa `DB::table('newsletter')`).
- A view já aplica `WHERE deleted_at IS NULL`; a exportação CSV **não** a usa.
- O `ORDER BY` final da definição da view é irrelevante para o `row_number()`; quem
  define a ordenação da listagem é o DataTables (ASC por padrão, ver
  `v6/public/assets/js/custom/contents/newsletter/datatables.js:57`).

## 3. Tabelas de apoio

### `menu_orders` (linha do módulo)

| Coluna | Valor |
|---|---|
| `id` | 2 |
| `slug` | `newsletter` (**singular**) |
| `clicks` | contador de acessos ao item de menu |
| `modulo_id` | 53 → `modulos.id` |

Criada/alimentada por `v6/database/migrations/2024_10_30_161709_inserting_menu_orders_newsletter.php`
(classe `InsertingMenuOrdersNewsletter`), idêntica à v5:

```php
DB::table('menu_orders')->insert([
    ['slug' => 'newsletter', 'clicks' => 0]
]);
```

O `down()` remove a linha por `slug = 'newsletter'`.

### `modulos` (registro do módulo)

Criada por `v6/database/migrations/2024_11_01_150206_create_modulos_table.php`:

| id | name | slug | status inicial |
|---|---|---|---|
| 53 | Newsletters | `newsletters` | 0 (desligado) |

Esta migration **não existe na v5** — o registro de módulos é novidade da v6.
O `status` é alternado em `/cms/modulos` (`Admin\ModulosController@update`) e o cache
`Cache::rememberForever('modulos')` é invalidado com `Cache::pull('modulos')`.

### `configurations` (chave `newsletters`)

Semeada por `v6/database/seeds/UpdateConfigurationOrderSeederNewsletter.php`:

| Campo | Valor |
|---|---|
| `key` | `newsletters` |
| `title` | `Desabilitar Newsletter?` |
| `description` | `Com esta opção marcada, às Newsletters não irão ser exibidas no portal.` |
| `small_description` | `Com esta opção marcada, às Newsletters não irão ser exibidas no portal` |
| `type` | `checkbox` |
| `value` | `0` (exibe) / `1` (oculta) |
| `order` | 27 |
| `category_configurations_id` | 1 |
| `notnull` | `false` |
| `status_create` | 1 |

Diff v5 → v6 do seed: a v5 repetia `'key' => 'newsletters'` e `'value' => 0` dentro do
`update()`; a v6 removeu essas duas linhas (o `key` já é o filtro do `where` e o `value`
não deve ser sobrescrito). Efeito prático na v6: a opção **não** é mais resetada para `0`
a cada execução do seed.

## 4. Relacionamentos

| Origem | Destino | Tipo | Observação |
|---|---|---|---|
| `newsletters.email` | — | `UNIQUE` | unicidade global, inclusive em linhas soft deleted |
| `menu_orders.modulo_id` | `modulos.id` | FK lógica (`belongsTo`) | `MenuOrder::modulo()` em `v6/app/Http/Model/Prefecture/MenuOrder.php` |
| `modulos.slug` | — | chave de aplicação | usada por `ModulosServiceProvider` para montar `$modulos` |

O módulo **não tem** FK de saída, nem tabela pivot, nem relacionamento Eloquent
definido no model `Newsletters` (nenhuma relação declarada na classe).

## 5. Migrations do módulo (v6)

| Data | Arquivo | Efeito |
|---|---|---|
| 2024-10-28 | `v6/database/migrations/2024_10_28_161745_create_newsletters_table.php` | cria a tabela `newsletters` |
| 2024-10-29 | `v6/database/migrations/2024_10_29_154058_create_view_newsletter.php` | cria a view `newsletter` |
| 2024-10-30 | `v6/database/migrations/2024_10_30_161709_inserting_menu_orders_newsletter.php` | insere `menu_orders.slug = 'newsletter'` |
| 2024-11-01 | `v6/database/migrations/2024_11_01_150206_create_modulos_table.php` | cria `modulos` e insere o registro id 53 (`newsletters`) |

## 6. Seeds do módulo (v6)

| Arquivo | Efeito |
|---|---|
| `v6/database/seeds/UpdateConfigurationOrderSeederNewsletter.php` | cria/atualiza a configuração `newsletters` (chave de exibição no portal) |

Não há seed de assinaturas de exemplo: a tabela nasce vazia e é populada pelo portal.

## 7. Model Eloquent

`v6/app/Http/Model/Prefecture/Newsletters.php`:

```php
namespace App\Http\Model\Prefecture;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Newsletters extends Model
{
    use SoftDeletes;

    protected $fillable = [
        'name', 'email', 'ip_address', 'sent_at', 'active'
    ];

    public $timestamps = true;
}
```

Pontos de atenção:

- **Classe no plural** (`Newsletters`). O nome do arquivo é `Newsletters.php` e a classe é
  `Newsletters`. **Não existe** o arquivo `NewsLetter.php` (singular) em `v6/app/Http/Model/Prefecture/` —
  nem classe `NewsLetter` em `v6/app/Http/Model/Prefecture/` (verificado por busca no
  diretório e por `grep "class NewsLetter"`).
- Sem `protected $table`: o Laravel deriva `newsletters` de `Str::plural(Str::snake('Newsletters'))`.
  Funciona, mas é implícito — na v7 declare a tabela explicitamente.
- `public $timestamps = true;` é redundante (o padrão do Eloquent já é `true`).
- Sem casts: `sent_at` e `active` chegam como `string`/`int` cru do Postgres. Na v6 o
  `active` gravado é `'1'`/`'0'` (do `input()`) e o Postgres converte para `boolean`;
  o model não expõe `active` como `bool` no PHP.
- Sem `$hidden`: os campos `ip_address` e `email` são serializados normalmente caso o
  model seja devolvido em alguma resposta JSON.
