Common Notifications Service
๐ Overviewโ
Service Path: internal/api/v1/notifications-center/services/common.service.js
The Common Notifications service provides unified operations across both CRM reminders and FCM push notifications collections. Core responsibilities include:
- Unified Count Aggregation: Calculate unread counts across both notification types
- Bulk Read Operations: Mark all unread notifications as read in single operation
- Parallel Execution: Simultaneous queries to both collections for performance
- Module Filtering: Optional filtering by notification module (projects, CRM, etc.)
- Multi-User Support: Independent read tracking per user
๐๏ธ Collections Usedโ
- crm.reminders (link removed - file does not exist) - CRM task/event reminders with due dates
- fcm-notifications (link removed - file does not exist) - Firebase Cloud Messaging push notifications
๐ Data Flowโ
Unified Count Aggregationโ
sequenceDiagram
participant Client
participant CommonService
participant RemindersDB
participant FcmDB
Client->>CommonService: getCount(accountId, userId, module, type)
par Parallel Aggregation
CommonService->>RemindersDB: Aggregate unread reminders
CommonService->>FcmDB: Aggregate unread FCM (with module filter)
end
RemindersDB-->>CommonService: { total: 5 }
FcmDB-->>CommonService: { total: 12, project: 7 }
CommonService->>CommonService: Combine results
CommonService-->>Client: { reminder: {total: 5}, fcm: {total: 12, project: 7}, total: 17 }
Bulk Read Operationโ
flowchart TD
A[Client Request: Mark All Read] --> B[Common Service]
B --> C{Execute in Parallel}
C --> D[Update CRM Reminders]
C --> E[Update FCM Notifications]
D --> F[CRM: $addToSet read_by: userId]
E --> G[FCM: $addToSet read_by: userId]
F --> H{Any Modified?}
G --> H
H -->|Yes| I[Return Success with Counts]
H -->|No| J[Return Already Read Message]
style D fill:#fff4e6
style E fill:#fff4e6
style I fill:#e8f5e9
๐ง Business Logic & Functionsโ
getCount({ module, type, accountId, userId })โ
Purpose: Calculate unread notification counts across CRM reminders and FCM notifications
Parameters:
accountId(ObjectId) - Account to queryuserId(ObjectId) - User to check read status formodule(String, optional) - Filter FCM by module ('projects', 'crm', etc.)type(String, optional) - Filter FCM by notification type
Returns:
{
reminder: {
total: 5 // Unread CRM reminders
},
fcm: {
total: 12, // Total unread FCM notifications
project: 7 // Project-specific unread count
},
total: 17 // Combined unread count
}
Business Logic Flow:
-
Convert IDs to ObjectIds
const accountObjectId = new mongoose.Types.ObjectId(accountId);
const uidObjectId = new mongoose.Types.ObjectId(userId); -
Build FCM Query Options
const fcmOptions = {
account: accountObjectId,
users: uidObjectId, // User must be recipient
removed_by: { $ne: uidObjectId }, // Not dismissed by user
read_by: { $ne: uidObjectId }, // Not read by user
...(module && { module }), // Optional module filter
...(type && { type }), // Optional type filter
};- users field: Array of recipient user IDs
- removed_by: Soft delete per user (dismissed notifications)
- read_by: Array of users who have read
- Dynamically adds module/type filters if provided
-
Build Reminder Query Options
const reminderOptions = {
account: accountObjectId,
assigned: uidObjectId, // User must be assigned
read_by: { $ne: uidObjectId }, // Not read by user
};- assigned field: Single user (reminders have single assignee)
- No module/type filtering for reminders
-
Create Reminder Aggregation
const reminderQuery = [{ $match: reminderOptions }, { $count: 'total' }];- Simple count aggregation
- No facets needed (only one count)
-
Create FCM Aggregation with Facets
const fcmQuery = [
{ $match: fcmOptions },
{
$facet: {
// Project-specific count
project: [{ $match: { module: 'projects' } }, { $count: 'total' }],
// Total count across all modules
all: [{ $count: 'total' }],
},
},
{
$project: {
project: { $ifNull: [{ $arrayElemAt: ['$project.total', 0] }, 0] },
total: { $ifNull: [{ $arrayElemAt: ['$all.total', 0] }, 0] },
},
},
];- $facet: Runs multiple aggregations in parallel
- project facet: Count notifications where module='projects'
- all facet: Count all notifications (after initial filter)
- $ifNull: Returns 0 if no results (handles empty arrays)
-
Execute Aggregations in Parallel
const [reminderData, fcmData] = await Promise.all([
CRMReminder.aggregate(reminderQuery).allowDiskUse(true).exec(),
FcmNotification.aggregate(fcmQuery).allowDiskUse(true).exec(),
]);- Promise.all: Runs both queries simultaneously
- allowDiskUse(true): Allows MongoDB to use disk for large result sets
- Returns arrays:
[reminderResult, fcmResult]
-
Extract Results with Defaults
const reminder = reminderData[0] ?? { total: 0 };
const fcm = fcmData[0] ?? { project: 0, total: 0 };- Nullish coalescing (??): Returns default if undefined/null
- Handles empty result sets gracefully
-
Combine and Return
return {
reminder,
fcm,
total: (reminder.total || 0) + (fcm.total || 0),
};
Key Business Rules:
- Reminder Assignment: User must be assigned to see reminder in count
- FCM Recipients: User must be in recipients array to see FCM notification
- Dismissed Exclusion: FCM notifications dismissed by user don't count
- Module Filtering: Only affects FCM notifications, not reminders
- Project Count: Always returned in FCM object for dashboard widgets
- Zero Defaults: Returns 0 counts if no notifications found
Example Usage:
// Get all unread counts
const counts = await getCount({
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});
// { reminder: {total: 5}, fcm: {total: 12, project: 7}, total: 17 }
// Get project notifications only
const projectCounts = await getCount({
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
module: 'projects',
});
// { reminder: {total: 5}, fcm: {total: 7, project: 7}, total: 12 }
Side Effects:
- None (read-only operation)
Performance Considerations:
-
Parallel Execution: Both queries run simultaneously (~50-150ms total)
-
Index Requirements:
// crm.reminders
{ account: 1, assigned: 1, read_by: 1 }
// fcm-notifications
{ account: 1, users: 1, read_by: 1, removed_by: 1 }
{ account: 1, users: 1, module: 1 } // For module filtering -
Disk Usage: Required for large collections (100K+ documents)
-
Expected Time: 50-150ms depending on collection size
-
Caching Opportunity: Frontend can cache for 30-60 seconds
readCommon({ accountId, userId })โ
Purpose: Mark all unread notifications as read for user in single bulk operation
Parameters:
accountId(ObjectId) - Account to updateuserId(ObjectId) - User marking notifications as read
Returns:
{
success: true,
message: "Notifications marked as read successfully",
stats: {
reminderUpdated: 3, // CRM reminders marked read
fcmUpdated: 8 // FCM notifications marked read
}
}
Business Logic Flow:
-
Convert IDs to ObjectIds
const accountObjectId = new mongoose.Types.ObjectId(accountId);
const uidObjectId = new mongoose.Types.ObjectId(userId); -
Create Base Filter
const baseFilter = {
account: accountObjectId,
read_by: { $ne: uidObjectId }, // Only update unread
};- Filters out notifications already read by user
- Prevents unnecessary updates
-
Create Update Operation
const updateOperation = {
$addToSet: { read_by: uidObjectId },
};- $addToSet: Adds user ID to array only if not already present
- Idempotent operation (safe to run multiple times)
- Won't create duplicate entries
-
Execute Bulk Updates in Parallel
const [reminderResult, fcmResult] = await Promise.all([
// Update CRM Reminders
CRMReminder.updateMany(baseFilter, updateOperation).exec(),
// Update FCM Notifications (additional filters)
FcmNotification.updateMany(
{
...baseFilter,
users: uidObjectId, // Must be a recipient
removed_by: { $ne: uidObjectId }, // Not dismissed
},
updateOperation,
).exec(),
]);- updateMany: Bulk update multiple documents
- Promise.all: Execute both updates simultaneously
- FCM additional filters: Respects user recipients and dismissals
-
Check if Any Updates Made
if (reminderResult.modifiedCount === 0 && fcmResult.modifiedCount === 0) {
return {
success: true,
message: 'All notifications are already marked as read',
stats: {
reminderUpdated: reminderResult.modifiedCount,
fcmUpdated: fcmResult.modifiedCount,
},
};
}- modifiedCount: Number of documents actually modified
- Returns different message if nothing changed
-
Return Success with Statistics
return {
success: true,
message: 'Notifications marked as read successfully',
stats: {
reminderUpdated: reminderResult.modifiedCount,
fcmUpdated: fcmResult.modifiedCount,
},
};
Key Business Rules:
- Bulk Operation: Updates all unread notifications in single query per collection
- Idempotent: Safe to call multiple times (uses $addToSet)
- User-Specific: Only affects authenticated user's read status
- Multi-User Safe: Other users' read status unaffected
- Soft Delete Aware: Respects FCM removed_by (won't update dismissed)
- Statistics: Returns count of modified documents for frontend feedback
Example Usage:
// Mark all notifications as read
const result = await readCommon({
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});
// { success: true, message: "...", stats: { reminderUpdated: 3, fcmUpdated: 8 } }
// Call again (idempotent)
const result2 = await readCommon({
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});
// { success: true, message: "All notifications are already marked as read", stats: { reminderUpdated: 0, fcmUpdated: 0 } }
Side Effects:
- Updates CRM Reminders: Adds user ID to read_by array
- Updates FCM Notifications: Adds user ID to read_by array
- Triggers Change Streams: If enabled, triggers MongoDB change streams
Performance Considerations:
- Bulk Update Efficiency: Much faster than individual updates
- Index Requirements: Same as getCount (account + read_by)
- Expected Time: 100-300ms for 100+ notifications
- Write Concern: Uses default MongoDB write concern (majority)
- No Transaction: Updates are independent (not atomic across collections)
๐ Integration Pointsโ
Internal Servicesโ
-
- Creates reminders with assigned users
- Triggers count refresh on new reminders
- Manages reminder completion status
-
- Creates push notifications for various modules
- Sets recipients via users array
- Tracks dismissals via removed_by
-
Projects Module (link removed - file does not exist):
- Sends FCM notifications for task assignments
- Uses module: 'projects' for filtering
- Includes project metadata in notifications
-
Socket Service (link removed - file does not exist):
- Emits notification events to trigger count refresh
- Real-time updates when new notifications arrive
Frontend Integrationโ
Notification Bell Component:
// Load initial counts
const loadCounts = async () => {
const response = await fetch('/v1/notifications-center/common/count', {
headers: { Authorization: `Bearer ${token}` },
});
const { data } = await response.json();
// Display badge
setBadgeCount(data.total);
setProjectCount(data.fcm.project);
setReminderCount(data.reminder.total);
};
// Mark all as read
const handleMarkAllRead = async () => {
const response = await fetch('/v1/notifications-center/common/read', {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
const { data } = await response.json();
// Show feedback
showToast(`Marked ${data.stats.reminderUpdated + data.stats.fcmUpdated} notifications as read`);
// Refresh counts
await loadCounts();
};
// WebSocket listener
socket.on('notification:new', async () => {
await loadCounts();
showNotificationToast();
});
๐งช Edge Cases & Special Handlingโ
Empty Result Setsโ
No Notifications:
// Both collections return empty arrays
const counts = await getCount({ accountId, userId });
// { reminder: {total: 0}, fcm: {total: 0, project: 0}, total: 0 }
All Notifications Already Readโ
Idempotent Read Operation:
const result = await readCommon({ accountId, userId });
// { success: true, message: "All notifications are already marked as read", stats: { reminderUpdated: 0, fcmUpdated: 0 } }
Module Filtering Edge Casesโ
Invalid Module:
// Invalid module name - returns 0 FCM count
const counts = await getCount({
accountId,
userId,
module: 'nonexistent',
});
// { reminder: {total: 5}, fcm: {total: 0, project: 0}, total: 5 }
Projects Module Filter:
// Only project notifications counted in total
const counts = await getCount({
accountId,
userId,
module: 'projects',
});
// { reminder: {total: 5}, fcm: {total: 7, project: 7}, total: 12 }
Multi-User Scenariosโ
Shared Notifications:
// FCM notification sent to multiple users
const notification = await FcmNotification.create({
account: accountId,
users: [user1Id, user2Id],
module: 'projects',
message: { title: 'Team Update', body: '...' },
});
// User 1 marks all as read
await readCommon({ accountId, userId: user1Id });
// Only adds user1Id to read_by
// User 2 still sees as unread
const user2Counts = await getCount({ accountId, userId: user2Id });
// { ..., fcm: {total: 1, ...}, total: 1 }
Dismissed Notificationsโ
FCM Soft Delete:
// User dismisses notification (adds to removed_by)
await FcmNotification.updateOne({ _id: notificationId }, { $addToSet: { removed_by: userId } });
// Dismissed notifications excluded from counts
const counts = await getCount({ accountId, userId });
// Dismissed notification not included
// Can't mark dismissed notifications as read
await readCommon({ accountId, userId });
// Won't update dismissed notification
โ ๏ธ Important Notesโ
-
Parallel Execution: Always use Promise.all for both collections to minimize latency. Sequential execution would double response time (100ms + 100ms vs 100ms).
-
Index Requirements: Proper indexes critical for performance. Without indexes on account + read_by, queries can take 5-10 seconds on large collections.
-
Project Count Special Case: FCM aggregation always includes project count via facet, even if not filtering by projects. Frontend uses this for project-specific notification bell.
-
Idempotent Operations: readCommon uses $addToSet which is idempotent. Safe to call multiple times without creating duplicate read_by entries.
-
Multi-User Independence: Each user's read_by status tracked independently. Marking notification as read doesn't affect other users who can see same notification.
-
Soft Delete Awareness: readCommon respects FCM removed_by array. Dismissed notifications not updated even though they're in user's recipients list.
-
Module Filtering Scope: Module and type filters only affect FCM notifications, not reminders. Reminders have no module classification.
-
Zero Defaults: Service returns 0 counts for empty result sets using nullish coalescing (??). Prevents undefined errors in frontend.
-
Disk Usage: allowDiskUse(true) required for large collections (100K+ documents). Without it, queries may fail with "exceeded memory limit" error.
-
Write Concern: Uses MongoDB default write concern (majority). For critical operations, consider explicit write concern configuration.
๐ Related Documentationโ
- FCM Service - FCM notification listing and individual read operations
- Reminder Service - CRM reminder listing with category filtering
- CRM Reminders Collection (link removed - file does not exist) - Reminder schema and indexes
- FCM Notifications Collection (link removed - file does not exist) - FCM schema and metadata
- Socket Service (link removed - file does not exist) - Real-time notification events
- Projects Module (link removed - file does not exist) - Project notification creation