Skip to content

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:

typescript
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 ​

MethodPathAuthPurpose
GET/api/notification-settingsJWT (any role)Fetch current user's preferences
PUT/api/notification-settingsJWT (any role)Partial update of preferences

GET Response ​

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

json
{
  "campaignUpdates": false,
  "marketingEmails": false
}

PUT Response ​

json
{
  "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 $set with 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:

EmailTemplate NameTriggerRecipient
Password Resetpassword_resetUser requests password resetUser
Email Verificationemail_verificationSignup / verify emailUser
Org Onboardingonboarding_orgSuperAdmin creates orgOrg Admin
Customer Onboardingonboarding_customerAdmin creates customerCustomer
Resend Onboarding (Admin)resend_onboarding_adminSuperAdmin re-sendsOrg Admin
Resend Onboarding (Customer)resend_onboarding_customerAdmin re-sendsCustomer
Deletion Initiateddeletion_initiatedUser requests account deletionUser
Deletion Completedeletion_completeDeletion confirmedUser
Account Deletedaccount_deletedAccount removedUser

Campaign Updates (campaignUpdates) ​

Emails about campaign lifecycle events.

EmailTemplate NameTriggerRecipient
Campaign Createdcampaign_createdPOST /upload-campaignCustomer
Campaign Startedcampaign_startedPOST /campaigns/:id/startCustomer
Campaign Stoppedcampaign_stoppedPOST /campaigns/:id/stopCustomer
Campaign Deletedcampaign_deletedDELETE /campaigns/:idCustomer
Campaign Auto-Stoppedcampaign_auto_stoppedScheduler (9PM IST TRAI window)Customer
Campaign Auto-Resumedcampaign_auto_startedScheduler (9AM IST TRAI window)Customer
Campaign Reportcampaign_reportPOST /export-campaign/:id/emailCustomer
Campaign CSV Exportcampaign_csv_exportGET /export-campaign-csv/:idCustomer
History CSV Exporthistory_csv_exportGET /export-historyCustomer

Agent Updates (agentUpdates) ​

Emails about agent create/delete events.

EmailTemplate NameTriggerRecipient
Agent Createdagent_createdPOST /create-agentCustomer
Agent Deletedagent_deletedDELETE /delete-agent/:idCustomer

Low Balance Alerts (lowBalanceAlerts) ​

Credit balance warning emails with anti-spam (Redis flag).

EmailTemplate NameTriggerRecipient
Customer Balance < 500low_balance_customer_500After credit consumptionCustomer
Customer Balance < 100 (Final)low_balance_customer_100After credit consumptionCustomer
Org Pool Balance < 500low_balance_organization_500After credit consumptionOrg Admin

Account Alerts (accountAlerts) ​

Emails about account status changes and resource assignments.

EmailTemplate NameTriggerRecipient
Customer Suspendedcustomer_suspendedAdmin suspends customerCustomer
Customer Unsuspendedcustomer_unsuspendedAdmin reactivates customerCustomer
Org Suspendedorg_suspendedSuperAdmin suspends orgOrg Admin
Org Unsuspendedorg_unsuspendedSuperAdmin reactivates orgOrg Admin
Number Assignednumber_assignedAdmin assigns phone numberCustomer

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.

EmailTemplate NameTriggerRecipient
Team Call Summaryteam_call_summaryScheduler (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):

CronIST timeWindow reported
0 12 * * *12:00 PMPrevious full calendar day (yesterday 00:00:00.000 → 23:59:59.999 IST)
0 18 * * *6:00 PMToday 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 helper computeIstWindow(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:

EmailTemplate NameTrigger
SIP Trunk Createdsip_trunk_createdPOST /superadmin/sip-trunks
SIP Trunk Updatedsip_trunk_updatedPUT /superadmin/sip-trunks/:id
SIP Trunk Deletedsip_trunk_deletedDELETE /superadmin/sip-trunks/:id
Phone Number Deletedphone_number_deletedDELETE /superadmin/phone-numbers/:id
Provider Capacity Createdprovider_capacity_createdPOST /superadmin/provider-capacity
Provider Capacity Updatedprovider_capacity_updatedPUT /superadmin/provider-capacity/:id
Provider Capacity Deletedprovider_capacity_deletedDELETE /superadmin/provider-capacity/:id
New Signup Notificationnew_signup_notificationUser signs up
Test Call Notificationtest_call_notificationUser makes a test call
Daily Call Summary (platform)daily_call_summaryScheduler (12:00 PM + 5:30 PM IST)

Notification Guard How It Works ​

Key Design Decisions ​

  1. Fail-open: If the DB query fails, the email is sent (better to over-send than silently drop)
  2. Unmapped templates pass through: Any new email template not yet mapped to a preference will be sent by default
  3. No userUID = no check: If the caller doesn't pass userUID, the email is sent (backward compatible)
  4. Single DB query: One lean findOne with select("notificationPreferences") minimal overhead

Frontend Integration ​

Customer Settings Page (/account → Notifications tab) ​

UI Components ​

Both Customer and Admin settings pages show these toggles:

ToggleKeyDescription
Campaign UpdatescampaignUpdatesCampaign lifecycle emails
Agent UpdatesagentUpdatesAgent create/delete emails
Call NotificationscallNotificationsCall completion emails (future)
Low Balance AlertslowBalanceAlertsCredit warning emails
Account AlertsaccountAlertsStatus changes, number assignments
Weekly ReportsweeklyReportsPerformance digest (future)
Marketing EmailsmarketingEmailsPromotional emails (future)
Team Call SummaryteamCallSummaryTwice-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 component

Adding a New Email Category ​

  1. Add the key to INotificationPreferences in types/index.ts
  2. Set default in DEFAULT_NOTIFICATION_PREFERENCES
  3. Add schema field in models/User.ts notificationPreferences subdocument
  4. Map templates in notification.guard.ts → TEMPLATE_TO_PREFERENCE record
  5. Add toggle to the frontend NOTIFICATION_ITEMS array in both settings pages
  6. 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 ​

  1. Add userUID?: string to your email function params
  2. Determine which preference category it belongs to
  3. Add the template name → preference key mapping in notification.guard.ts
  4. Add the guard check at the top of your function:
typescript
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
}
  1. Pass userUID from the controller call site

Last updated: