Skip to main content

Reminder Notifications Service

๐Ÿ“– Overviewโ€‹

Service Path: internal/api/v1/notifications-center/services/reminder.service.js

The Reminder Notifications service manages CRM reminder notifications including tasks and events with due dates. Core responsibilities include:

  • Category-Based Filtering: Filter by upcoming, past due, or completed reminders
  • Rich Data Population: Load related deals, contacts, businesses, and people
  • Dynamic Click Actions: Generate deep-link URLs to deal/contact pages
  • Category Counts: Calculate counts for all categories in single query
  • Pinned Reminders: Prioritize pinned reminders in sort order
  • Read Status Tracking: Per-user read status from read_by array
  • Deep Lookups: Multi-level $lookup for comprehensive data

๐Ÿ—„๏ธ Collections Usedโ€‹

  • crm.reminders (link removed - file does not exist) - CRM reminder tasks and events
  • crm.deals (link removed - file does not exist) - Deal data for deal-type reminders
  • crm.contacts (link removed - file does not exist) - Contact, business, and person data

๐Ÿ”„ Data Flowโ€‹

Reminder Category Logicโ€‹

flowchart TD
A[Reminder Created] --> B{Check reminder_status}

B -->|false - Not Completed| C{Compare due_date_time to now}
B -->|true - Completed| D[COMPLETED Category]

C -->|due_date >= now| E[UPCOMING Category]
C -->|due_date < now| F[PAST DUE Category]

E -->|Time Passes| C
F -->|User Completes| G[Update reminder_status = true]
G --> D

style E fill:#e8f5e9
style F fill:#ffebee
style D fill:#e3f2fd

Data Population Flowโ€‹

sequenceDiagram
participant Client
participant ReminderService
participant RemindersDB
participant DealsDB
participant ContactsDB

Client->>ReminderService: getReminder(skip, limit, type, accountId, userId)

ReminderService->>RemindersDB: Aggregate with $facet
Note over RemindersDB: counts facet + data facet

alt Deal-Type Reminder
RemindersDB->>DealsDB: $lookup deal
DealsDB->>ContactsDB: $lookup business + person
ContactsDB-->>RemindersDB: Populated deal data
else Contact-Type Reminder
RemindersDB->>ContactsDB: $lookup contact
ContactsDB->>ContactsDB: $lookup businesses, people, deals
ContactsDB-->>RemindersDB: Populated contact data
end

RemindersDB->>RemindersDB: Build click_action URLs
RemindersDB-->>ReminderService: { counts, data }

ReminderService-->>Client: Reminders with populated data

๐Ÿ”ง Business Logic & Functionsโ€‹

getReminder({ skip, limit, type, accountId, userId })โ€‹

Purpose: Retrieve paginated list of CRM reminders with category filtering and rich data population

Parameters:

  • skip (Number) - Number of documents to skip (pagination)
  • limit (Number) - Maximum documents to return
  • type (String) - Category filter: 'upcoming', 'past_due', 'completed', or 'all'
  • accountId (ObjectId) - Account to query
  • userId (ObjectId) - User assigned to reminders

Returns:

{
counts: [{
past_due: 5,
upcoming: 12,
completed: 23,
total: 40
}],
data: [
{
id: ObjectId,
headline: String,
content: String,
type: 'deal' | 'contact',
reminder_type: 'task' | 'event' | 'call',
reminder_category: 'upcoming' | 'past_due' | 'completed',
priority: 'low' | 'medium' | 'high',
pinned: Boolean,
reminder_status: Boolean,
due_date_time: Date,
is_read: Boolean,
created_at: Date,
updated_at: Date,
assigned: ObjectId,
created_by: ObjectId,
deal: { // If type='deal'
id: ObjectId,
name: String,
status: String,
pipeline_id: ObjectId,
business: { /* contact data */ },
person: { /* contact data */ }
},
contact: { // If type='contact'
id: ObjectId,
name: String,
email: String,
phone: String,
image: String,
type: 'person' | 'business',
deals: [/* deal objects */],
businesses: [/* business objects */],
people: [/* person objects */]
},
click_action: String // Generated deep-link URL
}
]
}

