Skip to content

Notifications Module

Comprehensive multi-channel notification system for contest alerts and system announcements.

Overview

The Notifications module handles all notification delivery across multiple channels (Email, WhatsApp, Push) with support for scheduling, retry logic, and delivery tracking.

Location

Server/src/notifications/
├── notifications.module.ts          # Module configuration
├── notifications.controller.ts      # REST API endpoints
├── notifications.service.ts         # Core notification orchestration
├── schemas/
│   └── notification.schema.ts       # Notification MongoDB schema
├── dto/
│   ├── notification.dto.ts          # Query and response DTOs
│   └── email.dto.ts                 # Email-specific DTOs
└── services/
    ├── email-notification.service.ts      # Email delivery
    ├── whatsapp-notification.service.ts   # WhatsApp delivery
    ├── push-notification.service.ts       # Push notification delivery
    └── admin-email.service.ts             # Admin email operations

Module Configuration

typescript
@Module({
  imports: [
    MongooseModule.forFeature([
      { name: User.name, schema: UserSchema },
      { name: Contest.name, schema: ContestSchema },
      { name: Notification.name, schema: NotificationSchema },
    ]),
  ],
  providers: [
    NotificationsService,
    EmailNotificationService,
    WhatsAppNotificationService,
    PushNotificationService,
    AdminEmailService,
  ],
  controllers: [NotificationsController],
  exports: [NotificationsService],
})
export class NotificationsModule {}

Notification Schema

Enums

NotificationStatus

typescript
enum NotificationStatus {
  PENDING = 'PENDING',      // Queued for delivery
  SENT = 'SENT',           // Successfully delivered
  FAILED = 'FAILED',       // Delivery failed
  RETRYING = 'RETRYING',   // Retry in progress
}

NotificationChannel

typescript
enum NotificationChannel {
  EMAIL = 'email',
  WHATSAPP = 'whatsapp',
  PUSH = 'push',
}

NotificationType

typescript
enum NotificationType {
  CONTEST_REMINDER = 'CONTEST_REMINDER',    // Upcoming contest alert
  CONTEST_STARTING = 'CONTEST_STARTING',    // Contest starting now
  CONTEST_ENDING = 'CONTEST_ENDING',        // Contest ending soon
  SYSTEM_ALERT = 'SYSTEM_ALERT',           // System announcements
}

Schema Structure

typescript
{
  // References
  userId: ObjectId,              // User reference (indexed)
  contestId?: ObjectId,          // Contest reference (optional, indexed)
  
  // Notification metadata
  type: NotificationType,        // Type of notification (indexed)
  title: string,                 // Notification title
  message: string,               // Notification message
  payload?: Record<string, any>, // Flexible payload data
  
  // Delivery information
  channels: NotificationChannel[],           // Target channels
  deliveryStatus: ChannelDeliveryStatus[],   // Per-channel status
  status: NotificationStatus,                // Overall status (indexed)
  
  // Timing
  scheduledAt?: Date,            // When to send (indexed)
  sentAt?: Date,                 // When sent (indexed)
  failedAt?: Date,               // When failed
  
  // Retry logic
  retryCount: number,            // Current retry count (default: 0)
  maxRetries: number,            // Max retries allowed (default: 3)
  lastRetryAt?: Date,            // Last retry timestamp
  nextRetryAt?: Date,            // Next retry scheduled (indexed)
  
  // Error tracking
  error?: string,                // Latest error message
  errorHistory: Array<{          // Full error history
    timestamp: Date,
    error: string,
    channel?: NotificationChannel,
  }>,
  
  // User interaction
  isRead: boolean,               // Read status (default: false)
  readAt?: Date,                 // When marked as read
  
  // Metadata
  isActive: boolean,             // Active status (default: true, indexed)
  expiresAt?: Date,              // Auto-cleanup date (indexed, TTL)
  
  // Timestamps
  createdAt: Date,               // Auto-generated
  updatedAt: Date,               // Auto-generated
}

Channel Delivery Status

typescript
interface ChannelDeliveryStatus {
  channel: NotificationChannel;
  status: NotificationStatus;
  sentAt?: Date;
  failedAt?: Date;
  error?: string;
  retryCount: number;
  lastRetryAt?: Date;
}

Virtual Fields

