838 lines
21 KiB
Markdown
838 lines
21 KiB
Markdown
# Bulk & Scheduled Notifications — Integration Guide
|
||
|
||
This guide documents how admin clients integrate with the Yimaru backend to send **immediate** or **scheduled** bulk notifications across four channels:
|
||
|
||
| Channel | Value | Endpoint |
|
||
|---------|-------|----------|
|
||
| SMS | `sms` | `POST /api/v1/notifications/bulk-sms` |
|
||
| Email | `email` | `POST /api/v1/notifications/bulk-email` |
|
||
| Push (FCM) | `push` | `POST /api/v1/notifications/bulk-push` |
|
||
| In-app | `in_app` | `POST /api/v1/notifications/bulk-in-app` |
|
||
|
||
Scheduled jobs are created by the same bulk endpoints when `scheduled_at` is provided. Management APIs list, inspect, and cancel pending jobs.
|
||
|
||
---
|
||
|
||
## Table of contents
|
||
|
||
1. [Architecture overview](#architecture-overview)
|
||
2. [Authentication & permissions](#authentication--permissions)
|
||
3. [Immediate vs scheduled](#immediate-vs-scheduled)
|
||
4. [Shared response envelopes](#shared-response-envelopes)
|
||
5. [Scheduled notification object](#scheduled-notification-object)
|
||
6. [Bulk SMS](#bulk-sms)
|
||
7. [Bulk email](#bulk-email)
|
||
8. [Bulk push](#bulk-push)
|
||
9. [Bulk in-app](#bulk-in-app)
|
||
10. [Scheduled job management](#scheduled-job-management)
|
||
11. [In-app real-time delivery (WebSocket)](#in-app-real-time-delivery-websocket)
|
||
12. [Recipient targeting rules](#recipient-targeting-rules)
|
||
13. [Error handling](#error-handling)
|
||
14. [Operational notes & limitations](#operational-notes--limitations)
|
||
15. [Admin UI integration checklist](#admin-ui-integration-checklist)
|
||
|
||
---
|
||
|
||
## Architecture overview
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant Admin as Admin client
|
||
participant API as Yimaru API
|
||
participant DB as PostgreSQL
|
||
participant Worker as Scheduler worker
|
||
participant Channel as SMS / Email / FCM / WS
|
||
|
||
alt Immediate send (no scheduled_at)
|
||
Admin->>API: POST /notifications/bulk-*
|
||
API->>Channel: Deliver now
|
||
API-->>Admin: 200 + sent/failed counts
|
||
else Scheduled send (scheduled_at set)
|
||
Admin->>API: POST /notifications/bulk-* + scheduled_at
|
||
API->>DB: INSERT scheduled_notifications (pending)
|
||
API-->>Admin: 201 + ScheduledNotification
|
||
loop Every ~30s
|
||
Worker->>DB: Claim due pending rows
|
||
Worker->>Channel: Dispatch bulk delivery
|
||
Worker->>DB: Mark sent or failed
|
||
end
|
||
end
|
||
```
|
||
|
||
- The scheduler runs **in-process** on each API instance (30-second poll).
|
||
- Row claiming uses `FOR UPDATE SKIP LOCKED` so multiple API replicas can run safely.
|
||
- There is **no separate “create scheduled job” endpoint** — scheduling is done via `scheduled_at` on the bulk send endpoints.
|
||
|
||
---
|
||
|
||
## Authentication & permissions
|
||
|
||
All bulk and scheduled-management endpoints require:
|
||
|
||
```http
|
||
Authorization: Bearer <access_token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
(Push and email use `multipart/form-data`; see below.)
|
||
|
||
### Required RBAC permissions
|
||
|
||
| Action | Permission key |
|
||
|--------|----------------|
|
||
| Bulk SMS | `notifications.bulk_sms` |
|
||
| Bulk email | `notifications.bulk_email` |
|
||
| Bulk push | `notifications.bulk_push` |
|
||
| Bulk in-app | `notifications.bulk_in_app` |
|
||
| List scheduled jobs | `notifications_scheduled.list` |
|
||
| Get scheduled job | `notifications_scheduled.get` |
|
||
| Cancel scheduled job | `notifications_scheduled.cancel` |
|
||
|
||
Team members with the **ADMIN** role bundle receive these permissions after RBAC seeding. Ensure migration `000083` has been applied for `in_app` scheduled channel support.
|
||
|
||
---
|
||
|
||
## Immediate vs scheduled
|
||
|
||
| `scheduled_at` | Behavior | HTTP status |
|
||
|----------------|----------|-------------|
|
||
| **Omitted or empty** | Send immediately | `200 OK` |
|
||
| **Present, RFC3339, future** | Create scheduled job | `201 Created` |
|
||
| **Present, past** | Rejected | `400 Bad Request` |
|
||
| **Invalid format** | Rejected | `400 Bad Request` |
|
||
|
||
**Format:** RFC3339 UTC recommended, e.g. `2026-06-15T09:00:00Z`
|
||
|
||
---
|
||
|
||
## Shared response envelopes
|
||
|
||
### Success (`domain.Response`)
|
||
|
||
```json
|
||
{
|
||
"message": "Human-readable summary",
|
||
"data": { },
|
||
"success": true,
|
||
"status_code": 200,
|
||
"metadata": null
|
||
}
|
||
```
|
||
|
||
### Error (`domain.ErrorResponse`)
|
||
|
||
```json
|
||
{
|
||
"message": "Short error title",
|
||
"error": "Optional detail string"
|
||
}
|
||
```
|
||
|
||
### Immediate send `data` shape (SMS, email, in-app)
|
||
|
||
```json
|
||
{
|
||
"total_recipients": 150,
|
||
"sent": 148,
|
||
"failed": 2
|
||
}
|
||
```
|
||
|
||
### Immediate push `data` shape
|
||
|
||
```json
|
||
{
|
||
"target_users": 150,
|
||
"sent": 200,
|
||
"failed": 5,
|
||
"image": "https://api.example.com/files/notification_images/abc.jpg"
|
||
}
|
||
```
|
||
|
||
> Push `sent`/`failed` count **device tokens**, not users. One user may have multiple devices.
|
||
|
||
---
|
||
|
||
## Scheduled notification object
|
||
|
||
Returned when scheduling (`201`) and from management GET endpoints.
|
||
|
||
```json
|
||
{
|
||
"id": 42,
|
||
"channel": "sms",
|
||
"title": "Optional subject / headline",
|
||
"message": "Body text",
|
||
"html": "Optional HTML (email only)",
|
||
"scheduled_at": "2026-06-15T09:00:00.000Z",
|
||
"status": "pending",
|
||
"target_user_ids": [1, 2, 3],
|
||
"target_role": "STUDENT",
|
||
"target_raw": { "phones": ["+251911000000"] },
|
||
"attempt_count": 0,
|
||
"last_error": "",
|
||
"processing_started_at": null,
|
||
"sent_at": null,
|
||
"cancelled_at": null,
|
||
"created_by": 7,
|
||
"created_at": "2026-06-10T12:00:00.000Z",
|
||
"updated_at": "2026-06-10T12:00:00.000Z"
|
||
}
|
||
```
|
||
|
||
### `channel` values
|
||
|
||
| Value | Description |
|
||
|-------|-------------|
|
||
| `sms` | AfroMessage SMS |
|
||
| `email` | Resend / messenger email |
|
||
| `push` | Firebase Cloud Messaging |
|
||
| `in_app` | DB notification + WebSocket broadcast |
|
||
|
||
### `status` lifecycle
|
||
|
||
| Status | Meaning |
|
||
|--------|---------|
|
||
| `pending` | Waiting for `scheduled_at` |
|
||
| `processing` | Claimed by worker, dispatch in progress |
|
||
| `sent` | Dispatch completed (at least one recipient succeeded) |
|
||
| `failed` | All deliveries failed or unsupported channel |
|
||
| `cancelled` | Cancelled via API before send |
|
||
|
||
### `target_raw` structure
|
||
|
||
Used when recipients cannot be expressed only via `target_user_ids` / `target_role`:
|
||
|
||
```json
|
||
{
|
||
"phones": ["+251911000000", "+251922000000"],
|
||
"emails": ["user@example.com"],
|
||
"type": "system_alert",
|
||
"level": "info"
|
||
}
|
||
```
|
||
|
||
| Field | Used by |
|
||
|-------|---------|
|
||
| `phones` | Scheduled SMS with direct phone numbers |
|
||
| `emails` | Scheduled email with direct addresses |
|
||
| `type`, `level` | Scheduled in-app metadata (defaults: `system_alert`, `info`) |
|
||
|
||
---
|
||
|
||
## Bulk SMS
|
||
|
||
### Endpoint
|
||
|
||
```
|
||
POST /api/v1/notifications/bulk-sms
|
||
Content-Type: application/json
|
||
Permission: notifications.bulk_sms
|
||
```
|
||
|
||
### Request body
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `message` | string | **Yes** | SMS body |
|
||
| `user_ids` | int64[] | No* | Platform user IDs (resolved to `phone_number`) |
|
||
| `role` | string | No* | All users with this platform role |
|
||
| `phone_numbers` | string[] | No* | Direct E.164 or local numbers |
|
||
| `scheduled_at` | string | No | RFC3339; omit for immediate send |
|
||
|
||
\* At least one of `user_ids`, `role`, or `phone_numbers` is required.
|
||
|
||
### Example — immediate
|
||
|
||
```http
|
||
POST /api/v1/notifications/bulk-sms
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"message": "Your subscription expires tomorrow.",
|
||
"role": "STUDENT"
|
||
}
|
||
```
|
||
|
||
### Example — scheduled
|
||
|
||
```json
|
||
{
|
||
"message": "Reminder: class starts at 9 AM.",
|
||
"user_ids": [10, 11, 12],
|
||
"scheduled_at": "2026-06-15T08:00:00Z"
|
||
}
|
||
```
|
||
|
||
### Example — scheduled with direct phones
|
||
|
||
```json
|
||
{
|
||
"message": "Welcome to Yimaru!",
|
||
"phone_numbers": ["+251911000000"],
|
||
"scheduled_at": "2026-06-15T09:00:00Z"
|
||
}
|
||
```
|
||
|
||
### Success responses
|
||
|
||
**Immediate (`200`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Bulk SMS sent",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": {
|
||
"total_recipients": 3,
|
||
"sent": 3,
|
||
"failed": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
**Scheduled (`201`):**
|
||
|
||
```json
|
||
{
|
||
"message": "SMS scheduled",
|
||
"success": true,
|
||
"status_code": 201,
|
||
"data": { /* ScheduledNotification */ }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Bulk email
|
||
|
||
### Endpoint
|
||
|
||
```
|
||
POST /api/v1/notifications/bulk-email
|
||
Content-Type: multipart/form-data
|
||
Permission: notifications.bulk_email
|
||
```
|
||
|
||
### Form fields
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `subject` | string | **Yes** | Email subject (stored as `title` when scheduled) |
|
||
| `message` | string | No** | Plain-text body |
|
||
| `html` | string | No** | HTML body |
|
||
| `user_ids` | string | No* | JSON array string, e.g. `[1,2,3]` |
|
||
| `role` | string | No* | Platform role filter |
|
||
| `emails` | string | No* | JSON array string, e.g. `["a@b.com"]` |
|
||
| `scheduled_at` | string | No | RFC3339 |
|
||
| `file` | file | No | Attachment (immediate send only) |
|
||
|
||
\* At least one of `user_ids`, `role`, or `emails` is required.
|
||
|
||
\** At least one of `message` or `html` is required.
|
||
|
||
### Example — immediate (cURL)
|
||
|
||
```bash
|
||
curl -X POST "https://api.example.com/api/v1/notifications/bulk-email" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "subject=New course available" \
|
||
-F "html=<p>Check out our new module!</p>" \
|
||
-F "role=STUDENT" \
|
||
-F "file=@banner.png"
|
||
```
|
||
|
||
### Example — scheduled (cURL)
|
||
|
||
```bash
|
||
curl -X POST "https://api.example.com/api/v1/notifications/bulk-email" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "subject=Weekly digest" \
|
||
-F "message=Here is your weekly summary." \
|
||
-F "emails=[\"admin@example.com\",\"user@example.com\"]" \
|
||
-F "scheduled_at=2026-06-15T09:00:00Z"
|
||
```
|
||
|
||
> **Note:** Attachments are **not** stored for scheduled emails. Upload and send immediately if an attachment is required.
|
||
|
||
### Success responses
|
||
|
||
**Immediate (`200`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Bulk email sent",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": {
|
||
"total_recipients": 50,
|
||
"sent": 49,
|
||
"failed": 1
|
||
}
|
||
}
|
||
```
|
||
|
||
**Scheduled (`201`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Email scheduled",
|
||
"success": true,
|
||
"status_code": 201,
|
||
"data": {
|
||
"id": 43,
|
||
"channel": "email",
|
||
"title": "Weekly digest",
|
||
"message": "Here is your weekly summary.",
|
||
"html": "",
|
||
"scheduled_at": "2026-06-15T09:00:00.000Z",
|
||
"status": "pending",
|
||
"target_raw": { "emails": ["admin@example.com", "user@example.com"] },
|
||
"created_by": 7
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Bulk push
|
||
|
||
### Endpoint
|
||
|
||
```
|
||
POST /api/v1/notifications/bulk-push
|
||
Content-Type: multipart/form-data
|
||
Permission: notifications.bulk_push
|
||
```
|
||
|
||
Requires **FCM** credentials (`FCM_SERVICE_ACCOUNT_KEY`) on the server.
|
||
|
||
### Form fields
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `title` | string | **Yes** | Notification title |
|
||
| `message` | string | **Yes** | Notification body |
|
||
| `user_ids` | string | No* | JSON array string, e.g. `[1,2,3]` |
|
||
| `role` | string | No* | Platform role filter |
|
||
| `scheduled_at` | string | No | RFC3339 |
|
||
| `file` | file | No | Image (immediate send only) |
|
||
|
||
\* At least one of `user_ids` or `role` is required.
|
||
|
||
### Example — immediate
|
||
|
||
```bash
|
||
curl -X POST "https://api.example.com/api/v1/notifications/bulk-push" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "title=Practice reminder" \
|
||
-F "message=Complete today's practice." \
|
||
-F "user_ids=[1,2,3]" \
|
||
-F "file=@thumb.jpg"
|
||
```
|
||
|
||
### Example — scheduled
|
||
|
||
```bash
|
||
curl -X POST "https://api.example.com/api/v1/notifications/bulk-push" \
|
||
-H "Authorization: Bearer <token>" \
|
||
-F "title=Exam tomorrow" \
|
||
-F "message=Don't forget to review Unit 3." \
|
||
-F "role=STUDENT" \
|
||
-F "scheduled_at=2026-06-14T18:00:00Z"
|
||
```
|
||
|
||
> **Note:** Push images are **not** preserved when scheduling. Use immediate send for image pushes.
|
||
|
||
### Success responses
|
||
|
||
**Immediate (`200`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Bulk push notification sent",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": {
|
||
"target_users": 3,
|
||
"sent": 4,
|
||
"failed": 0,
|
||
"image": "https://api.example.com/files/notification_images/abc.jpg"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Scheduled (`201`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Push notification scheduled",
|
||
"success": true,
|
||
"status_code": 201,
|
||
"data": { /* ScheduledNotification, channel: "push" */ }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Bulk in-app
|
||
|
||
In-app notifications are persisted in the `notifications` table and pushed to connected clients via WebSocket (`CREATED_NOTIFICATION` event). Users without an active socket still see them on next `GET /api/v1/notifications`.
|
||
|
||
### Endpoint
|
||
|
||
```
|
||
POST /api/v1/notifications/bulk-in-app
|
||
Content-Type: application/json
|
||
Permission: notifications.bulk_in_app
|
||
```
|
||
|
||
### Request body
|
||
|
||
| Field | Type | Required | Description |
|
||
|-------|------|----------|-------------|
|
||
| `title` | string | **Yes** | Headline (`payload.headline`) |
|
||
| `message` | string | **Yes** | Body (`payload.message`) |
|
||
| `user_ids` | int64[] | No* | Target user IDs |
|
||
| `role` | string | No* | All users with platform role |
|
||
| `scheduled_at` | string | No | RFC3339 |
|
||
| `type` | string | No | Notification type (default `system_alert`) |
|
||
| `level` | string | No | `info`, `warning`, `error`, `success` (default `info`) |
|
||
|
||
\* At least one of `user_ids` or `role` is required.
|
||
|
||
### Notification `type` values (common)
|
||
|
||
| Value | Use case |
|
||
|-------|----------|
|
||
| `system_alert` | Admin broadcasts (default) |
|
||
| `subscription_expiring` | Billing reminders |
|
||
| `course_completed` | Learning milestones |
|
||
| `payment_verified` | Payment events |
|
||
|
||
### Example — immediate
|
||
|
||
```json
|
||
{
|
||
"title": "Maintenance tonight",
|
||
"message": "The app will be unavailable 11 PM–1 AM UTC.",
|
||
"role": "STUDENT"
|
||
}
|
||
```
|
||
|
||
### Example — scheduled
|
||
|
||
```json
|
||
{
|
||
"title": "New practice unlocked",
|
||
"message": "Unit 4 practice is now available.",
|
||
"user_ids": [100, 101, 102],
|
||
"type": "system_alert",
|
||
"level": "info",
|
||
"scheduled_at": "2026-06-16T07:00:00Z"
|
||
}
|
||
```
|
||
|
||
### Success responses
|
||
|
||
**Immediate (`200`):**
|
||
|
||
```json
|
||
{
|
||
"message": "Bulk in-app notification sent",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": {
|
||
"total_recipients": 3,
|
||
"sent": 3,
|
||
"failed": 0
|
||
}
|
||
}
|
||
```
|
||
|
||
**Scheduled (`201`):**
|
||
|
||
```json
|
||
{
|
||
"message": "In-app notification scheduled",
|
||
"success": true,
|
||
"status_code": 201,
|
||
"data": {
|
||
"id": 44,
|
||
"channel": "in_app",
|
||
"title": "New practice unlocked",
|
||
"message": "Unit 4 practice is now available.",
|
||
"scheduled_at": "2026-06-16T07:00:00.000Z",
|
||
"status": "pending",
|
||
"target_user_ids": [100, 101, 102],
|
||
"target_raw": { "type": "system_alert", "level": "info" }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Scheduled job management
|
||
|
||
### List scheduled notifications
|
||
|
||
```
|
||
GET /api/v1/notifications/scheduled
|
||
Permission: notifications_scheduled.list
|
||
```
|
||
|
||
#### Query parameters
|
||
|
||
| Param | Type | Default | Description |
|
||
|-------|------|---------|-------------|
|
||
| `status` | string | — | `pending`, `processing`, `sent`, `failed`, `cancelled` |
|
||
| `channel` | string | — | `sms`, `email`, `push`, `in_app` |
|
||
| `after` | RFC3339 | — | `scheduled_at >= after` |
|
||
| `before` | RFC3339 | — | `scheduled_at <= before` |
|
||
| `limit` | int | 20 | Page size |
|
||
| `page` | int | 1 | Page number (1-based) |
|
||
|
||
#### Example
|
||
|
||
```http
|
||
GET /api/v1/notifications/scheduled?channel=in_app&status=pending&page=1&limit=20
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
#### Response
|
||
|
||
```json
|
||
{
|
||
"scheduled_notifications": [ /* ScheduledNotification[] */ ],
|
||
"total_count": 42,
|
||
"limit": 20,
|
||
"page": 1
|
||
}
|
||
```
|
||
|
||
### Get one scheduled notification
|
||
|
||
```
|
||
GET /api/v1/notifications/scheduled/:id
|
||
Permission: notifications_scheduled.get
|
||
```
|
||
|
||
#### Response (`200`)
|
||
|
||
```json
|
||
{
|
||
"message": "Scheduled notification retrieved",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": { /* ScheduledNotification */ }
|
||
}
|
||
```
|
||
|
||
### Cancel scheduled notification
|
||
|
||
```
|
||
POST /api/v1/notifications/scheduled/:id/cancel
|
||
Permission: notifications_scheduled.cancel
|
||
```
|
||
|
||
Only jobs with status `pending` or `processing` can be cancelled.
|
||
|
||
#### Response (`200`)
|
||
|
||
```json
|
||
{
|
||
"message": "Scheduled notification cancelled",
|
||
"success": true,
|
||
"status_code": 200,
|
||
"data": {
|
||
"id": 42,
|
||
"status": "cancelled",
|
||
"cancelled_at": "2026-06-10T14:30:00.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## In-app real-time delivery (WebSocket)
|
||
|
||
Mobile/web clients should connect to receive in-app notifications instantly.
|
||
|
||
### Connect
|
||
|
||
```
|
||
GET /api/v1/ws/connect?token=<access_token>
|
||
Permission: notifications.ws_connect (validated via query token)
|
||
```
|
||
|
||
Auth uses the **JWT access token in query string** (not `Authorization` header).
|
||
|
||
### Broadcast payload
|
||
|
||
When an in-app notification is created, connected clients receive:
|
||
|
||
```json
|
||
{
|
||
"type": "CREATED_NOTIFICATION",
|
||
"recipient_id": 123,
|
||
"payload": {
|
||
"id": "notif-uuid",
|
||
"recipient_id": 123,
|
||
"type": "system_alert",
|
||
"level": "info",
|
||
"delivery_channel": "in_app",
|
||
"payload": {
|
||
"headline": "Title",
|
||
"message": "Body"
|
||
},
|
||
"is_read": false,
|
||
"timestamp": "2026-06-10T12:00:00.000Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
### Polling fallback
|
||
|
||
```
|
||
GET /api/v1/notifications?limit=20&offset=0
|
||
Permission: notifications.list_mine
|
||
```
|
||
|
||
```
|
||
GET /api/v1/notifications/unread
|
||
Permission: notifications.count_unread
|
||
```
|
||
|
||
---
|
||
|
||
## Recipient targeting rules
|
||
|
||
### Platform roles (`role` field)
|
||
|
||
Valid platform `users.role` values:
|
||
|
||
| Role | Value |
|
||
|------|-------|
|
||
| Student | `STUDENT` |
|
||
| Open learner | `OPEN_LEARNER` |
|
||
| Instructor | `INSTRUCTOR` |
|
||
| Admin | `ADMIN` |
|
||
| Super admin | `SUPER_ADMIN` |
|
||
| Support | `SUPPORT` |
|
||
|
||
When `role` is set without `user_ids`, the API loads **all users** matching that role.
|
||
|
||
### Combining targets
|
||
|
||
| Channel | `user_ids` | `role` | Direct (`phones` / `emails`) |
|
||
|---------|------------|--------|------------------------------|
|
||
| SMS | ✓ | ✓ | `phone_numbers` |
|
||
| Email | ✓ | ✓ | `emails` (form field) |
|
||
| Push | ✓ | ✓ | — |
|
||
| In-app | ✓ | ✓ | — |
|
||
|
||
For immediate SMS/email, all sources are **unioned** (deduplicated). Scheduled jobs store the targeting fields as-is and resolve at dispatch time.
|
||
|
||
---
|
||
|
||
## Error handling
|
||
|
||
### Common `400` errors
|
||
|
||
| Message | Cause |
|
||
|---------|-------|
|
||
| `Message is required` | Empty SMS/in-app body |
|
||
| `Title is required` | Empty push/in-app title |
|
||
| `No recipients specified` | Missing all targeting fields |
|
||
| `No target users found` | Role/user_ids resolved to zero users |
|
||
| `Invalid scheduled_at format` | Not RFC3339 |
|
||
| `scheduled_at must be in the future` | Past timestamp |
|
||
| `Invalid user_ids format` | Push/email: not a JSON array string |
|
||
|
||
### Common `401` / `403`
|
||
|
||
| Situation | Result |
|
||
|-----------|--------|
|
||
| Missing/invalid Bearer token | `401` |
|
||
| Missing RBAC permission | `403` |
|
||
|
||
### Common `500`
|
||
|
||
| Situation | Result |
|
||
|-----------|--------|
|
||
| FCM not configured (push) | Push send fails |
|
||
| AfroMessage / Resend misconfigured | SMS/email failures |
|
||
| DB error creating scheduled row | Schedule request fails |
|
||
|
||
### Partial delivery
|
||
|
||
For immediate sends, the API returns `sent` and `failed` counts. The HTTP status is still `200` if the request was processed.
|
||
|
||
For scheduled jobs, if **at least one** recipient succeeds, status becomes `sent`. If **all** fail, status becomes `failed` and `last_error` is set.
|
||
|
||
---
|
||
|
||
## Operational notes & limitations
|
||
|
||
| Topic | Detail |
|
||
|-------|--------|
|
||
| Scheduler interval | ~30 seconds (not exact-to-the-second) |
|
||
| Scheduled push images | Not supported — image upload only on immediate push |
|
||
| Scheduled email attachments | Not supported — attachments only on immediate send |
|
||
| Scheduled job retry | No automatic retry; failed jobs stay `failed` |
|
||
| Edit / reschedule | Not supported — cancel and create a new job |
|
||
| Multi-instance | Safe via DB row locking |
|
||
| SMS provider | AfroMessage |
|
||
| Email provider | Resend (messenger service) |
|
||
| Push provider | Firebase Cloud Messaging |
|
||
| History records | Immediate SMS/email/push call `RecordNotification` for known `user_ids`; scheduled SMS/email/push do **not** write per-user history rows. In-app always creates notification rows (immediate and scheduled). |
|
||
| Migration | `000083` required for `in_app` scheduled channel |
|
||
|
||
---
|
||
|
||
## Admin UI integration checklist
|
||
|
||
### Compose screen (per channel)
|
||
|
||
- [ ] Title/subject + message fields (channel-specific)
|
||
- [ ] Recipient picker: user multi-select, role dropdown, and/or direct phones/emails
|
||
- [ ] Toggle: **Send now** vs **Schedule**
|
||
- [ ] Date/time picker → serialize as RFC3339 UTC (`scheduled_at`)
|
||
- [ ] Optional: image upload (push immediate only), email attachment (immediate only)
|
||
- [ ] Optional: in-app `type` and `level` selectors
|
||
|
||
### After submit
|
||
|
||
- [ ] Immediate: show `sent` / `failed` / `total_recipients` from `data`
|
||
- [ ] Scheduled: show returned `id`, `scheduled_at`, `status` — link to job detail view
|
||
|
||
### Scheduled jobs list
|
||
|
||
- [ ] Call `GET /notifications/scheduled` with filters (`status`, `channel`, date range)
|
||
- [ ] Paginate with `page` + `limit`
|
||
- [ ] Show status badges: pending → processing → sent / failed / cancelled
|
||
- [ ] Cancel button → `POST /notifications/scheduled/:id/cancel` for `pending`/`processing`
|
||
|
||
### Learner app (in-app only)
|
||
|
||
- [ ] WebSocket connect on login with `?token=`
|
||
- [ ] Handle `CREATED_NOTIFICATION` events
|
||
- [ ] Fallback poll `GET /notifications` and `GET /notifications/unread`
|
||
|
||
---
|
||
|
||
## Quick reference — all endpoints
|
||
|
||
| Method | Path | Purpose |
|
||
|--------|------|---------|
|
||
| `POST` | `/api/v1/notifications/bulk-sms` | Bulk / schedule SMS |
|
||
| `POST` | `/api/v1/notifications/bulk-email` | Bulk / schedule email |
|
||
| `POST` | `/api/v1/notifications/bulk-push` | Bulk / schedule push |
|
||
| `POST` | `/api/v1/notifications/bulk-in-app` | Bulk / schedule in-app |
|
||
| `GET` | `/api/v1/notifications/scheduled` | List scheduled jobs |
|
||
| `GET` | `/api/v1/notifications/scheduled/:id` | Get scheduled job |
|
||
| `POST` | `/api/v1/notifications/scheduled/:id/cancel` | Cancel scheduled job |
|
||
| `GET` | `/api/v1/ws/connect?token=` | In-app WebSocket |
|
||
| `GET` | `/api/v1/notifications` | User notification inbox |
|