Business Logic Flow:

  1. Convert IDs and Get Current Date

    const accountObjectId = new mongoose.Types.ObjectId(accountId);
    const uidObjectId = new mongoose.Types.ObjectId(userId);
    const currentDate = new Date();
  2. Get Active Domain Name

    const domainName = await getActiveDomain({
    accountId: accountObjectId.toString(),
    proto: true,
    });
    • Fetches account's custom domain or default domain
    • Includes protocol (https://)
    • Used for building click_action URLs
  3. Build Base Match Options

    const options = {
    account: accountObjectId,
    assigned: uidObjectId, // User must be assigned
    };
    • Only shows reminders assigned to specific user
    • Account-scoped for multi-tenancy
  4. Define Category Filters

    const reminderFilters = {
    upcoming: {
    reminder_status: false,
    due_date_time: { $gte: currentDate },
    },
    past_due: {
    due_date_time: { $lt: currentDate },
    reminder_status: false,
    },
    completed: {
    reminder_status: true,
    },
    };

    const filter = reminderFilters[type] || {};
    • upcoming: Not completed AND due date in future
    • past_due: Not completed AND due date in past
    • completed: Marked as complete (regardless of due date)
    • Empty filter: Returns all categories if type not recognized
  5. Create Aggregation Pipeline with Facets

    const query = [
    { $match: options },
    {
    $facet: {
    // Calculate category counts
    counts: [
    {
    $group: {
    _id: null,
    past_due: {
    $sum: {
    $cond: [
    {
    $and: [
    { $lt: ['$due_date_time', currentDate] },
    { $eq: ['$reminder_status', false] },
    ],
    },
    1,
    0,
    ],
    },
    },
    upcoming: {
    $sum: {
    $cond: [
    {
    $and: [
    { $gte: ['$due_date_time', currentDate] },
    { $eq: ['$reminder_status', false] },
    ],
    },
    1,
    0,
    ],
    },
    },
    completed: {
    $sum: { $cond: [{ $eq: ['$reminder_status', true] }, 1, 0] },
    },
    total: { $sum: 1 },
    },
    },
    ],

    // Get filtered, paginated data
    data: [
    { $match: filter },
    { $sort: { pinned: -1, due_date_time: -1 } },
    { $skip: skip },
    { $limit: limit },
    // ... (continued below)
    ],
    },
    },
    ];
    • counts facet: Calculates all category counts in single pass
      • Uses $cond to count documents matching each category
      • Returns single document with all counts
    • data facet: Filters by selected category and paginates
  6. Add reminder_category Field

    {
    $addFields: {
    reminder_category: {
    $cond: [
    { $eq: ['$reminder_status', true] },
    'completed',
    {
    $cond: [
    { $lt: ['$due_date_time', currentDate] },
    'past_due',
    'upcoming',
    ],
    },
    ],
    },
    },
    }
    • Dynamically calculates category for each reminder
    • Completed takes precedence over due date logic
  7. Lookup Deal Data (for type='deal')

    {
    $lookup: {
    from: 'crm.deals',
    localField: 'deal',
    foreignField: '_id',
    as: 'deal',
    pipeline: [
    // Nested lookup for business
    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'business',
    foreignField: '_id',
    as: 'business',
    },
    },
    // Nested lookup for person
    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'person',
    foreignField: '_id',
    as: 'person',
    },
    },
    {
    $project: {
    _id: 0,
    id: '$_id',
    name: 1,
    status: 1,
    pipeline_id: 1,
    business: { $arrayElemAt: ['$business', 0] },
    person: { $arrayElemAt: ['$person', 0] },
    },
    },
    ],
    },
    }
    • 3-level lookup: reminder โ†’ deal โ†’ business/person contacts
    • Returns single deal object with nested contact data
  8. Lookup Contact Data (for type='contact')

    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'contact',
    foreignField: '_id',
    as: 'contact',
    pipeline: [
    // Lookup related businesses
    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'businesses',
    foreignField: '_id',
    as: 'businesses',
    },
    },
    // Lookup related people
    {
    $lookup: {
    from: 'crm.contacts',
    localField: 'people',
    foreignField: '_id',
    as: 'people',
    },
    },
    // Lookup related deals
    {
    $lookup: {
    from: 'crm.deals',
    localField: 'deals',
    foreignField: '_id',
    as: 'deals',
    },
    },
    {
    $project: {
    _id: 0,
    id: '$_id',
    name: 1,
    email: 1,
    image: 1,
    phone: 1,
    type: 1,
    deals: 1,
    businesses: 1,
    people: 1,
    },
    },
    ],
    },
    }
    • 4-level lookup: reminder โ†’ contact โ†’ businesses/people/deals
    • Returns comprehensive contact relationship data
  9. Flatten Arrays and Add is_read

    {
    $addFields: {
    deal: { $arrayElemAt: ['$deal', 0] },
    contact: { $arrayElemAt: ['$contact', 0] },
    is_read: {
    $cond: {
    if: { $in: [uidObjectId, { $ifNull: ['$read_by', []] }] },
    then: true,
    else: false,
    },
    },
    },
    }
    • $lookup returns arrays, convert to single object
    • Calculate is_read per user from read_by array
  10. Build click_action URLs

    {
    $project: {
    id: '$_id',
    _id: 0,
    headline: 1,
    content: 1,
    type: 1,
    reminder_type: 1,
    reminder_category: 1,
    priority: 1,
    pinned: 1,
    reminder_status: 1,
    due_date_time: 1,
    is_read: 1,
    created_at: '$createdAt',
    updated_at: '$updatedAt',
    assigned: 1,
    created_by: 1,
    deal: 1,
    contact: 1,
    click_action: {
    $switch: {
    branches: [
    {
    case: { $eq: ['$type', 'deal'] },
    then: {
    $cond: {
    if: { $ifNull: ['$deal', false] },
    then: {
    $concat: [
    domainName,
    '/deals/my-deal?type=deal',
    '&pipeline=',
    { $toString: { $ifNull: ['$deal.pipeline_id', ''] } },
    '&id=',
    { $toString: { $ifNull: ['$deal.id', ''] } },
    ],
    },
    else: domainName,
    },
    },
    },
    {
    case: { $eq: ['$type', 'contact'] },
    then: {
    $cond: {
    if: { $ifNull: ['$contact', false] },
    then: {
    $concat: [
    domainName,
    '/contacts?type=',
    {
    $cond: {
    if: { $eq: ['$contact.type', 'person'] },
    then: 'people',
    else: 'businesses',
    },
    },
    '&id=',
    { $toString: { $ifNull: ['$contact.id', ''] } },
    '&tab=activity',
    ],
    },
    else: domainName,
    },
    },
    },
    ],
    default: domainName,
    },
    },
    },
    }
    • Deal URLs: /deals/my-deal?type=deal&pipeline={id}&id={id}
    • Contact URLs: /contacts?type=people&id={id}&tab=activity (or businesses)
    • Falls back to domain root if data missing
  11. Execute Aggregation

    const reminderData = await CRMReminder.aggregate(query);
    return reminderData[0] || { counts: [], data: [] };
    • Returns first element with counts and data facets
    • Returns empty structure if no results