typescript
isDelivered: boolean           // status === SENT
isFailed: boolean              // status === FAILED
isPending: boolean             // status === PENDING
canRetry: boolean              // retryCount < maxRetries && (FAILED || RETRYING)
successfulChannels: string[]   // Channels with SENT status
failedChannels: string[]       // Channels with FAILED status

Indexes

typescript
// Compound indexes for performance
{ userId: 1, createdAt: -1 }
{ userId: 1, status: 1 }
{ userId: 1, contestId: 1 } (unique, sparse)
{ contestId: 1, createdAt: -1 }
{ status: 1, scheduledAt: 1 }
{ status: 1, nextRetryAt: 1 }
{ type: 1, createdAt: -1 }
{ expiresAt: 1 } (TTL index for auto-cleanup)

Core Services

NotificationsService

Main orchestration service for notification delivery.

Key Methods

sendNotification()

typescript
async sendNotification(
  userId: string,
  contestId: string,
  type: NotificationType,
  channels: NotificationChannel[]
): Promise<Notification>

Creates and sends notification across specified channels.

getNotificationHistory()

typescript
async getNotificationHistory(
  userId: string,
  options: {
    page: number;
    limit: number;
    status?: NotificationStatus;
    type?: NotificationType;
    startDate?: Date;
    endDate?: Date;
  }
): Promise<PaginatedNotificationsResponseDto>

Retrieves paginated notification history with filters.

markNotificationAsRead()

typescript
async markNotificationAsRead(id: string): Promise<Notification>

Marks a notification as read.

markAllNotificationsAsRead()

typescript
async markAllNotificationsAsRead(userId: string): Promise<{ modifiedCount: number }>

Marks all notifications as read for a user.

retryFailedNotification()

typescript
async retryFailedNotification(id: string): Promise<Notification>

Retries a failed notification if retry count allows.

getNotificationStats()

typescript
async getNotificationStats(
  userId?: string,
  startDate?: Date,
  endDate?: Date
): Promise<NotificationStatsResponseDto>

Returns notification statistics.

cleanupOldNotifications()

typescript
async cleanupOldNotifications(daysOld: number = 90): Promise<{ deletedCount: number }>

Removes notifications older than specified days.

healthCheckAll()

typescript
async healthCheckAll(): Promise<{
  email: { status: string; configured: boolean };
  whatsapp: { status: string; configured: boolean };
  push: { status: string; configured: boolean };
}>

Checks health of all notification services.

EmailNotificationService

Handles email delivery using Nodemailer.

Methods

send()

typescript
async send(
  email: string,
  payload: {
    userId: string;
    contestId: string;
    contestName: string;
    platform: string;
    startTime: Date;
    hoursUntilStart: number;
  }
): Promise<{ success: boolean; messageId?: string; error?: string }>

Features:

  • HTML email templates
  • Contest reminder formatting
  • Error handling and logging
  • SMTP configuration

WhatsAppNotificationService

Handles WhatsApp message delivery (stub implementation).

Methods

send()

typescript
async send(
  phoneNumber: string,
  payload: NotificationPayload
): Promise<{ success: boolean; messageId?: string; error?: string }>

Note: Currently a stub implementation. Requires WhatsApp Business API integration.

PushNotificationService

Handles push notification delivery (stub implementation).

Methods

send()

typescript
async send(
  userId: string,
  payload: NotificationPayload
): Promise<{ success: boolean; messageId?: string; error?: string }>

Note: Currently a stub implementation. Requires Firebase Cloud Messaging or similar.

AdminEmailService

Admin-specific email operations for bulk and custom emails.

Methods

sendCustomEmail()

typescript
async sendCustomEmail(dto: SendCustomEmailDto): Promise<{
  success: boolean;
  sent: number;
  failed: number;
  results: Array<{ email: string; success: boolean; error?: string }>;
}>

Send custom HTML email to specific addresses.

sendBulkEmail()

typescript
async sendBulkEmail(dto: SendBulkEmailDto): Promise<{
  success: boolean;
  sent: number;
  failed: number;
  results: Array<{ userId: string; email: string; success: boolean; error?: string }>;
}>

Send bulk email to users by IDs.

sendAnnouncement()

typescript
async sendAnnouncement(dto: SendAnnouncementDto): Promise<{
  success: boolean;
  sent: number;
  failed: number;
  totalUsers: number;
}>

