# NPD Notifications Flow

Central notification layer for borrower and affiliate apps. Events are dispatched through `NotificationDispatcher`, which routes each event to the configured channels.

## Architecture

```
Domain trigger → NotificationDispatcher → NotificationMessageBuilder
                                       → NotificationService (email / SMS / in-app / FCM)
```

- **Catalog:** `App\Notifications\NotificationEvent` (string-backed enum)
- **Dispatcher:** `App\Services\Notification\NotificationDispatcher`
- **Low-level channels:** `App\Services\Notification\NotificationService`
- **In-app inbox:** `notifications` table (`GET /api/v1/profile/notifications`, `GET /api/v1/affiliate/notifications`)

## Channel policy

| Channel | Default |
|---------|---------|
| Email | Branded mailable (`WelcomeMail`, `PasswordChangedMail`, `GenericNotificationMail`) |
| In-app | Always for catalog events (except OTP-only) |
| FCM push | When user has `fcm_token` |
| SMS | **Selective only:** OTP events, password reset OTP, critical overdue (`OverdueReminder`) |

Register FCM after login: `PUT /api/v1/profile/fcm-token`.

## Event catalog (Phase 1)

| Event | `data.type` / push | Channels | Trigger |
|-------|------------------|----------|---------|
| `auth.welcome` | `auth.welcome` | email, in-app, push | Borrower signup complete (`registration_mode=created`) |
| `affiliate.welcome` | `affiliate.welcome` | email, in-app, push | Admin approves affiliate |
| `auth.password_changed` | `auth.password_changed` | email, in-app, push | Password reset success |
| `loan.submitted` | `loan.submitted` | email, in-app, push | Loan booked, status `pending` |
| `loan.approved` | `loan.approved` | email, in-app, push | Admin/auto approve |
| `loan.declined` | `loan.declined` | email, in-app, push | `POST /api/v1/admin/loans/{id}/decline` |
| `loan.disbursed` | `loan.disbursed` | email, in-app, push | Wallet disbursal |
| `loan.repayment_successful` | `loan.repayment_successful` | email, in-app, push | Automated/manual repayment |
| `loan.fully_repaid` | `loan.fully_repaid` | email, in-app, push | Last installment paid |
| `loan.settled` | `loan.settled` | email, in-app, push | Full settlement (wallet/Paystack) |
| `account.activated` / `account.deactivated` | same | email, in-app, push | Admin suspend toggle (borrowers) |
| `affiliate.account_activated` / `affiliate.account_deactivated` | same | email, in-app, push | Affiliate approve/decline or suspend |
| `affiliate.referral_registered` | `affiliate.referral_registered` | email, in-app, push | New referee completes signup |
| `affiliate.referral_loan_disbursed` | `affiliate.referral_loan_disbursed` | email, in-app, push | Referral loan disbursed |
| `affiliate.commission_earned` | `affiliate.commission_earned` | email, in-app, push | Commission credited |
| `affiliate.withdrawal_processing` | `affiliate.withdrawal_processing` | email, in-app, push | Admin approves withdrawal |
| `affiliate.withdrawal_declined` | `affiliate.withdrawal_declined` | email, in-app, push | Admin rejects withdrawal |
| `affiliate.withdrawal_successful` | `affiliate.withdrawal_successful` | email, in-app, push | Paystack `transfer.success` |

OTP events (`borrower.email_otp`, `affiliate.email_otp`, `auth.password_reset_otp`) remain on `EmailService` with optional SMS.

## In-app payload shape

```json
{
  "title": "Loan approved",
  "message": "Hi Jane, your loan LN-ABC has been approved.",
  "event": "loan.approved",
  "data": {
    "type": "loan.approved",
    "event": "loan.approved",
    "loan_reference": "LN-ABC",
    "loan_id": "42"
  }
}
```

Mobile deep links should read `event` or `data.type`.

## Phase 2 — Repayment reminders

**Command:** `php artisan app:send-repayment-reminders` (scheduled daily)

| Event | When |
|-------|------|
| `loan.repayment_reminder_7d` | `due_date = today + 7`, status pending/grace |
| `loan.repayment_reminder_3d` | `due_date = today + 3` |
| `loan.repayment_reminder_1d` | `due_date = tomorrow` |
| `loan.repayment_due_today` | `due_date = today` |
| `loan.overdue_reminder` | `due_date < today`, status overdue/defaulting (**SMS enabled**) |

Dedupe: `notification_logs` table (`user_id`, `event`, `reference_id`, `sent_on`) — one reminder per schedule per day.

## Phase 2 — Admin broadcast

`POST /api/v1/admin/notifications/push` (permission: `settings.write`)

```json
{
  "audience": "borrowers|affiliates|all",
  "title": "Maintenance notice",
  "body": "The app will be down tonight.",
  "image_url": "optional",
  "data": { "type": "admin_broadcast" }
}
```

Channels: in-app + push only (no SMS, no email).

## Phase 2 — Ad banners with body

- `ad_banners.body` nullable text column
- Public/admin banner payloads include `body`
- `POST /api/v1/admin/banners/{id}/publish` — optional inbox + push to `audience` (default `borrowers`)

## FCM requirements

- User must have `fcm_token` set via `PUT /api/v1/profile/fcm-token`
- Firebase credentials in `config/services.php` / `.env` (`FIREBASE_CREDENTIALS`, etc.)
- Push payload includes `type`, `event`, and event-specific keys (`loan_reference`, `reference`, `amount`)

## Testing

```bash
cd api && php artisan migrate --env=testing
php artisan test --filter=NotificationSystemTest
```