Key Business Rules:

  • Assignment Filtering: Only shows reminders assigned to specified user
  • Category Logic:
    • Completed status takes precedence over due date
    • Upcoming: Not completed + due date >= current date
    • Past Due: Not completed + due date < current date
  • Sorting Priority:
    1. Pinned reminders first (pinned: -1)
    2. Due date descending (due_date_time: -1)
    3. Most urgent/important appears first
  • Category Counts: Always calculated regardless of filter
  • Deep Population: 3-4 levels of $lookup for comprehensive data
  • Dynamic URLs: Click actions generated in database for consistency

Example Usage:

// Get upcoming reminders
const upcomingResult = await getReminder({
skip: 0,
limit: 25,
type: 'upcoming',
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});
// {
// counts: [{ past_due: 5, upcoming: 12, completed: 23, total: 40 }],
// data: [...12 upcoming reminders with populated data]
// }

// Get past due reminders
const pastDueResult = await getReminder({
skip: 0,
limit: 25,
type: 'past_due',
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});
// {
// counts: [{ past_due: 5, upcoming: 12, completed: 23, total: 40 }],
// data: [...5 past due reminders]
// }

Side Effects:

  • None (read-only operation)

Performance Considerations:

  • Complex Aggregation: 3-4 level $lookup operations (expensive)

  • allowDiskUse: Required for large result sets (automatically handled)

  • Index Requirements:

    // Primary index
    { account: 1, assigned: 1, reminder_status: 1, due_date_time: 1 }

    // Sort index
    { account: 1, assigned: 1, pinned: -1, due_date_time: -1 }

    // Read filtering
    { account: 1, assigned: 1, read_by: 1 }
  • Expected Time: 200-500ms (due to nested lookups)

  • Optimization Opportunity: Consider denormalizing frequently accessed fields


readReminder({ notificationId, accountId, userId })โ€‹

Purpose: Mark single reminder as read for authenticated user

Parameters:

  • notificationId (ObjectId) - Reminder ID to mark as read
  • accountId (ObjectId) - Account reminder belongs to
  • userId (ObjectId) - User marking as read

Returns:

{
_id: ObjectId,
headline: String,
content: String,
type: String,
reminder_type: String,
priority: String,
pinned: Boolean,
reminder_status: Boolean,
due_date_time: Date,
read_by: [ObjectId], // Updated with userId
account: ObjectId,
assigned: ObjectId,
created_by: ObjectId,
deal: ObjectId,
contact: ObjectId,
createdAt: Date,
updatedAt: Date
}

