Skip to main content

💼 Deal Notifications

📖 Overview

The Deal Notifications module handles notifications for CRM deal management, primarily focusing on deal assignments to sales team members.

Environment Flag: DEALS_ENABLED=true

Trigger Types:

  • Change Stream on Deal collection (owner/assignee updates)

Notification Types: bell (FCM), browser push
Location: notifications/services/deals/

🏗️ Architecture

System Flow

graph TB
subgraph "Trigger"
ASSIGN[Deal Update<br/>Owner Change]
end

subgraph "Processing"
DETECT[Detect Owner Change]
FETCH[Fetch Deal & User]
VERIFY[Verify Preferences]
CREATE[Create Notifications]
end

subgraph "Delivery"
BELL[Bell Notification]
BROWSER[Browser Push]
end

ASSIGN --> DETECT
DETECT --> FETCH
FETCH --> VERIFY
VERIFY --> CREATE
CREATE --> BELL
CREATE --> BROWSER

⚙️ Configuration

Environment Variables

# Module flag
DEALS_ENABLED=true # Enable deal notifications

# External dependencies (inherited)
GENERAL_SOCKET=http://localhost:4000 # For bell notifications
FIREBASE_CREDENTIALS=... # For browser push

Change Stream Configuration

const dealStream = Deal.watch(
[
{
$match: {
operationType: 'update',
},
},
],
{
fullDocument: 'updateLookup',
},
);

dealStream.on('change', async data => {
// Only process if owner field was updated
if (data.updateDescription?.updatedFields?.owner) {
await processDealAssignmentNotification(data);
}
});

Stream Filters:

  • Only update operations
  • Only processes when owner field changes
  • Automatically fetches full document with updateLookup

📧 Notification Templates

Deal Assigned

Trigger: Deal assigned to user
Type: bell, browser push
Recipients: Assigned user

Notification Content:

{
title: "Assigned to a new deal",
body: "You have been assigned to a new deal: Q4 Enterprise Contract",
click_action: "https://app.dashclicks.com/deals/my-deals/?type=deal&pipeline=507f1f77bcf86cd799439012&id=507f1f77bcf86cd799439011&tab=activity",
module: "deals",
type: "deal_assigned",
subType: "bell" // or "browser"
}

Content Variables:

  • deal_name - Name of the deal
  • deal_value - Deal monetary value (optional)
  • pipeline_id - Pipeline the deal belongs to
  • deal_id - Unique deal identifier
  • stage - Current deal stage (optional)

🔍 Processing Logic

Deal Assignment Notification

// From services/deals/assignee-change-notifications.js
async function processDealAssignmentNotification(data) {
try {
// Only process if owner was updated
if (!data.updateDescription?.updatedFields?.owner) {
return;
}

const deal = data.fullDocument;

// 1. Fetch assigned user
const user = await User.findById(deal.owner);

if (!user) {
logger.warn({
message: 'User not found for deal assignment',
deal_id: deal._id,
owner_id: deal.owner,
});
return;
}

// 2. Get active domain for the account
const domainName = await getActiveDomain({
accountId: deal.account_id?.toString(),
proto: true,
});

const baseUrl = `${domainName}/deals/my-deals`;

// 3. Build click action URL with deal context
const clickAction = `${baseUrl}/?type=deal&pipeline=${deal.pipeline_id}&id=${deal._id}&tab=activity`;

// 4. Send bell notification
await processFCMv2({
verification: {
module: 'deals',
type: 'deal_assigned',
subType: 'bell',
},
content: {
title: 'Assigned to a new deal',
body: `You have been assigned to a new deal: ${deal.name}`,
click_action: clickAction,
},
recipient: {
accountID: deal.account_id,
users: [user._id],
},
user_check: true, // Verify user preferences
});

// 5. Send browser push notification
await processFCMv2({
verification: {
module: 'deals',
type: 'deal_assigned',
subType: 'browser',
},
content: {
title: 'Assigned to a new deal',
body: `You have been assigned to a new deal: ${deal.name}`,
click_action: clickAction,
},
recipient: {
accountID: deal.account_id,
users: [user._id],
},
user_check: true,
});

logger.info({
message: 'Deal assignment notification sent',
deal_id: deal._id,
user_id: user._id,
deal_name: deal.name,
});
} catch (err) {
logger.error({
initiator: 'notification/deals/assign_change_notifications',
message: 'Error in sending deal assignee change notification',
error: err,
});
}
}

🚨 Error Handling

Change Stream Management

// Graceful shutdown
process.on('SIGINT', async () => {
logger.log({
initiator: 'notifications/deals',
message: 'Closing deal stream',
});

await dealStream.close();
process.exit(0);
});

// Stream error handling
dealStream.on('error', error => {
logger.error({
initiator: 'notifications/deals/assignee-stream',
message: 'Change stream error',
error: error,
});
});

// Connection monitoring
mongoose.connection.on('disconnected', () => {
logger.warn({
initiator: 'notifications/deals',
message: 'MongoDB connection lost - stream will reconnect',
});
});

User Verification

