Skip to main content

Contacts Trigger - CRM Synchronization

Overview

File: queue-manager/triggers/contacts.js
Function: syncAccountWithCRM()
Purpose: Real-time synchronization of contact data to accounts, orders, tasks, and sites

The contacts trigger ensures that when CRM contact information changes, all related business entities are immediately updated to maintain data consistency across the platform.

Trigger Configuration

Watched Collection

  • Collection: Contacts
  • Events: Insert, Replace, Update
  • Watched Fields:
    • name
    • email
    • phone
    • address (street, unit, city, state_province, postal_code, country)
    • social
    • website
    • image

Match Conditions

const matchConditions = {
$or: [
{ operationType: { $in: ['insert', 'replace'] } },
{
$and: [
{ operationType: 'update' },
{
$or: [
{ 'updateDescription.updatedFields.name': { $exists: true } },
{ 'updateDescription.updatedFields.email': { $exists: true } },
{ 'updateDescription.updatedFields.phone': { $exists: true } },
{ 'updateDescription.updatedFields.address': { $exists: true } },
{ 'updateDescription.updatedFields.social': { $exists: true } },
{ 'updateDescription.updatedFields.website': { $exists: true } },
{ 'updateDescription.updatedFields.image': { $exists: true } },
],
},
],
},
],
'fullDocument.account': { $exists: true },
};

Data Flow

sequenceDiagram
participant CRM as Internal API/CRM
participant CONTACT as Contacts Collection
participant TRIGGER as contacts.js Trigger
participant ACCOUNT as Accounts Collection
participant ORDER as Orders Collection
participant TASK as Projects Tasks
participant PULSE as Projects Pulses
participant USER as Users Collection
participant SITE as Agency Websites
participant INSTA as Instasites

CRM->>CONTACT: Update contact info
CONTACT->>TRIGGER: Change stream event

TRIGGER->>ACCOUNT: Update account fields

alt Account has orders
TRIGGER->>ORDER: Update seller metadata
TRIGGER->>ORDER: Update buyer metadata
end

alt Email changed
TRIGGER->>USER: Update owner email
TRIGGER->>TASK: Update order_metadata
end

alt Name changed
TRIGGER->>TASK: Update order_metadata
TRIGGER->>ORDER: Update metadata
end

TRIGGER->>SITE: Set business_details_updated
TRIGGER->>INSTA: Set business_details_updated

Processing Logic

Step 1: Build Update Payload

const updatePayload = {
name: fullDocument.name,
email: fullDocument.email,
phone: fullDocument.phone || '',
address: fullDocument.address || {},
social: fullDocument.social || {},
website: fullDocument.website || '',
logo: fullDocument.image,
};

Step 2: Update Account

Updates the associated account with all contact information:

const acc = await Account.findOneAndUpdate(
{ _id: fullDocument.account },
{ $set: updatePayload },
{ new: true },
)
.lean()
.exec();

Step 3: Update Orders (if has_orders)

If the account has orders, update order metadata:

const accountMetadata = {
id: acc._id,
name: acc.name,
email: acc.email,
phone: acc.phone || '',
image: acc.logo,
};

// Update seller orders
await Order.updateMany(
{ seller_account: acc._id },
{ $set: { 'metadata.seller': accountMetadata } },
);

// Update buyer orders
await Order.updateMany({ buyer_account: acc._id }, { $set: { 'metadata.buyer': accountMetadata } });

Step 4: Email Change Cascades

When email changes, additional updates occur:

if (updates.email) {
// Update owner user
let owner = await User.findOne({
account: fullDocument.account,
is_owner: true,
});

if (owner && owner.email != updates.email) {
await User.updateOne({ _id: owner._id }, { $set: { email: updates.email } });
}

// Update tasks metadata
await ProjectsTasks.updateMany(
{ 'order_metadata.seller.id': acc._id },
{ $set: { 'order_metadata.seller.email': updates.email } },
);

await ProjectsTasks.updateMany(
{ 'order_metadata.buyer.id': acc._id },
{ $set: { 'order_metadata.buyer.email': updates.email } },
);
}

Step 5: Name Change Cascades

When name changes, propagate to all related entities:

if (updates.name) {
// Update tasks
await ProjectsTasks.updateMany(
{ 'order_metadata.seller.id': acc._id },
{ $set: { 'order_metadata.seller.name': updates.name } },
);

// Update orders
const nameSet = { 'metadata.account_name': updates.name };
if (acc.main) {
nameSet['metadata.main_account_name'] = updates.name;
}

await Order.updateMany({ seller_account: acc._id }, { $set: nameSet });

await Order.updateMany({ buyer_account: acc._id }, { $set: nameSet });

// Update subscriptions
if (acc.main) {
// Main account: update main_account_name everywhere
await Subscription.updateMany(
{
$or: [{ account: acc._id }, { 'metadata.main_account_id': acc._id }],
},
{ $set: { 'metadata.main_account_name': updates.name } },
);

// Update account_name where this is the account_id
await Subscription.updateMany(
{ 'metadata.account_id': acc._id },
{ $set: { 'metadata.account_name': updates.name } },
);
} else {
// Sub account: update sub_account_name
await Subscription.updateMany(
{ 'metadata.sub_account_id': acc._id },
{ $set: { 'metadata.sub_account_name': updates.name } },
);
}
}

Step 6: Sync Sites

Flag agency websites and instasites for business detail updates:

