Skip to main content

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 query
  • userId (ObjectId) - User to check read status for
  • module (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:

  1. Convert IDs to ObjectIds

    const accountObjectId = new mongoose.Types.ObjectId(accountId);
    const uidObjectId = new mongoose.Types.ObjectId(userId);
  2. 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
  3. 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
  4. Create Reminder Aggregation

    const reminderQuery = [{ $match: reminderOptions }, { $count: 'total' }];
    • Simple count aggregation
    • No facets needed (only one count)
  5. 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)
  6. 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]
  7. 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
  8. 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 update
  • userId (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:

  1. Convert IDs to ObjectIds

    const accountObjectId = new mongoose.Types.ObjectId(accountId);
    const uidObjectId = new mongoose.Types.ObjectId(userId);
  2. Create Base Filter

    const baseFilter = {
    account: accountObjectId,
    read_by: { $ne: uidObjectId }, // Only update unread
    };
    • Filters out notifications already read by user
    • Prevents unnecessary updates
  3. 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
  4. 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
  5. 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
  6. 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โ€‹

  • CRM Reminders:

    • Creates reminders with assigned users
    • Triggers count refresh on new reminders
    • Manages reminder completion status
  • FCM Notifications:

    • 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โ€‹

  1. Parallel Execution: Always use Promise.all for both collections to minimize latency. Sequential execution would double response time (100ms + 100ms vs 100ms).

  2. Index Requirements: Proper indexes critical for performance. Without indexes on account + read_by, queries can take 5-10 seconds on large collections.

  3. 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.

  4. Idempotent Operations: readCommon uses $addToSet which is idempotent. Safe to call multiple times without creating duplicate read_by entries.

  5. 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.

  6. Soft Delete Awareness: readCommon respects FCM removed_by array. Dismissed notifications not updated even though they're in user's recipients list.

  7. Module Filtering Scope: Module and type filters only affect FCM notifications, not reminders. Reminders have no module classification.

  8. Zero Defaults: Service returns 0 counts for empty result sets using nullish coalescing (??). Prevents undefined errors in frontend.

  9. Disk Usage: allowDiskUse(true) required for large collections (100K+ documents). Without it, queries may fail with "exceeded memory limit" error.

  10. Write Concern: Uses MongoDB default write concern (majority). For critical operations, consider explicit write concern configuration.

  • 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
๐Ÿ’ฌ

Documentation Assistant

Ask me anything about the docs

Hi! I'm your documentation assistant. Ask me anything about the docs!

I can help you with:
- Code examples
- Configuration details
- Troubleshooting
- Best practices

Try asking: How do I configure the API?
09:31 AM