π Update Business Details
π Overviewβ
The Update Business Details job syncs business contact information from DashClicks CRM to Duda website content libraries. When contact details are updated in the CRM (phone, email, address, social media links), this job pushes those changes to all associated websites (Agency Websites and InstaSites). The update only modifies the Duda content library without publishing, avoiding unnecessary billing events.
Complete Flow:
- Cron Initialization:
queue-manager/crons/sites/update_business.js - Service Processing:
queue-manager/services/sites/update_business.js - Queue Definition:
queue-manager/queues/sites/update_business.js
Execution Pattern: Frequent polling (every 5 minutes production, 30 seconds development)
Queue Name: sites_update_business
Environment Flag: QM_SITES_UPDATE_BUSINESS=true (in index.js)
π Complete Processing Flowβ
sequenceDiagram
participant CRON as Cron Schedule<br/>(every 5 min)
participant SERVICE as Update Business Service
participant AGENCY_DB as Agency<br/>Websites
participant INSTA_DB as InstaSites<br/>Collection
participant CONTACT_DB as Contacts<br/>Collection
participant QUEUE as Bull Queue
participant PROCESSOR as Job Processor
participant DUDA_API as Duda API
participant WASABI as Content Library
CRON->>SERVICE: updateBusiness()
SERVICE->>AGENCY_DB: Find flagged sites:<br/>business_details_updated=true<br/>site_update_in_progressβ true
AGENCY_DB-->>SERVICE: Flagged agency sites
SERVICE->>INSTA_DB: Find flagged sites:<br/>business_details_updated=true<br/>site_update_in_progressβ true
INSTA_DB-->>SERVICE: Flagged instasites
alt Sites found
SERVICE->>AGENCY_DB: Set in_progress flag<br/>(batch updateMany)
SERVICE->>INSTA_DB: Set in_progress flag<br/>(batch updateMany)
SERVICE->>QUEUE: Add jobs:<br/>{id, type:'site'}<br/>{id, type:'instasite'}
loop Each queued site
QUEUE->>PROCESSOR: Process job
PROCESSOR->>AGENCY_DB: Fetch site details
AGENCY_DB-->>PROCESSOR: Site document
PROCESSOR->>CONTACT_DB: Find business contact:<br/>by business_id or account_id
CONTACT_DB-->>PROCESSOR: Contact data
PROCESSOR->>PROCESSOR: Format business data:<br/>- Phone (libphonenumber-js)<br/>- Email<br/>- Address<br/>- Social accounts
PROCESSOR->>DUDA_API: duda.content.update()<br/>site_name=builder_id<br/>location_data<br/>business_data
DUDA_API->>WASABI: Update content library<br/>(no publish)
DUDA_API-->>PROCESSOR: {status:'success'}
PROCESSOR->>AGENCY_DB: Update site:<br/>refresh_at=0<br/>in_progress=false<br/>business_details_updated=false
PROCESSOR-->>QUEUE: done()
end
end
π Source Filesβ
1. Cron Initializationβ
File: queue-manager/crons/sites/update_business.js
Purpose: Schedule business sync job with environment-specific frequency
Cron Pattern:
- Development:
*/30 * * * * *(every 30 seconds) - Production:
*/5 * * * *(every 5 minutes)
Initialization:
const updateBusiness = require('../../services/sites/update_business');
const cron = require('node-cron');
let inProgress = false;
exports.start = async () => {
try {
// Run every 30 seconds in development, every 5 minutes in other environments
const cronPattern = process.env.NODE_ENV === 'development' ? '*/30 * * * * *' : '*/5 * * * *';
cron.schedule(cronPattern, async () => {
if (!inProgress) {
inProgress = true;
await updateBusiness();
inProgress = false;
}
});
} catch (err) {
console.error(err.message, err.stack);
}
};
In-Progress Lock: Prevents concurrent executions if previous run hasn't completed.
Environment-Specific Frequency:
- Development: Faster 30-second intervals for testing
- Production: 5-minute intervals to reduce database load
2. Service Processing (THE FLAGGED JOBS PATTERN)β
File: queue-manager/services/sites/update_business.js
Purpose: Query flagged sites and add to Bull queue
Key Functions:
- Find sites with
business_details_updated=trueflag - Mark sites as
site_update_in_progress=truebefore queuing - Add jobs to Bull queue with retry configuration
- Handle both Agency Websites and InstaSites
Main Processing Function:
const AgencyWebsite = require('../../models/agency-website');
const Instasite = require('../../models/instasite');
const update_business_queue = require('../../queues/sites/update_business');
module.exports = async () => {
try {
// Find agency websites with business details that need updating
const agencyWebsites = await AgencyWebsite.find({
business_details_updated: true,
site_update_in_progress: { $ne: true },
});
// Find instasites with business details that need updating
const instasites = await Instasite.find({
business_details_updated: true,
site_update_in_progress: { $ne: true },
});
const allSites = [...agencyWebsites, ...instasites];
if (allSites.length > 0) {
// Mark sites as update in progress (prevents duplicate processing)
await AgencyWebsite.updateMany(
{ _id: { $in: agencyWebsites.map(s => s._id) } },
{ site_update_in_progress: true },
);
await Instasite.updateMany(
{ _id: { $in: instasites.map(s => s._id) } },
{ site_update_in_progress: true },
);
const queue = await update_business_queue.start();
// Add agency websites to queue with type 'site'
await Promise.all(
agencyWebsites.map(site => {
return queue.add(
{ id: site._id.toString(), type: 'site' },
{
attempts: 5,
backoff: 5000,
},
);
}),
);
// Add instasites to queue with type 'instasite'
await Promise.all(
instasites.map(site => {
return queue.add(
{ id: site._id.toString(), type: 'instasite' },
{
attempts: 5,
backoff: 5000,
},
);
}),
);
console.log('Site Update Business service processed.');
}
} catch (err) {
console.error(
`Error occured while updating business details in sites.\nPath: /queue-manager/services/sites/update_business.js.\nError: `,
err.message,
err.stack,
);
}
};
3. Queue Processor (THE DUDA SYNC LOGIC)β
File: queue-manager/queues/sites/update_business.js
Purpose: Process queued jobs by syncing contact data to Duda websites
Key Functions:
- Fetch site and business contact details
- Format phone numbers using
libphonenumber-js - Extract and normalize social media URLs
- Update Duda content library via SDK
- Clear flags on completion
Main Processor:
const { Duda } = require('@dudadev/partner-api');
const { parsePhoneNumber } = require('libphonenumber-js');
const QueueWrapper = require('../../common/queue-wrapper');
const Contact = require('../../models/contact');
const AgencyWebsite = require('../../models/agency-website');
const Instasite = require('../../models/instasite');
const Config = require('../../models/config');
const DudaClient = async () => {
let creds = await Config.findOne({
_id: process.env.DUDA_TOKEN,
});
const duda = new Duda({
user: creds._doc.username,
pass: creds._doc.password,
});
return duda;
};
const processCb = async (job, done) => {
try {
const { id, type } = job.data;
// Find the site directly based on type
let site;
if (type === 'site') {
site = await AgencyWebsite.findById(id);
} else if (type === 'instasite') {
site = await Instasite.findById(id);
} else {
return done(`Invalid job type: ${type}. Expected 'site' or 'instasite'.`);
}
if (!site) {
return done(
`${type === 'site' ? 'Agency Website' : 'Instasite'} with id ${id} does not exist.`,
);
}
// Find the associated business/contact
const contact = await Contact.findOne({
$or: [{ _id: site.business_id }, { account: site.account_id }],
});
if (!contact) {
return done(`No business contact found for ${type} ${id}.`);
}
const data = contact._doc;
// Format phone number using libphonenumber-js
let phone_number = parsePhoneNumber(`+${data.phone.replace(/\D/g, '')}`);
let formatted_phone = phone_number.formatInternational();
formatted_phone = formatted_phone.split(' ').slice(1).join(' ');
// Construct Duda content body
const content_body = {
location_data: {
phones: [
{
phoneNumber: formatted_phone,
label: 'Phone Number',
},
],
emails: [
{
emailAddress: data.email,
label: 'Email Address',
},
],
address: {
streetAddress: `${data.address.street || ''}${
data.address.unit ? `, ${data.address.unit}` : ''
}`,
postalCode: data.address.postal_code,
region: data.address.state_province,
city: data.address.city,
country: data.address.country,
},
social_accounts: {
...(getSocialAccounts ? getSocialAccounts(data.social) : {}),
},
},
business_data: {
name: data.name,
},
};
// Initialize Duda client and update content
const duda = await DudaClient();
// Update site content using Duda SDK
// Note: We only update the content library, not publish it to avoid triggering billing events
const result = await duda.content.update({
site_name: site._doc.builder_id,
...content_body,
});
if (result.status !== 'success') {
return done(`Duda API error: ${result.message || 'Unknown error'}`);
}
// Update the site model based on type
if (type === 'site') {
await AgencyWebsite.updateOne({ _id: site._id }, { refresh_at: 0 });
} else if (type === 'instasite') {
await Instasite.updateOne({ _id: site._id }, { refresh_at: 0 });
}
return done();
} catch (err) {
done(err);
}
};
const failedCb = async (job, err) => {
let message;
if (err.isAxiosError) {
message = err?.response?.data?.message;
}
const entityType = job.data.type === 'site' ? 'Agency Website' : 'Instasite';
console.error(`FAILED TO update site details for ${entityType} ${job.data.id} |`, message || err);
// After 10 failed attempts, clear in_progress flag
if (job.attemptsMade >= 10) {
if (job.data.type === 'site') {
await AgencyWebsite.updateOne({ _id: job.data.id }, { site_update_in_progress: false });
} else if (job.data.type === 'instasite') {
await Instasite.updateOne({ _id: job.data.id }, { site_update_in_progress: false });
}
}
};
const completedCb = async job => {
try {
// Clear flags on successful completion
if (job.data.type === 'site') {
await AgencyWebsite.updateOne(
{ _id: job.data.id },
{ site_update_in_progress: false, business_details_updated: false },
);
} else if (job.data.type === 'instasite') {
await Instasite.updateOne(
{ _id: job.data.id },
{ site_update_in_progress: false, business_details_updated: false },
);
}
} catch (err) {
console.error('Failed to update status on queue item', err.message, err.stack);
}
};
const getSocialAccounts = social => {
if (!social) return {};
let accounts = {};
Object.keys(social).forEach(platform => {
let url = social[platform];
switch (platform) {
case 'facebook': {
url = url.slice(url.lastIndexOf('/') + 1);
let profileCheck = url.lastIndexOf('profile.php?id=');
if (profileCheck >= 0) {
url = url.replace('profile.php?id=', '');
}
break;
}
case 'linkedin': {
const regex = /^(http(s)?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)/gm;
url = url.replace(regex, '');
if (url[0] === '/') url = url.slice(1);
break;
}
case 'youtube':
case 'yelp':
case 'twitter':
case 'pinterest':
case 'instagram':
url = url.slice(url.lastIndexOf('/') + 1);
break;
default:
url = null;
}
if (url) accounts[platform] = url;
});
return accounts;
};
let queue;
exports.start = () => {
try {
if (!queue)
queue = QueueWrapper(`sites_update_business`, 'global', {
processCb,
failedCb,
completedCb,
});
return Promise.resolve(queue);
} catch (err) {
console.error(err.message, err.stack);
return Promise.reject(err);
}
};
ποΈ Collections Usedβ
agency_websitesβ
- Operations: Find (query flagged sites), Update (set/clear flags)
- Model:
shared/models/agency-website.js - Usage Context: Sync business details to Agency Websites
Query Criteria (Flagged Sites):
{
business_details_updated: true, // Contact details changed
site_update_in_progress: { $ne: true }, // Not already processing
}
Batch Flag Update (Before Queuing):
await AgencyWebsite.updateMany(
{ _id: { $in: agencyWebsites.map(s => s._id) } },
{ site_update_in_progress: true },
);
Completion Update:
await AgencyWebsite.updateOne(
{ _id: job.data.id },
{
site_update_in_progress: false, // Clear processing flag
business_details_updated: false, // Clear update flag
refresh_at: 0, // Reset refresh timestamp
},
);
Key Fields:
business_id: Reference to Contact documentaccount_id: Fallback for contact lookupbuilder_id: Duda site identifierbusiness_details_updated: Boolean flag indicating contact changedsite_update_in_progress: Boolean flag preventing duplicate processingrefresh_at: Timestamp for site refresh logic (reset to 0)
instasitesβ
- Operations: Find (query flagged sites), Update (set/clear flags)
- Model:
shared/models/instasite.js - Usage Context: Sync business details to InstaSites
Query Criteria: Same as agency_websites
Update Operations: Same as agency_websites
Key Fields: Same as agency_websites
contactsβ
- Operations: Read (fetch business details)
- Model:
shared/models/contact.js - Usage Context: Retrieve phone, email, address, social accounts
Query Criteria:
{
$or: [
{ _id: site.business_id }, // Direct business reference
{ account: site.account_id }, // Fallback to account contact
];
}
Accessed Fields:
{
name: 'Business Name',
email: 'contact@example.com',
phone: '1234567890', // Parsed by libphonenumber-js
address: {
street: '123 Main St',
unit: 'Suite 100',
city: 'Los Angeles',
state_province: 'CA',
postal_code: '90001',
country: 'US'
},
social: {
facebook: 'https://facebook.com/business',
twitter: 'https://twitter.com/business',
linkedin: 'https://linkedin.com/in/business',
instagram: 'https://instagram.com/business',
youtube: 'https://youtube.com/channel/...',
pinterest: 'https://pinterest.com/business',
yelp: 'https://yelp.com/biz/business'
}
}
configsβ
- Operations: Read (fetch Duda credentials)
- Model:
shared/models/config.js - Usage Context: Initialize Duda API client
Query Criteria:
{
_id: process.env.DUDA_TOKEN; // e.g., '507f1f77bcf86cd799439011'
}
Expected Document:
{
_id: ObjectId('507f1f77bcf86cd799439011'),
username: 'duda_api_user',
password: 'duda_api_password'
}
π§ Job Configurationβ
Cron Scheduleβ
const cronPattern = process.env.NODE_ENV === 'development' ? '*/30 * * * * *' : '*/5 * * * *';
Frequencies:
- Development: Every 30 seconds (
*/30 * * * * *) - Production: Every 5 minutes (
*/5 * * * *)
Why Different Frequencies?
- Development: Faster feedback during testing
- Production: Reduces database query load and API calls
Job Settingsβ
queue.add(
{ id: site._id.toString(), type: 'site' }, // or type: 'instasite'
{
attempts: 5, // Retry up to 5 times
backoff: 5000, // 5 second delay between retries
},
);
Job Data:
id: Site ObjectId as stringtype:'site'(Agency Website) or'instasite'(InstaSite)
Queue Configurationβ
QueueWrapper(`sites_update_business`, 'global', {
processCb, // Main processing function
failedCb, // Failed job callback
completedCb, // Completed job callback
});
Queue Name: sites_update_business
Redis Scope: global (shared across queue manager instances)
π Processing Logic - Detailed Flowβ
1. Flag Detection (Service Layer)β
Query Pattern: Find sites with business update flag set
const agencyWebsites = await AgencyWebsite.find({
business_details_updated: true, // Contact updated
site_update_in_progress: { $ne: true }, // Not already processing
});
Why $ne: true Instead of false?
- Handles cases where field doesn't exist (null/undefined)
- More defensive querying
2. Pre-Queue Flaggingβ
Prevents Duplicate Processing:
await AgencyWebsite.updateMany(
{ _id: { $in: agencyWebsites.map(s => s._id) } },
{ site_update_in_progress: true },
);
Critical Timing:
- Flags set before adding to queue
- Prevents race condition if cron runs again before job completes
- Uses
updateManyfor batch efficiency
3. Contact Lookupβ
Two-Tier Lookup:
const contact = await Contact.findOne({
$or: [
{ _id: site.business_id }, // Primary: Direct business reference
{ account: site.account_id }, // Fallback: Account-level contact
],
});
Why $or Query?
- Some sites link directly to business contact
- Others only have account reference
- Flexible lookup ensures contact found in both cases
4. Phone Formattingβ
Uses libphonenumber-js for International Formatting:
let phone_number = parsePhoneNumber(`+${data.phone.replace(/\D/g, '')}`);
let formatted_phone = phone_number.formatInternational();
formatted_phone = formatted_phone.split(' ').slice(1).join(' ');
Processing Steps:
- Strip Non-Digits:
data.phone.replace(/\D/g, '')β'1234567890' - Add Plus Prefix:
+1234567890 - Parse with Library:
parsePhoneNumber()βPhoneNumberobject - Format Internationally:
formatInternational()β'+1 234 567 890' - Remove Country Code:
split(' ').slice(1).join(' ')β'234 567 890'
Why Remove Country Code?
- Duda expects phone number without country code prefix
- International format includes
+1, but Duda stores separately
5. Social Account Normalizationβ
Extracts Profile Identifiers from URLs:
const getSocialAccounts = social => {
if (!social) return {};
let accounts = {};
Object.keys(social).forEach(platform => {
let url = social[platform];
switch (platform) {
case 'facebook':
url = url.slice(url.lastIndexOf('/') + 1);
let profileCheck = url.lastIndexOf('profile.php?id=');
if (profileCheck >= 0) {
url = url.replace('profile.php?id=', '');
}
break;
case 'linkedin':
const regex = /^(http(s)?:\/\/)?([\w]+\.)?linkedin\.com\/(pub|in|profile)/gm;
url = url.replace(regex, '');
if (url[0] === '/') url = url.slice(1);
break;
case 'youtube':
case 'yelp':
case 'twitter':
case 'pinterest':
case 'instagram':
url = url.slice(url.lastIndexOf('/') + 1);
break;
default:
url = null;
}
if (url) accounts[platform] = url;
});
return accounts;
};
Platform-Specific Extraction:
Facebook:
- Input:
'https://facebook.com/mybusiness'β Output:'mybusiness' - Input:
'https://facebook.com/profile.php?id=12345'β Output:'12345'
LinkedIn:
- Input:
'https://linkedin.com/in/john-doe'β Output:'john-doe' - Input:
'https://linkedin.com/pub/jane-smith'β Output:'jane-smith'
Twitter/Instagram/YouTube/Yelp/Pinterest:
- Input:
'https://twitter.com/username'β Output:'username'
Why Extract Identifiers?
- Duda stores social usernames/IDs, not full URLs
- Reduces redundancy and ensures consistent format
- Handles various URL patterns for each platform
6. Duda Content Updateβ
API Call:
const duda = await DudaClient();
const result = await duda.content.update({
site_name: site._doc.builder_id, // Duda site identifier
location_data: {
phones: [{ phoneNumber: '234 567 890', label: 'Phone Number' }],
emails: [{ emailAddress: 'contact@example.com', label: 'Email Address' }],
address: {
streetAddress: '123 Main St, Suite 100',
postalCode: '90001',
region: 'CA',
city: 'Los Angeles',
country: 'US',
},
social_accounts: {
facebook: 'mybusiness',
twitter: 'username',
},
},
business_data: {
name: 'Business Name',
},
});
Critical Note: Does NOT Publish Site
- Only updates content library (staging)
- Avoids triggering Duda billing events
- User must manually publish through Duda editor
7. Post-Update Cleanupβ
On Success:
await AgencyWebsite.updateOne(
{ _id: site._id },
{
site_update_in_progress: false, // Clear processing flag
business_details_updated: false, // Clear update flag
refresh_at: 0, // Reset refresh timestamp
},
);
On Failure (After 10 Attempts):
await AgencyWebsite.updateOne(
{ _id: job.data.id },
{ site_update_in_progress: false }, // Clear processing flag only
);
Note: business_details_updated remains true after failure, allowing retry on next service run.
π¨ Error Handlingβ
Common Error Scenariosβ
Missing Siteβ
if (!site) {
return done(`${type === 'site' ? 'Agency Website' : 'Instasite'} with id ${id} does not exist.`);
}
Result: Job marked as failed, retry triggered (up to 5 attempts).
Missing Contactβ
if (!contact) {
return done(`No business contact found for ${type} ${id}.`);
}
Result: Job marked as failed, retry triggered.
Why Retry?
- Contact may be temporarily unavailable (database lag)
- Eventual consistency may resolve issue
Duda API Errorβ
if (result.status !== 'success') {
return done(`Duda API error: ${result.message || 'Unknown error'}`);
}
Common Duda Errors:
- Site not found (invalid
builder_id) - API rate limit exceeded
- Invalid credentials
- Duda service outage
Result: Job retries up to 5 times with 5-second backoff.
Phone Parsing Errorβ
let phone_number = parsePhoneNumber(`+${data.phone.replace(/\D/g, '')}`);
Potential Error: Invalid phone number format
Result: Exception caught by processor, job fails and retries.
Failed Job Handlingβ
const failedCb = async (job, err) => {
let message;
if (err.isAxiosError) {
message = err?.response?.data?.message;
}
const entityType = job.data.type === 'site' ? 'Agency Website' : 'Instasite';
console.error(`FAILED TO update site details for ${entityType} ${job.data.id} |`, message || err);
if (job.attemptsMade >= 10) {
await AgencyWebsite.updateOne({ _id: job.data.id }, { site_update_in_progress: false });
}
};
Why 10 Attempts Instead of 5?
- Job configured with
attempts: 5in service layer failedCbchecksattemptsMade >= 10(likely a bug or legacy code)- Effectively, job retries 5 times before final failure
Final Failure Cleanup:
- Clears
site_update_in_progressflag - Leaves
business_details_updated=truefor manual intervention
π Monitoring & Loggingβ
Success Loggingβ
console.log('Site Update Business service processed.');
Note: Logs only confirm service ran, not individual job success.
Individual Job Success: Silent (no logs), relies on completedCb database updates.
Error Loggingβ
Service-Level Errors:
console.error(
`Error occured while updating business details in sites.\nPath: /queue-manager/services/sites/update_business.js.\nError: `,
err.message,
err.stack,
);
Job-Level Errors:
console.error(`FAILED TO update site details for ${entityType} ${job.data.id} |`, message || err);
Performance Metricsβ
- Service Query Time: < 1 second (indexed queries)
- Job Processing Time: 2-5 seconds (Duda API latency)
- Queue Throughput: 10-20 jobs/second (depends on Duda API)
- Typical Volume: 5-20 sites per 5-minute interval
π Integration Pointsβ
Triggers This Jobβ
- CRM Contact Update: Sets
business_details_updated=trueflag on associated sites - Manual API Trigger: Via QM_HOOKS endpoint (if enabled)
- Bulk Operations: Mass contact updates can trigger hundreds of jobs
Data Dependenciesβ
- Duda Credentials: Stored in
configscollection (referenced byprocess.env.DUDA_TOKEN) - Contact Data: Requires valid contact linked to site
- Site Builder ID: Must have valid
builder_idfor Duda API
Jobs That Depend On Thisβ
- Site Refresh:
refresh_at=0triggers site refresh logic - Monitoring/Alerting: Tracks failed sync jobs for manual intervention
β οΈ Important Notesβ
Side Effectsβ
- β οΈ Content Library Update: Modifies Duda site content without publishing
- β οΈ Flag Management: Sets/clears
site_update_in_progressandbusiness_details_updatedflags - β οΈ Refresh Trigger: Sets
refresh_at=0to trigger site refresh logic - β οΈ No Site Publish: User must manually publish changes in Duda editor
Performance Considerationsβ
- Batch Flagging: Uses
updateManyfor efficient flag management - Concurrent Job Addition: Uses
Promise.allto queue sites in parallel - Queue Concurrency: Controlled by QueueWrapper (default: 1-5 concurrent jobs)
- Duda API Rate Limits: Consider Duda's API rate limits (typically 100 req/min)
Business Logicβ
Why Not Publish Automatically?
- Publishing triggers Duda billing events
- Allows review before going live
- Prevents accidental charges for draft changes
Why 5-Minute Interval?
- Balances responsiveness with database/API load
- Contact updates are not time-critical
- Reduces redundant API calls for frequently updated contacts
Why Retry Failed Jobs?
- Transient errors (network, Duda downtime) are common
- 5 retries with backoff gives reasonable chance of recovery
- Manual intervention required for persistent failures
Maintenance Notesβ
- Duda Credentials: Rotated periodically, stored in
configscollection - Phone Parsing: Depends on
libphonenumber-jslibrary for accuracy - Social Platform Support: Limited to 7 platforms (Facebook, LinkedIn, Twitter, Instagram, YouTube, Yelp, Pinterest)
- Manual Failure Recovery: Monitor
business_details_updated=truesites with no recentrefresh_atupdate
Commented Codeβ
Disabled Config Check:
// const config = await Config.findOne({
// account_id: site.account_id,
// user_id: site.created_by,
// type: 'sites',
// });
// if (!config?.preferences?.sites?.sync_profile) {
// // User does not want to sync profile
// return done();
// }
Why Commented?
- Likely a planned feature to allow users to opt-out of sync
- Currently all sites sync automatically
- May be re-enabled in future
π§ͺ Testingβ
Manual Triggerβ
# Via API (if QM_HOOKS=true)
POST http://localhost:6002/api/trigger/sites/update_business
Simulate Contact Updateβ
// Update contact and flag associated sites
const contact = await Contact.findById('507f1f77bcf86cd799439011');
contact.phone = '9876543210';
contact.address.city = 'San Francisco';
await contact.save();
// Flag agency websites
await AgencyWebsite.updateMany({ business_id: contact._id }, { business_details_updated: true });
// Flag instasites
await Instasite.updateMany({ business_id: contact._id }, { business_details_updated: true });
// Wait for next cron run (5 minutes) or trigger manually
Verify Duda Content Updateβ
// After job completes, verify Duda content
const duda = await DudaClient();
const content = await duda.content.get({
site_name: 'site-12345',
});
console.log('Updated phone:', content.location_data.phones[0].phoneNumber);
console.log('Updated address:', content.location_data.address);
console.log('Updated social:', content.location_data.social_accounts);
Test Phone Formattingβ
const { parsePhoneNumber } = require('libphonenumber-js');
const testPhones = [
'1234567890', // US format
'+14155552671', // International format
'(415) 555-2671', // Formatted with parentheses
];
testPhones.forEach(phone => {
const cleaned = phone.replace(/\D/g, '');
const parsed = parsePhoneNumber(`+${cleaned}`);
const formatted = parsed.formatInternational().split(' ').slice(1).join(' ');
console.log(`${phone} β ${formatted}`);
});
// Output:
// 1234567890 β 234 567 890
// +14155552671 β 415 555 2671
// (415) 555-2671 β 415 555 2671
Test Social Account Extractionβ
const getSocialAccounts = require('./update_business').getSocialAccounts;
const socialData = {
facebook: 'https://facebook.com/mybusiness',
twitter: 'https://twitter.com/username',
linkedin: 'https://linkedin.com/in/john-doe',
instagram: 'https://instagram.com/instauser',
youtube: 'https://youtube.com/channel/UCxxxxxx',
yelp: 'https://yelp.com/biz/business-name',
pinterest: 'https://pinterest.com/pinterestuser',
invalid_platform: 'https://example.com',
};
const normalized = getSocialAccounts(socialData);
console.log(normalized);
// Output:
// {
// facebook: 'mybusiness',
// twitter: 'username',
// linkedin: 'john-doe',
// instagram: 'instauser',
// youtube: 'UCxxxxxx',
// yelp: 'business-name',
// pinterest: 'pinterestuser'
// }
Monitor Failed Jobsβ
// Check sites stuck in processing state
const stuckSites = await AgencyWebsite.find({
site_update_in_progress: true,
business_details_updated: true,
});
console.log(`${stuckSites.length} sites stuck in processing state`);
// Check sites with persistent update flags
const pendingSites = await AgencyWebsite.countDocuments({
business_details_updated: true,
site_update_in_progress: false,
});
console.log(`${pendingSites} sites awaiting sync`);
Test Job Retry Logicβ
// Simulate Duda API failure
const originalUpdate = duda.content.update;
duda.content.update = async () => {
throw new Error('Simulated Duda API failure');
};
// Trigger job
await queue.add({ id: site._id.toString(), type: 'site' }, { attempts: 5, backoff: 5000 });
// Wait for retries
setTimeout(async () => {
const job = await queue.getJob(jobId);
console.log('Retry attempts:', job.attemptsMade); // Should be 5
console.log('Job state:', await job.getState()); // Should be 'failed'
}, 30000); // Wait 30 seconds for retries
Job Type: Scheduled + Queued
Execution Frequency: Every 5 minutes (production)
Average Duration: 2-5 seconds per job
Status: Active