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 operationsModule Configuration
@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
enum NotificationStatus {
PENDING = 'PENDING', // Queued for delivery
SENT = 'SENT', // Successfully delivered
FAILED = 'FAILED', // Delivery failed
RETRYING = 'RETRYING', // Retry in progress
}NotificationChannel
enum NotificationChannel {
EMAIL = 'email',
WHATSAPP = 'whatsapp',
PUSH = 'push',
}NotificationType
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
{
// 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
interface ChannelDeliveryStatus {
channel: NotificationChannel;
status: NotificationStatus;
sentAt?: Date;
failedAt?: Date;
error?: string;
retryCount: number;
lastRetryAt?: Date;
}Virtual Fields
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 statusIndexes
// 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()
async sendNotification(
userId: string,
contestId: string,
type: NotificationType,
channels: NotificationChannel[]
): Promise<Notification>Creates and sends notification across specified channels.
getNotificationHistory()
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()
async markNotificationAsRead(id: string): Promise<Notification>Marks a notification as read.
markAllNotificationsAsRead()
async markAllNotificationsAsRead(userId: string): Promise<{ modifiedCount: number }>Marks all notifications as read for a user.
retryFailedNotification()
async retryFailedNotification(id: string): Promise<Notification>Retries a failed notification if retry count allows.
getNotificationStats()
async getNotificationStats(
userId?: string,
startDate?: Date,
endDate?: Date
): Promise<NotificationStatsResponseDto>Returns notification statistics.
cleanupOldNotifications()
async cleanupOldNotifications(daysOld: number = 90): Promise<{ deletedCount: number }>Removes notifications older than specified days.
healthCheckAll()
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()
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()
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()
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()
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()
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()
async sendAnnouncement(dto: SendAnnouncementDto): Promise<{
success: boolean;
sent: number;
failed: number;
totalUsers: number;
}>Send announcement to all users or filtered subset.
sendContestReminder()
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:
{
"email": "test@example.com"
}Response:
{
"success": true,
"messageId": "abc123",
"message": "Test email sent to test@example.com",
"payload": { ... }
}POST /notifications/test/whatsapp
Test WhatsApp delivery.
Request:
{
"phoneNumber": "+1234567890"
}POST /notifications/test/push
Test push notification delivery.
Request:
{
"userId": "user-id"
}Status Endpoints
GET /notifications/status
Get service status for all channels.
Response:
{
"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:
{
"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:
{
"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:
{
"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:
{
"userIds": ["user1", "user2", "user3"],
"subject": "Contest Alert",
"html": "<h1>New Contest Available</h1>",
"text": "New Contest Available"
}Response:
{
"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:
{
"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:
{
"success": true,
"sent": 150,
"failed": 2,
"totalUsers": 152
}POST /notifications/emails/contest-reminder
Send contest reminder to specific users.
Request:
{
"contestId": "contest-id",
"userIds": ["user1", "user2"],
"customMessage": "Don't forget to register!"
}Response:
{
"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 IDcontestId(optional) - Filter by contest IDstatus(optional) - Filter by status (PENDING, SENT, FAILED, RETRYING)type(optional) - Filter by typechannel(optional) - Filter by channelisRead(optional) - Filter by read statusstartDate(optional) - Filter by start date (ISO 8601)endDate(optional) - Filter by end date (ISO 8601)page(default: 1) - Page numberlimit(default: 20) - Items per pagesortBy(default: createdAt) - Sort fieldsortOrder(default: desc) - Sort order
Response:
{
"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:
{
"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:
{
"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:
{
"success": true,
"modifiedCount": 15
}GET /notifications/notifications/stats
Get notification statistics.
Query Parameters:
userId(optional) - Filter by user IDstartDate(optional) - Start date (ISO 8601)endDate(optional) - End date (ISO 8601)
Response:
{
"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:
{
"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:
{
"success": true,
"deletedCount": 150
}DTOs
NotificationQueryDto
{
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
{
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
{
userIds: string[]; // 1-1000 user IDs
subject: string; // 1-200 chars
html: string; // HTML content
text?: string; // Plain text (optional)
}SendAnnouncementDto
{
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
{
contestId: string;
userIds: string[]; // 1-1000 user IDs
customMessage?: string; // Optional custom message
}Configuration
Environment Variables
# 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-idNotification 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 errorsRetry 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 FAILEDBest Practices
✅ Do
- Use appropriate channels - Respect user preferences
- Handle failures gracefully - Implement retry logic
- Log all operations - Track delivery success/failure
- Clean up old data - Run periodic cleanup jobs
- Test thoroughly - Use test endpoints before production
- Monitor delivery rates - Track success/failure metrics
- Respect rate limits - Implement throttling for bulk operations
❌ Don't
- Don't spam users - Respect notification preferences
- Don't ignore errors - Log and handle all failures
- Don't send without validation - Validate all DTOs
- Don't skip retry logic - Implement proper retry mechanisms
- Don't hardcode templates - Use configurable templates
- Don't expose sensitive data - Sanitize error messages
Testing
Unit Tests
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
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
# 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/statsTroubleshooting
Email Not Sending
Symptoms: Email notifications fail with SMTP errors
Solutions:
- Verify SMTP credentials in
.env - Check SMTP host and port
- Enable "Less secure app access" for Gmail
- Use app-specific password for Gmail
- Check firewall/network settings
High Failure Rate
Symptoms: Many notifications marked as FAILED
Solutions:
- Check service health endpoints
- Review error logs for patterns
- Verify external service credentials
- Check rate limits
- Implement exponential backoff
Notifications Not Received
Symptoms: Users not receiving notifications
Solutions:
- Check user preferences (channels enabled)
- Verify notifyBefore time window
- Check notification history for user
- Verify contest sync is working
- Check spam folders for emails

