Yimaru-BackEnd/docs/bulk-scheduled-notifications-integration.md
2026-06-12 04:57:03 -07:00

21 KiB
Raw Blame History

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
  2. Authentication & permissions
  3. Immediate vs scheduled
  4. Shared response envelopes
  5. Scheduled notification object
  6. Bulk SMS
  7. Bulk email
  8. Bulk push
  9. Bulk in-app
  10. Scheduled job management
  11. In-app real-time delivery (WebSocket)
  12. Recipient targeting rules
  13. Error handling
  14. Operational notes & limitations
  15. Admin UI integration checklist

Architecture overview

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:

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)

{
  "message": "Human-readable summary",
  "data": { },
  "success": true,
  "status_code": 200,
  "metadata": null
}

Error (domain.ErrorResponse)

{
  "message": "Short error title",
  "error": "Optional detail string"
}

Immediate send data shape (SMS, email, in-app)

{
  "total_recipients": 150,
  "sent": 148,
  "failed": 2
}

Immediate push data shape

{
  "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.

{
  "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:

{
  "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

POST /api/v1/notifications/bulk-sms
Authorization: Bearer <token>
Content-Type: application/json

{
  "message": "Your subscription expires tomorrow.",
  "role": "STUDENT"
}

Example — scheduled

{
  "message": "Reminder: class starts at 9 AM.",
  "user_ids": [10, 11, 12],
  "scheduled_at": "2026-06-15T08:00:00Z"
}

Example — scheduled with direct phones

{
  "message": "Welcome to Yimaru!",
  "phone_numbers": ["+251911000000"],
  "scheduled_at": "2026-06-15T09:00:00Z"
}

Success responses

Immediate (200):

{
  "message": "Bulk SMS sent",
  "success": true,
  "status_code": 200,
  "data": {
    "total_recipients": 3,
    "sent": 3,
    "failed": 0
  }
}

Scheduled (201):

{
  "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)

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)

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):

{
  "message": "Bulk email sent",
  "success": true,
  "status_code": 200,
  "data": {
    "total_recipients": 50,
    "sent": 49,
    "failed": 1
  }
}

Scheduled (201):

{
  "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

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

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):

{
  "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):

{
  "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

{
  "title": "Maintenance tonight",
  "message": "The app will be unavailable 11 PM1 AM UTC.",
  "role": "STUDENT"
}

Example — scheduled

{
  "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):

{
  "message": "Bulk in-app notification sent",
  "success": true,
  "status_code": 200,
  "data": {
    "total_recipients": 3,
    "sent": 3,
    "failed": 0
  }
}

Scheduled (201):

{
  "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

GET /api/v1/notifications/scheduled?channel=in_app&status=pending&page=1&limit=20
Authorization: Bearer <token>

Response

{
  "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)

{
  "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)

{
  "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:

{
  "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