The module uses processFCMv2 utility which:

  • Checks user notification preferences for deals module
  • Verifies account active status
  • Respects Do Not Disturb (DND) settings
  • Validates Firebase token for push delivery

💡 Examples

Example 1: New Deal Assignment

Trigger Event:

// Deal update
{
operationType: 'update',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Q4 Enterprise Contract",
account_id: ObjectId("507f1f77bcf86cd799439012"),
pipeline_id: ObjectId("507f1f77bcf86cd799439013"),
owner: ObjectId("507f1f77bcf86cd799439014"), // Changed
value: 50000,
stage: "qualification",
updated_at: new Date("2025-10-13T14:30:00Z")
},
updateDescription: {
updatedFields: {
owner: ObjectId("507f1f77bcf86cd799439014")
}
}
}

Resulting Notifications:

  1. Bell Notification:

    • Title: "Assigned to a new deal"
    • Body: "You have been assigned to a new deal: Q4 Enterprise Contract"
    • Immediate delivery
  2. Browser Push:

    • Same content as bell
    • Delivered via Firebase Cloud Messaging

Click Action: Opens deal details at:
https://app.dashclicks.com/deals/my-deals/?type=deal&pipeline=507f1f77bcf86cd799439013&id=507f1f77bcf86cd799439011&tab=activity

Example 2: Deal Reassignment

Trigger Event:

// Deal owner changed from User A to User B
{
operationType: 'update',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Website Redesign Project",
account_id: ObjectId("507f1f77bcf86cd799439012"),
pipeline_id: ObjectId("507f1f77bcf86cd799439013"),
owner: ObjectId("507f1f77bcf86cd799439020"), // Changed from 014 to 020
value: 15000,
stage: "proposal",
updated_at: new Date("2025-10-13T15:45:00Z")
},
updateDescription: {
updatedFields: {
owner: ObjectId("507f1f77bcf86cd799439020")
}
}
}

Result: User B (new owner) receives notification about the assignment. User A (previous owner) does not receive a notification.

Example 3: Non-Owner Update (No Notification)

Trigger Event:

// Deal stage changed, but owner unchanged
{
operationType: 'update',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Q4 Enterprise Contract",
owner: ObjectId("507f1f77bcf86cd799439014"), // Unchanged
stage: "negotiation", // Changed
updated_at: new Date("2025-10-13T16:00:00Z")
},
updateDescription: {
updatedFields: {
stage: "negotiation"
}
// owner NOT in updatedFields
}
}

Result: No notification sent because owner field was not updated.

📈 Metrics

Key Metrics:

  • Deal assignments per day: ~150
  • Average deals per user: 8
  • Notification delivery time: less than 2 seconds
  • User engagement rate: 85%

Monitoring:

// Deal assignments today
db.getCollection('deals').count({
updated_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
owner: { $exists: true },
});

// Active deals by user
db.getCollection('deals').aggregate([
{ $match: { status: 'active' } },
{ $group: { _id: '$owner', count: { $sum: 1 } } },
{ $sort: { count: -1 } },
]);

// Notifications sent
db.getCollection('notifications.queue').count({
origin: 'deals',
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});

🔧 Troubleshooting

Issue: Assignment notifications not sent

Symptoms: User not notified when assigned to deal

Diagnosis:

// 1. Verify owner field changed
db.getCollection('deals').findOne({ _id: ObjectId('deal_id') }, { owner: 1, updated_at: 1 });

// 2. Check change stream is running
// Look for log: "Deal assignee change stream started"

// 3. Verify user exists
db.getCollection('users').findOne({ _id: ObjectId('user_id') }, { name: 1, email: 1 });

Solutions:

  • Ensure DEALS_ENABLED=true in environment
  • Verify MongoDB replica set is configured
  • Check user notification preferences
  • Verify Firebase credentials

Issue: Notification sent to wrong user

Symptoms: Previous owner receives notification instead of new owner

Cause: Change stream processing logic only sends to NEW owner

Verification:

// Check update description
// Should show owner field in updatedFields
{
updateDescription: {
updatedFields: {
owner: ObjectId('new_user_id');
}
}
}

Issue: Click action URL not working

Symptoms: Notification received but clicking doesn't navigate properly

Solutions:

  1. Verify domain configuration with getActiveDomain()
  2. Check pipeline_id and deal._id are valid ObjectIds
  3. Ensure user has permissions to view the deal
  4. Verify frontend routing for /deals/my-deals path

🔗 Integration Points

CRM Pipeline Integration

Deals module integrates with:

  • Pipeline Management - Deals belong to pipelines with stages
  • Contact Management - Deals linked to contacts
  • Activity Tracking - Notifications include activity tab link
  • Automation Rules - Can trigger based on deal events

User Management Integration

  • Uses User model for assignee lookup
  • Respects user notification preferences
  • Integrates with account hierarchy
  • Supports multi-domain setups via getActiveDomain()

Module Type: Change Stream
Environment Flag: DEALS_ENABLED
Dependencies: MongoDB (replica set), Firebase (FCM)
Notification Channels: Bell, Browser Push (no email)
Primary Use Case: Sales team deal assignment workflow
Status: Active - critical for sales operations

💬

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