Send announcement to all users or filtered subset.

sendContestReminder()

typescript
async sendContestReminder(dto: SendContestReminderDto): Promise<{
  success: boolean;
  sent: number;
  failed: number;
  results: Array<{ userId: string; success: boolean; error?: string }>;
}>

Send contest reminder to specific users.

API Endpoints

Test Endpoints (Admin Only)

POST /notifications/test/email

Test email delivery.

Request:

json
{
  "email": "test@example.com"
}

Response:

json
{
  "success": true,
  "messageId": "abc123",
  "message": "Test email sent to test@example.com",
  "payload": { ... }
}

POST /notifications/test/whatsapp

Test WhatsApp delivery.

Request:

json
{
  "phoneNumber": "+1234567890"
}

POST /notifications/test/push

Test push notification delivery.

Request:

json
{
  "userId": "user-id"
}

Status Endpoints

GET /notifications/status

Get service status for all channels.

Response:

json
{
  "email": { "enabled": true, "configured": true },
  "whatsapp": { "enabled": false, "configured": false },
  "push": { "enabled": false, "configured": false }
}

GET /notifications/health

Health check for all notification services.

Response:

json
{
  "email": { "status": "healthy", "configured": true },
  "whatsapp": { "status": "not_configured", "configured": false },
  "push": { "status": "not_configured", "configured": false }
}

Email Endpoints (Admin Only)

POST /notifications/emails/custom

Send custom email to specific addresses.

Request:

json
{
  "to": ["user1@example.com", "user2@example.com"],
  "subject": "Important Update",
  "html": "<h1>Hello</h1><p>This is a test</p>",
  "text": "Hello, This is a test",
  "replyTo": "support@example.com"
}

Response:

json
{
  "success": true,
  "sent": 2,
  "failed": 0,
  "results": [
    { "email": "user1@example.com", "success": true },
    { "email": "user2@example.com", "success": true }
  ]
}

POST /notifications/emails/bulk

Send bulk email to users by IDs.

Request:

json
{
  "userIds": ["user1", "user2", "user3"],
  "subject": "Contest Alert",
  "html": "<h1>New Contest Available</h1>",
  "text": "New Contest Available"
}

Response:

json
{
  "success": true,
  "sent": 3,
  "failed": 0,
  "results": [
    { "userId": "user1", "email": "user1@example.com", "success": true },
    { "userId": "user2", "email": "user2@example.com", "success": true },
    { "userId": "user3", "email": "user3@example.com", "success": true }
  ]
}

POST /notifications/emails/announcement

Send announcement to all or filtered users.

Request:

json
{
  "subject": "Platform Maintenance",
  "title": "Scheduled Maintenance",
  "message": "We will be performing maintenance on...",
  "actionUrl": "https://example.com/status",
  "actionText": "View Status",
  "filters": {
    "platforms": ["codeforces", "leetcode"],
    "isActive": true
  }
}

Response:

json
{
  "success": true,
  "sent": 150,
  "failed": 2,
  "totalUsers": 152
}

POST /notifications/emails/contest-reminder

Send contest reminder to specific users.

Request:

json
{
  "contestId": "contest-id",
  "userIds": ["user1", "user2"],
  "customMessage": "Don't forget to register!"
}

Response:

json
{
  "success": true,
  "sent": 2,
  "failed": 0,
  "results": [
    { "userId": "user1", "success": true },
    { "userId": "user2", "success": true }
  ]
}

Notification History Endpoints

GET /notifications/notifications

Get notification history with filters.

Query Parameters:

  • userId (optional) - Filter by user ID
  • contestId (optional) - Filter by contest ID
  • status (optional) - Filter by status (PENDING, SENT, FAILED, RETRYING)
  • type (optional) - Filter by type
  • channel (optional) - Filter by channel
  • isRead (optional) - Filter by read status
  • startDate (optional) - Filter by start date (ISO 8601)
  • endDate (optional) - Filter by end date (ISO 8601)
  • page (default: 1) - Page number
  • limit (default: 20) - Items per page
  • sortBy (default: createdAt) - Sort field
  • sortOrder (default: desc) - Sort order

Response:

