Notification Preferences System
End-to-end email notification management for Oktivo. Users control which emails they receive via per-category toggles. Critical system emails (password reset, onboarding, verification) are never blocked.
Architecture Overview
Notification Preference Schema
Stored as a subdocument on the User collection:
interface INotificationPreferences {
campaignUpdates: boolean; // Campaign lifecycle emails
callNotifications: boolean; // Call completion emails (future)
weeklyReports: boolean; // Weekly digest emails (future)
marketingEmails: boolean; // Promotional emails (future)
lowBalanceAlerts: boolean; // Credit balance warning emails
agentUpdates: boolean; // Agent create/delete emails
accountAlerts: boolean; // Suspend/unsuspend, number assigned
teamCallSummary: boolean; // Twice-daily team-member call summary (account owners)
}Defaults: All true (enabled) for new users. Existing users without the field get defaults via merge-on-read.
API Endpoints
| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | /api/notification-settings | JWT (any role) | Fetch current user's preferences |
| PUT | /api/notification-settings | JWT (any role) | Partial update of preferences |
GET Response
{
"success": true,
"data": {
"campaignUpdates": true,
"callNotifications": true,
"weeklyReports": true,
"marketingEmails": true,
"lowBalanceAlerts": true,
"agentUpdates": true,
"accountAlerts": true,
"teamCallSummary": true
}
}PUT Request (partial only send changed keys)
{
"campaignUpdates": false,
"marketingEmails": false
}PUT Response
{
"success": true,
"message": "Notification preferences updated",
"data": { ... full preferences object ... }
}Validation Rules
- Only known keys accepted (unknown keys → 400)
- Values must be booleans (non-boolean → 400)
- Empty body → 400
- Uses MongoDB
$setwith dot notation for atomic partial updates
Email Categories & Classification
Always-Send Emails (Cannot Be Disabled)
These are critical system emails that bypass all preference checks:
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Password Reset | password_reset | User requests password reset | User |
| Email Verification | email_verification | Signup / verify email | User |
| Org Onboarding | onboarding_org | SuperAdmin creates org | Org Admin |
| Customer Onboarding | onboarding_customer | Admin creates customer | Customer |
| Resend Onboarding (Admin) | resend_onboarding_admin | SuperAdmin re-sends | Org Admin |
| Resend Onboarding (Customer) | resend_onboarding_customer | Admin re-sends | Customer |
| Deletion Initiated | deletion_initiated | User requests account deletion | User |
| Deletion Complete | deletion_complete | Deletion confirmed | User |
| Account Deleted | account_deleted | Account removed | User |
Campaign Updates (campaignUpdates)
Emails about campaign lifecycle events.
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Campaign Created | campaign_created | POST /upload-campaign | Customer |
| Campaign Started | campaign_started | POST /campaigns/:id/start | Customer |
| Campaign Stopped | campaign_stopped | POST /campaigns/:id/stop | Customer |
| Campaign Deleted | campaign_deleted | DELETE /campaigns/:id | Customer |
| Campaign Auto-Stopped | campaign_auto_stopped | Scheduler (9PM IST TRAI window) | Customer |
| Campaign Auto-Resumed | campaign_auto_started | Scheduler (9AM IST TRAI window) | Customer |
| Campaign Report | campaign_report | POST /export-campaign/:id/email | Customer |
| Campaign CSV Export | campaign_csv_export | GET /export-campaign-csv/:id | Customer |
| History CSV Export | history_csv_export | GET /export-history | Customer |
Agent Updates (agentUpdates)
Emails about agent create/delete events.
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Agent Created | agent_created | POST /create-agent | Customer |
| Agent Deleted | agent_deleted | DELETE /delete-agent/:id | Customer |
Low Balance Alerts (lowBalanceAlerts)
Credit balance warning emails with anti-spam (Redis flag).
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Customer Balance < 500 | low_balance_customer_500 | After credit consumption | Customer |
| Customer Balance < 100 (Final) | low_balance_customer_100 | After credit consumption | Customer |
| Org Pool Balance < 500 | low_balance_organization_500 | After credit consumption | Org Admin |
Account Alerts (accountAlerts)
Emails about account status changes and resource assignments.
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Customer Suspended | customer_suspended | Admin suspends customer | Customer |
| Customer Unsuspended | customer_unsuspended | Admin reactivates customer | Customer |
| Org Suspended | org_suspended | SuperAdmin suspends org | Org Admin |
| Org Unsuspended | org_unsuspended | SuperAdmin reactivates org | Org Admin |
| Number Assigned | number_assigned | Admin assigns phone number | Customer |
Team Call Summary (teamCallSummary)
A per-team-member call-activity report emailed to account owners (role customer, accountOwnerUID: null, status: active) twice a day. One email per account; rows are that account's own team members only (owner + members under the same accountOwnerUID), with Total / Inbound / Outbound / P2P / AI Agent counts. Default on. When an owner toggles it off, the guard blocks the email like any other preference-gated category.
| Template Name | Trigger | Recipient | |
|---|---|---|---|
| Team Call Summary | team_call_summary | Scheduler (12:00 PM + 6:00 PM IST) | Account Owner |
Schedule & windows (teamCallSummary.service.ts, cron registered in scheduler.service.ts, timezone Asia/Kolkata, overlap-guarded via isTeamCallSummaryRunning):
| Cron | IST time | Window reported |
|---|---|---|
0 12 * * * | 12:00 PM | Previous full calendar day (yesterday 00:00:00.000 → 23:59:59.999 IST) |
0 18 * * * | 6:00 PM | Today so far (midnight IST → run instant) |
Attribution: calls are grouped per team member via the shared attribution ladder in callAttribution.ts (the same logic the CRM "Member" column uses), not by the raw Contact.crmUserId field. Calls that can't be attributed to a specific member fold into an "Unattributed" row. Excludes callType: "note" and isTestCall: true, matching the platform Daily Call Summary aggregation.
Related: the platform-admin "Daily Call Summary" (
daily_call_summary,dailyCallSummary.service.ts) is not preference-gated — it goes to a hardcoded admin list (see SuperAdmin-Only Emails). Both services share the window-mode helpercomputeIstWindow(now, mode); the platform 12:00 PM run also reports the previous full day, keeping the two reports in sync.
Call Notifications (callNotifications)
Status: Reserved for future use.
Will include call completion summaries, missed call notifications, etc.
Weekly Reports (weeklyReports)
Status: Reserved for future use.
Will include weekly performance digest with campaign stats, call metrics, etc.
Marketing Emails (marketingEmails)
Status: Reserved for future use.
Will include promotional offers, product updates, feature announcements, etc.
SuperAdmin-Only Emails (Not Preference-Gated)
These go to a hardcoded admin email list and are not controlled by notification preferences:
| Template Name | Trigger | |
|---|---|---|
| SIP Trunk Created | sip_trunk_created | POST /superadmin/sip-trunks |
| SIP Trunk Updated | sip_trunk_updated | PUT /superadmin/sip-trunks/:id |
| SIP Trunk Deleted | sip_trunk_deleted | DELETE /superadmin/sip-trunks/:id |
| Phone Number Deleted | phone_number_deleted | DELETE /superadmin/phone-numbers/:id |
| Provider Capacity Created | provider_capacity_created | POST /superadmin/provider-capacity |
| Provider Capacity Updated | provider_capacity_updated | PUT /superadmin/provider-capacity/:id |
| Provider Capacity Deleted | provider_capacity_deleted | DELETE /superadmin/provider-capacity/:id |
| New Signup Notification | new_signup_notification | User signs up |
| Test Call Notification | test_call_notification | User makes a test call |
| Daily Call Summary (platform) | daily_call_summary | Scheduler (12:00 PM + 5:30 PM IST) |
Notification Guard How It Works
Key Design Decisions
- Fail-open: If the DB query fails, the email is sent (better to over-send than silently drop)
- Unmapped templates pass through: Any new email template not yet mapped to a preference will be sent by default
- No userUID = no check: If the caller doesn't pass
userUID, the email is sent (backward compatible) - Single DB query: One lean
findOnewithselect("notificationPreferences")minimal overhead
Frontend Integration
Customer Settings Page (/account → Notifications tab)
UI Components
Both Customer and Admin settings pages show these toggles:
| Toggle | Key | Description |
|---|---|---|
| Campaign Updates | campaignUpdates | Campaign lifecycle emails |
| Agent Updates | agentUpdates | Agent create/delete emails |
| Call Notifications | callNotifications | Call completion emails (future) |
| Low Balance Alerts | lowBalanceAlerts | Credit warning emails |
| Account Alerts | accountAlerts | Status changes, number assignments |
| Weekly Reports | weeklyReports | Performance digest (future) |
| Marketing Emails | marketingEmails | Promotional emails (future) |
| Team Call Summary | teamCallSummary | Twice-daily team-member call summary (customer settings page) |
File Structure
backend/client_backend/src/
├── types/
│ └── index.ts # INotificationPreferences, DEFAULT_NOTIFICATION_PREFERENCES
├── models/
│ └── User.ts # notificationPreferences subdocument schema
├── controllers/
│ └── notification-settings.controller.ts # GET + PUT handlers
├── routes/
│ └── notification-settings.routes.ts # Route definitions
├── services/
│ └── email/
│ ├── notification.guard.ts # shouldSendEmail(), template→preference mapping
│ ├── email.actions.ts # Guard integrated into all customer-facing emails
│ └── index.ts # Re-exports notification guard
app/src/
├── types/
│ └── auth.ts # NotificationPreferences interface
├── services/
│ └── notification.service.ts # API client (getPreferences, updatePreferences)
├── pages/
│ ├── customer/AccountSettingsPage.tsx # NotificationsTab component
│ └── admin/AccountSettingsPage.tsx # AdminNotificationsTab componentAdding a New Email Category
- Add the key to
INotificationPreferencesintypes/index.ts - Set default in
DEFAULT_NOTIFICATION_PREFERENCES - Add schema field in
models/User.tsnotificationPreferences subdocument - Map templates in
notification.guard.ts→TEMPLATE_TO_PREFERENCErecord - Add toggle to the frontend
NOTIFICATION_ITEMSarray in both settings pages - Add to frontend type in
types/auth.ts→NotificationPreferences
No migration needed new fields get defaults via merge-on-read.
Adding a New Email That Respects Preferences
- Add
userUID?: stringto your email function params - Determine which preference category it belongs to
- Add the template name → preference key mapping in
notification.guard.ts - Add the guard check at the top of your function:
export async function sendMyNewEmail(params: {
// ...other params
userUID?: string;
}): Promise<string | null> {
if (params.userUID) {
const allowed = await shouldSendEmail(params.userUID, "my_template_name");
if (!allowed) return null;
}
// ...build and send email
}- Pass
userUIDfrom the controller call site