await AgencyWebsite.updateMany(
{ account_id: fullDocument.account },
{ $set: { business_details_updated: true } },
);

await Instasite.updateMany(
{
business_id: acc.business,
status: { $nin: ['PENDING', 'FAILED', 'PURGED'] },
},
{ $set: { business_details_updated: true } },
);

Collections Updated

Primary Updates

  • Accounts - All contact fields synchronized

Conditional Updates (has_orders)

  • Orders - seller/buyer metadata
  • Subscriptions - account_name, main_account_name, sub_account_name

Field-Specific Updates

  • Users - Owner email (when email changes)
  • Projects Tasks - order_metadata.seller/buyer
  • Projects Pulses - order_metadata.seller/buyer
  • Agency Websites - business_details_updated flag
  • Instasites - business_details_updated flag

Use Cases

Use Case 1: Client Phone Number Update

Scenario: Sales rep updates client phone in CRM

Flow:

  1. Contact phone updated to "(555) 123-4567"
  2. Trigger fires immediately
  3. Account phone updated
  4. All orders with this account (seller/buyer) get updated metadata
  5. All tasks inherit updated order metadata
  6. Client sees new phone everywhere instantly

Use Case 2: Business Rebranding

Scenario: Client changes business name

Flow:

  1. Contact name changed from "ABC Corp" to "ABC Corporation"
  2. Trigger cascades update to:
    • Account name
    • Order metadata (seller/buyer)
    • Subscription metadata
    • Task order_metadata
  3. Agency websites and instasites flagged for regeneration
  4. Sites rebuild with new business name

Use Case 3: Email Migration

Scenario: Client migrates to new email domain

Flow:

  1. Contact email updated
  2. Account email updated
  3. Owner user email synchronized
  4. All orders reflect new email in metadata
  5. All tasks have updated email
  6. Email communications use new address

Error Handling

Account Not Found

if (!acc) {
logger.log({
initiator: 'queue-manager/triggers/contacts',
message: 'Account not found for update',
});
return;
}

Site Sync Errors

try {
await AgencyWebsite.updateMany(/* ... */);
await Instasite.updateMany(/* ... */);
} catch (error) {
logger.error({
initiator: 'queue-manager/triggers/contacts',
error,
message: 'Error updating sites',
});
}

Main Update Errors

try {
const acc = await Account.findOneAndUpdate(/* ... */);
// ... cascade updates ...
} catch (error) {
logger.error({
initiator: 'queue-manager/triggers/contacts',
error,
message: 'Error updating account with CRM data',
});
}

Performance Considerations

Optimizations

  1. Early Exit: Return immediately if account not found
  2. Conditional Updates: Only update orders if has_orders: true
  3. Lean Queries: All queries use .lean() for performance
  4. Parallel Updates: Multiple updateMany calls (not awaited sequentially)
  5. Specific Field Updates: Only update changed fields, not entire documents

Potential Bottlenecks

  1. High-Volume Updates: Multiple order updates per contact change
  2. Subscription Updates: Can affect many subscriptions for main accounts
  3. Site Regeneration: Agency websites/instasites may queue rebuilds

Main Account vs Sub-Account Logic

Main Account Name Change

if (acc.main) {
// Update main_account_name everywhere this account participates
await Subscription.updateMany(
{
$or: [{ account: acc._id }, { 'metadata.main_account_id': acc._id }],
},
{ $set: { 'metadata.main_account_name': updates.name } },
);
}

Sub-Account Name Change

else {
// Update sub_account_name where this is the sub account
await Subscription.updateMany(
{ 'metadata.sub_account_id': acc._id },
{ $set: { 'metadata.sub_account_name': updates.name } }
);
}

Downstream Triggers

  • order.js: Orders updated by this trigger may fire order triggers
  • users.js: User email updates may fire user triggers

Complementary Triggers

  • accounts.js: Handles account-specific changes (manager)
  • projects.js: Ensures tasks/pulses get metadata updates

Monitoring

Key Metrics

  • Trigger Frequency: How often contacts are updated
  • Cascade Volume: Number of collections updated per trigger
  • Error Rate: Failed updates to accounts/orders/tasks
  • Query Performance: Time spent in database operations

Logging

logger.log({
initiator: 'queue-manager/triggers/contacts',
message: 'Account not found for update',
});

logger.error({
initiator: 'queue-manager/triggers/contacts',
error,
message: 'Error updating account with CRM data',
});

Best Practices

1. Field-Specific Logic

Only execute expensive cascades when relevant fields change:

if (updates.email) {
// Email-specific cascades
}

if (updates.name) {
// Name-specific cascades
}

2. Avoid Unnecessary Updates

Check for actual changes before updating:

if (owner && owner.email != updates.email) {
await User.updateOne(/* ... */);
}

3. Handle Missing Data Gracefully

phone: fullDocument.phone || '',
address: fullDocument.address || {},

Summary

The contacts trigger is the primary synchronization mechanism for CRM data, ensuring that contact changes immediately propagate to accounts, orders, tasks, subscriptions, and sites. It maintains data consistency across 8+ collections with intelligent cascading logic for main/sub accounts and conditional updates based on field changes.

Key Characteristics:

  • ⚡ Immediate synchronization on contact changes
  • 🔄 Cascades to 8+ collections
  • 🎯 Field-specific update logic
  • 🛡️ Graceful error handling
  • 🏢 Main/sub-account awareness
💬

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