json
{
  "notifications": [
    {
      "id": "notification-id",
      "userId": "user-id",
      "contestId": "contest-id",
      "type": "CONTEST_REMINDER",
      "title": "Contest Starting Soon",
      "message": "Codeforces Round #900 starts in 2 hours",
      "channels": ["email", "push"],
      "status": "SENT",
      "deliveryStatus": [
        {
          "channel": "email",
          "status": "SENT",
          "sentAt": "2024-02-15T10:00:00Z",
          "retryCount": 0
        }
      ],
      "isRead": false,
      "createdAt": "2024-02-15T08:00:00Z",
      "updatedAt": "2024-02-15T10:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 45,
    "totalPages": 3
  }
}

GET /notifications/notifications/:id

Get notification by ID.

Response:

json
{
  "id": "notification-id",
  "userId": "user-id",
  "type": "CONTEST_REMINDER",
  "title": "Contest Starting Soon",
  "message": "...",
  "status": "SENT",
  "createdAt": "2024-02-15T08:00:00Z"
}

PATCH /notifications/notifications/:id/read

Mark notification as read.

Response:

json
{
  "success": true,
  "notification": {
    "id": "notification-id",
    "isRead": true,
    "readAt": "2024-02-15T12:00:00Z"
  }
}

PATCH /notifications/notifications/user/:userId/read-all

Mark all notifications as read for a user.

Response:

json
{
  "success": true,
  "modifiedCount": 15
}

GET /notifications/notifications/stats

Get notification statistics.

Query Parameters:

  • userId (optional) - Filter by user ID
  • startDate (optional) - Start date (ISO 8601)
  • endDate (optional) - End date (ISO 8601)

Response:

json
{
  "total": 1000,
  "sent": 950,
  "failed": 30,
  "pending": 20,
  "byChannel": {
    "email": 800,
    "whatsapp": 100,
    "push": 100
  },
  "byType": {
    "CONTEST_REMINDER": 700,
    "CONTEST_STARTING": 200,
    "CONTEST_ENDING": 50,
    "SYSTEM_ALERT": 50
  },
  "successRate": 95.0,
  "averageDeliveryTime": 1500
}

POST /notifications/notifications/:id/retry

Retry failed notification.

Response:

json
{
  "success": true,
  "notification": {
    "id": "notification-id",
    "status": "RETRYING",
    "retryCount": 1
  }
}

DELETE /notifications/notifications/cleanup

Cleanup old notifications.

Query Parameters:

  • daysOld (default: 90) - Delete notifications older than this many days

Response:

json
{
  "success": true,
  "deletedCount": 150
}

DTOs

NotificationQueryDto

typescript
{
  userId?: string;
  contestId?: string;
  status?: NotificationStatus;
  type?: NotificationType;
  channel?: NotificationChannel;
  isRead?: boolean;
  startDate?: string;  // ISO 8601
  endDate?: string;    // ISO 8601
  page: number;        // default: 1
  limit: number;       // default: 20
  sortBy: 'createdAt' | 'sentAt' | 'scheduledAt';  // default: 'createdAt'
  sortOrder: 'asc' | 'desc';  // default: 'desc'
}

SendCustomEmailDto

typescript
{
  to: string | string[];  // Email address(es)
  subject: string;        // 1-200 chars
  html: string;           // HTML content
  text?: string;          // Plain text (optional)
  replyTo?: string;       // Reply-to address (optional)
}

SendBulkEmailDto

typescript
{
  userIds: string[];      // 1-1000 user IDs
  subject: string;        // 1-200 chars
  html: string;           // HTML content
  text?: string;          // Plain text (optional)
}

SendAnnouncementDto

typescript
{
  subject: string;        // 1-200 chars
  title: string;          // 1-100 chars
  message: string;        // Announcement message
  actionUrl?: string;     // Optional action URL
  actionText?: string;    // Optional action button text
  filters?: {
    platforms?: string[];  // Filter by platforms
    isActive?: boolean;    // Filter by active status
  };
}

SendContestReminderDto

typescript
{
  contestId: string;
  userIds: string[];      // 1-1000 user IDs
  customMessage?: string; // Optional custom message
}

Configuration

Environment Variables

bash
# Email Configuration (Nodemailer)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
EMAIL_FROM=CodeNotify <noreply@codenotify.com>

# WhatsApp Configuration (Future)
WHATSAPP_API_KEY=your-api-key
WHATSAPP_PHONE_NUMBER=+1234567890

# Push Notification Configuration (Future)
FCM_SERVER_KEY=your-fcm-server-key
FCM_PROJECT_ID=your-project-id

Notification Flow

Contest Reminder Flow

1. Scheduler checks for upcoming contests (every 30 minutes)
2. For each contest, find users who:
   - Have the platform enabled in preferences
   - Match the notifyBefore time window
   - Haven't been notified yet for this contest
3. For each eligible user:
   - Create Notification document with PENDING status
   - Get user's enabled channels from preferences
   - Send notification to each channel:
     a. Email: EmailNotificationService.send()
     b. WhatsApp: WhatsAppNotificationService.send()
     c. Push: PushNotificationService.send()
   - Update deliveryStatus for each channel
4. Update overall notification status:
   - SENT if all channels succeeded
   - FAILED if all channels failed
   - SENT if at least one channel succeeded
5. Log results and errors

Retry Logic

1. Failed notifications are marked with FAILED status
2. Retry job runs periodically (e.g., every hour)
3. For each FAILED notification:
   - Check if retryCount < maxRetries
   - Check if nextRetryAt <= now
   - Attempt redelivery on failed channels
   - Increment retryCount
   - Update status and deliveryStatus
4. If max retries reached, mark as permanently FAILED

Best Practices

✅ Do

  1. Use appropriate channels - Respect user preferences
  2. Handle failures gracefully - Implement retry logic
  3. Log all operations - Track delivery success/failure
  4. Clean up old data - Run periodic cleanup jobs
  5. Test thoroughly - Use test endpoints before production
  6. Monitor delivery rates - Track success/failure metrics
  7. Respect rate limits - Implement throttling for bulk operations

❌ Don't

  1. Don't spam users - Respect notification preferences
  2. Don't ignore errors - Log and handle all failures
  3. Don't send without validation - Validate all DTOs
  4. Don't skip retry logic - Implement proper retry mechanisms
  5. Don't hardcode templates - Use configurable templates
  6. Don't expose sensitive data - Sanitize error messages

Testing

Unit Tests

typescript
describe('NotificationsService', () => {
  it('should create notification with correct channels', async () => {
    const notification = await service.sendNotification(
      'user-id',
      'contest-id',
      NotificationType.CONTEST_REMINDER,
      [NotificationChannel.EMAIL]
    );
    expect(notification.channels).toContain(NotificationChannel.EMAIL);
  });

  it('should mark notification as read', async () => {
    const notification = await service.markNotificationAsRead('notification-id');
    expect(notification.isRead).toBe(true);
    expect(notification.readAt).toBeDefined();
  });
});

Integration Tests

typescript
describe('NotificationsController (e2e)', () => {
  it('POST /notifications/test/email should send test email', () => {
    return request(app.getHttpServer())
      .post('/notifications/test/email')
      .set('Authorization', `Bearer ${adminToken}`)
      .send({ email: 'test@example.com' })
      .expect(200)
      .expect((res) => {
        expect(res.body.success).toBe(true);
      });
  });
});

Monitoring

Key Metrics

  • Delivery Success Rate: (sent / total) * 100
  • Channel Performance: Success rate per channel
  • Average Delivery Time: Time from creation to delivery
  • Retry Rate: (retried / failed) * 100
  • Failure Reasons: Common error patterns

Health Checks

bash
# Check service status
curl http://localhost:3000/notifications/status

# Check service health
curl http://localhost:3000/notifications/health

# Get statistics
curl http://localhost:3000/notifications/notifications/stats

Troubleshooting

Email Not Sending

Symptoms: Email notifications fail with SMTP errors

Solutions:

  1. Verify SMTP credentials in .env
  2. Check SMTP host and port
  3. Enable "Less secure app access" for Gmail
  4. Use app-specific password for Gmail
  5. Check firewall/network settings

High Failure Rate

Symptoms: Many notifications marked as FAILED

Solutions:

  1. Check service health endpoints
  2. Review error logs for patterns
  3. Verify external service credentials
  4. Check rate limits
  5. Implement exponential backoff

Notifications Not Received

Symptoms: Users not receiving notifications

Solutions:

  1. Check user preferences (channels enabled)
  2. Verify notifyBefore time window
  3. Check notification history for user
  4. Verify contest sync is working
  5. Check spam folders for emails

Built with ❤️ for competitive programmers worldwide.