Business Logic Flow:

  1. Convert IDs to ObjectIds

    const uidObjectId = new mongoose.Types.ObjectId(userId);
  2. Build Match Options

    const options = {
    _id: new mongoose.Types.ObjectId(notificationId),
    account: new mongoose.Types.ObjectId(accountId),
    read_by: { $ne: uidObjectId }, // Not already read
    };
    • Filters out reminders already read by user
    • Note: No assigned check (unlike getFCM, doesn't verify assignment)
  3. Find Reminder

    const reminderData = await CRMReminder.findOne(options).lean().exec();

    if (!reminderData) {
    throw notFound('Reminder not found');
    }
    • lean(): Returns plain JavaScript object
    • Throws 404: If not found or already read
  4. Update Reminder (Add User to read_by)

    const reminderRead = await CRMReminder.findOneAndUpdate(
    options,
    { $addToSet: { read_by: uidObjectId } },
    { new: true },
    );
    • $addToSet: Idempotent, adds only if not present
    • new: true: Returns updated document
    • Uses same conditions as findOne for safety
  5. Return Updated Document

    return reminderRead;

Key Business Rules:

  • Already Read: Returns 404 if userId in read_by array
  • Atomic Operation: Uses $addToSet for idempotent updates
  • Full Document Return: Returns complete updated reminder (unlike FCM's {success: true})
  • Multi-User Safe: Only adds current user to read_by
  • No Assignment Check: Doesn't verify user is assigned (security consideration)

Example Usage:

// Mark reminder as read
try {
const reminder = await readReminder({
notificationId: '670512a5e4b0f8a5c9d3e1b2',
accountId: '60a7f8d5e4b0d8f3a4c5e1b0',
userId: '60a7f8d5e4b0d8f3a4c5e1b1',
});

// Access updated reminder data
console.log(reminder.read_by); // [..., userId]
} catch (error) {
// error.message === 'Reminder not found'
// Could mean: not exists or already read
}

Side Effects:

  • Updates Reminder: Adds userId to read_by array
  • Triggers Change Streams: If MongoDB change streams enabled

Performance Considerations:

  • Index Requirements:

    { _id: 1, account: 1, read_by: 1 }
  • Expected Time: 10-30ms (single document operation)

  • Write Concern: Uses default MongoDB write concern


๐Ÿ”€ Integration Pointsโ€‹

CRM Module Integrationโ€‹

Creating Reminders:

// When deal task created
const reminder = await CRMReminder.create({
account: accountId,
assigned: userId,
created_by: creatorId,
type: 'deal',
reminder_type: 'task',
headline: 'Follow up on proposal',
content: 'Call client to discuss proposal details and answer questions',
priority: 'high',
pinned: false,
reminder_status: false,
due_date_time: new Date('2024-10-15T14:00:00Z'),
deal: dealId,
});

Completing Reminders:

// Mark reminder as complete
await CRMReminder.findByIdAndUpdate(reminderId, {
reminder_status: true,
updatedAt: new Date(),
});
// Changes category from upcoming/past_due to completed

Frontend Integrationโ€‹

Reminder Dashboard Component:

// Load reminders by category
const loadReminders = async (category = 'upcoming', page = 1) => {
const skip = (page - 1) * 25;

const response = await fetch(
`/v1/notifications-center/reminder?type=${category}&page=${skip}&limit=25`,
{ headers: { Authorization: `Bearer ${token}` } },
);

const { data, counts } = await response.json();

return {
reminders: data,
counts: counts[0], // { past_due: 5, upcoming: 12, completed: 23, total: 40 }
total: counts[0]?.total || 0,
};
};

// Category tabs with counts
const CategoryTabs = ({ counts }) => (
<Tabs>
<Tab label={`Upcoming (${counts.upcoming})`} value="upcoming" />
<Tab label={`Past Due (${counts.past_due})`} value="past_due" />
<Tab label={`Completed (${counts.completed})`} value="completed" />
</Tabs>
);

// Click reminder to navigate
const handleReminderClick = async reminder => {
// Mark as read
try {
await fetch(`/v1/notifications-center/reminder/read/${reminder.id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}` },
});
} catch (error) {
// Already read, ignore
}

// Navigate to deal or contact
window.location.href = reminder.click_action;
};

