# Mailing List Management

## Package

`jsefton/laravel-mailing-list` v1.0.0 — a thin Laravel package that provides two Eloquent models
(`MailingList`, `MailingListEmail`), an optional JSON API, and basic Artisan commands.
The package ships **no events and no confirmation email logic** — all confirmation behaviour is
implemented in this application.

---

## Database tables

| Table | Key columns |
|---|---|
| `mailing_lists` | `id`, `name`, `data` (JSON), `timestamps` |
| `mailing_list_emails` | `id`, `mailing_list_id`, `email`, `first_name`, `last_name`, `source`, `subscribed` (tinyint, default 1), `data` (JSON), `timestamps` |

`subscribed = 0` means **pending confirmation**. `subscribed = 1` means confirmed.

---

## Known lists

These IDs are hard-coded or env-controlled in the application.
Verify against the database if the list IDs have been recreated.

| ID | Purpose | How populated |
|---|---|---|
| 2 | Support / updates | Guest support ticket submission (`SupportTicketController`) |
| 3 | Affiliates | Affiliate account activation (`AffiliateActivatedListener`) |
| `MAILING_LIST_DOCS_ID` (env) | Docs AI chat | Identity collection during a docs chat session (`SupportChatController`) |

> **Adding a new list:** create it via `php artisan mailing-list:create` or directly in the DB,
> note the auto-incremented ID, and use `MailingListEmail::updateOrCreate()` from your code.
> The confirmation email fires automatically (see below).

---

## Subscription confirmation flow

### Overview

When a subscriber is **newly created** with `subscribed = false`, a double-opt-in confirmation
email is sent automatically. The subscriber clicks the link, the row is flipped to
`subscribed = true`, and a success page is shown.

```
updateOrCreate() / create()          MailingListEmailObserver::created()
─────────────────────────────  →  sends ConfirmSubscriptionMail (if subscribed = false)
                                       │
                                       └─ signed URL valid for 24 hours
                                              │
                                   GET /mailing/confirm/{id}   (public, signed middleware)
                                              │
                                   entry.subscribed = true
                                              │
                                   view: emails.mailing_confirm_success
```

### Observer — the single dispatch point

**`App\Observers\MailingListEmailObserver`** — registered in `AppServiceProvider::boot()`.

```php
MailingListEmail::observe(MailingListEmailObserver::class);
```

Listens to the Eloquent `created` event only (not `updated`). This means:
- A **new** pending subscriber → confirmation email sent immediately.
- `updateOrCreate()` hitting an **existing** pending entry → `updated` fires, no duplicate email.
- A row created already `subscribed = true` (future use) → observer returns early, no email sent.
- Failures are caught, logged, and never propagated to the caller.

**No call-site needs to know about confirmation emails.** Any code that writes a new
`MailingListEmail` row with `subscribed = false` gets the confirmation automatically.

### Mailable — `App\Mail\ConfirmSubscriptionMail`

Accepts a `MailingListEmail` instance. Generates a **24-hour temporally signed URL** via
`URL::temporarySignedRoute('mailing.confirm', now()->addDay(), ['id' => $entry->id])`.

Renders two email templates:
- `resources/views/emails/mailing_confirm.blade.php` — HTML
- `resources/views/emails/mailing_confirm_plain.blade.php` — plain text fallback

### Confirmation route

```
GET /mailing/confirm/{id}   (public, `signed` middleware, no auth required)
```

Handled by `MailingListAdminController::confirm()`:
- Valid signature → `entry.subscribed = true`, renders `emails.mailing_confirm_success`
- Expired / invalid signature → renders `emails.mailing_confirm_expired`
- Other error → flash error, redirect `/`

### Email templates

| View | Purpose |
|---|---|
| `resources/views/emails/mailing_confirm.blade.php` | HTML confirmation email |
| `resources/views/emails/mailing_confirm_plain.blade.php` | Plain text fallback |
| `resources/views/emails/mailing_confirm_success.blade.php` | Web page shown after confirming |
| `resources/views/emails/mailing_confirm_expired.blade.php` | Web page shown for expired/invalid links |

---

## Subscription entry points

| Entry point | File | List | Source label | Observer fires? |
|---|---|---|---|---|
| Affiliate activation | `app/Listeners/Affiliate/AffiliateActivatedListener.php` | 3 | `'Affiliate Programme'` | ✅ |
| Guest support ticket | `app/Http/Controllers/SupportTicketController.php` | 2 | `'Guest Support Ticket'` | ✅ |
| Docs AI chat identity collection | `app/Http/Controllers/SupportChatController.php` | `MAILING_LIST_DOCS_ID` | `'Docs AI Chat'` | ✅ |

All three write `subscribed = false` via `updateOrCreate()`. The observer fires on `created` only,
so none of them need any email-sending logic at the call site.

---

## Admin interface

### Routes (inside `appshell` admin prefix + `auth` middleware)

| Method | URI | Route name | Action |
|---|---|---|---|
| `GET` | `admin/mailing-list` | `appshell.mailing_list.index` | List all mailing lists with subscriber counts |
| `GET` | `admin/mailing-list/{list}` | `appshell.mailing_list.show` | View subscribers for a list (searchable, paginated) |
| `DELETE` | `admin/mailing-list/{list}/subscribers/{entry}` | `appshell.mailing_list.destroy` | Hard-delete a subscriber |
| `POST` | `admin/mailing-list/{list}/subscribers/{entry}/resend` | `appshell.mailing_list.resend` | Resend confirmation email to a pending subscriber |

### Resend confirmation

`MailingListAdminController::resendConfirmation()` — guards against resending to already-confirmed
subscribers (redirects back with a warning flash). On success or failure, redirects back with a
flash message.

The **Resend** button is shown next to the **Remove** button in the subscriber table, but only for
rows with `subscribed = false` (status badge "Pending").

### Views

| View | Path |
|---|---|
| List index | `resources/views/vendor/vanilo/mailing-list/index.blade.php` |
| Subscriber list | `resources/views/vendor/vanilo/mailing-list/show.blade.php` |

---

## Adding a new subscription entry point

1. Call `MailingListEmail::updateOrCreate()` with `subscribed = false`.
2. That's it — the observer sends the confirmation email automatically.

```php
$entry = MailingListEmail::updateOrCreate(
    ['mailing_list_id' => $listId, 'email' => $email],
    MailingListEmail::map(['name' => $name, 'email' => $email]) + [
        'mailing_list_id' => $listId,
        'source'          => 'My New Source',
        'subscribed'      => false,
    ]
);
```

> `updateOrCreate()` hitting an existing row fires `updated`, not `created`, so no duplicate
> confirmation is sent to someone who submits the same form twice.

---

## Package config

`config/mailing-list.php` (published from the package):

| Key | Default | Description |
|---|---|---|
| `api` | `true` (env `MAILING_LIST_API`) | Enables the package's built-in JSON API at `route` |
| `route` | `/api/mailing-list` | API route prefix |

The API is separate from the admin UI and the confirmation flow.