// Complete reminder
const handleCompleteReminder = async reminderId => {
await fetch(`/v1/crm/reminders/${reminderId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ reminder_status: true }),
});

// Refresh list
await loadReminders();
};

๐Ÿงช Edge Cases & Special Handlingโ€‹

Category Transitionโ€‹

Reminder Becomes Past Due:

// Created as upcoming
const reminder = await CRMReminder.create({
assigned: userId,
due_date_time: new Date('2024-10-15T14:00:00Z'), // Future date
reminder_status: false,
});

// Time passes, now past due
const now = new Date('2024-10-16T10:00:00Z');

// Automatically appears in past_due category
const pastDue = await getReminder({
skip: 0,
limit: 25,
type: 'past_due',
accountId,
userId,
});
// Includes reminder (due_date < now and reminder_status = false)

Pinned Remindersโ€‹

Sort Priority:

// Create pinned and unpinned reminders
await CRMReminder.create([
{ assigned: userId, due_date_time: new Date('2024-10-20'), pinned: true },
{ assigned: userId, due_date_time: new Date('2024-10-15'), pinned: false },
{ assigned: userId, due_date_time: new Date('2024-10-25'), pinned: true },
]);

// Get reminders (sorted by pinned DESC, then due_date DESC)
const result = await getReminder({ skip: 0, limit: 25, accountId, userId });
// Order: Oct 25 (pinned), Oct 20 (pinned), Oct 15 (unpinned)

Deal or Contact Not Found:

// Reminder references deleted deal
const reminder = await CRMReminder.create({
type: 'deal',
deal: deletedDealId, // Deal was deleted
// ... other fields
});

// Lookup returns empty array
const result = await getReminder({ skip: 0, limit: 25, accountId, userId });
// reminder.deal === null (arrayElemAt returns null for empty array)
// reminder.click_action === domainName (fallback)

Already Readโ€‹

Idempotent Read Operation:

// First read succeeds
const reminder1 = await readReminder({ notificationId, accountId, userId });
// reminder1.read_by includes userId

// Second read fails
await readReminder({ notificationId, accountId, userId });
// Throws: notFound('Reminder not found')

Contact Type URL Generationโ€‹

Person vs Business:

// Person contact
const personReminder = {
type: 'contact',
contact: { id: '...', type: 'person' },
};
// click_action: /contacts?type=people&id=...&tab=activity

// Business contact
const businessReminder = {
type: 'contact',
contact: { id: '...', type: 'business' },
};
// click_action: /contacts?type=businesses&id=...&tab=activity

Empty Categoryโ€‹

No Past Due Reminders:

const result = await getReminder({
skip: 0,
limit: 25,
type: 'past_due',
accountId,
userId,
});
// {
// counts: [{ past_due: 0, upcoming: 12, completed: 5, total: 17 }],
// data: [] // Empty array
// }

โš ๏ธ Important Notesโ€‹

  1. Deep Lookups: Service performs 3-4 level $lookup operations which are expensive. Consider denormalizing frequently accessed fields (deal_name, contact_name) for production optimization.

  2. Category Calculation: Categories calculated dynamically based on current date and reminder_status. Reminders automatically transition from upcoming to past_due without updates.

  3. Pinned Priority: Pinned reminders always appear first regardless of due date. Sort order: pinned DESC, then due_date_time DESC.

  4. Category Counts: Always returned regardless of selected filter. Used for category tab badges showing counts for all categories.

  5. Click Action URLs: Generated in database using $concat and $switch. Ensures consistent URL format across all clients.

  6. Assignment Filtering: getReminder only returns reminders where assigned equals userId. Users only see their own reminders.

  7. Read Status: Calculated per-user from read_by array. Multiple users can be in read_by for shared scenarios (though reminders typically have single assignee).

  8. Full Document Return: readReminder returns complete updated reminder document, unlike FCM which returns \{success: true\}. Allows frontend to update cache without additional fetch.

  9. No Assignment Check in Read: readReminder doesn't verify user is assigned to reminder. Security consideration - any user can mark any reminder as read if they know the ID.

  10. Domain Resolution: getActiveDomain called once per request to build click_action URLs. Consider caching domain lookups for high-traffic scenarios.

  • Common Notifications Service - Unified count and bulk read operations
  • FCM Service - FCM push notification management
  • CRM Reminders Collection (link removed - file does not exist) - Reminder schema and indexes
  • CRM Deals Collection (link removed - file does not exist) - Deal data structure
  • CRM Contacts Collection (link removed - file does not exist) - Contact, business, person schemas
  • CRM Reminders Management - Create, update, delete reminders
  • Domain Service (link removed - file does not exist) - Active domain resolution
๐Ÿ’ฌ

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