Skip to main content

πŸ”„ 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:

  1. Cron Initialization: queue-manager/crons/sites/update_business.js
  2. Service Processing: queue-manager/services/sites/update_business.js
  3. 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=true flag
  • Mark sites as site_update_in_progress=true before 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 document
  • account_id: Fallback for contact lookup
  • builder_id: Duda site identifier
  • business_details_updated: Boolean flag indicating contact changed
  • site_update_in_progress: Boolean flag preventing duplicate processing
  • refresh_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 string
  • type: '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 updateMany for 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:

  1. Strip Non-Digits: data.phone.replace(/\D/g, '') β†’ '1234567890'
  2. Add Plus Prefix: +1234567890
  3. Parse with Library: parsePhoneNumber() β†’ PhoneNumber object
  4. Format Internationally: formatInternational() β†’ '+1 234 567 890'
  5. 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: 5 in service layer
  • failedCb checks attemptsMade >= 10 (likely a bug or legacy code)
  • Effectively, job retries 5 times before final failure

Final Failure Cleanup:

  • Clears site_update_in_progress flag
  • Leaves business_details_updated=true for 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=true flag 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 configs collection (referenced by process.env.DUDA_TOKEN)
  • Contact Data: Requires valid contact linked to site
  • Site Builder ID: Must have valid builder_id for Duda API

Jobs That Depend On This​

  • Site Refresh: refresh_at=0 triggers 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_progress and business_details_updated flags
  • ⚠️ Refresh Trigger: Sets refresh_at=0 to trigger site refresh logic
  • ⚠️ No Site Publish: User must manually publish changes in Duda editor

Performance Considerations​

  • Batch Flagging: Uses updateMany for efficient flag management
  • Concurrent Job Addition: Uses Promise.all to 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 configs collection
  • Phone Parsing: Depends on libphonenumber-js library for accuracy
  • Social Platform Support: Limited to 7 platforms (Facebook, LinkedIn, Twitter, Instagram, YouTube, Yelp, Pinterest)
  • Manual Failure Recovery: Monitor business_details_updated=true sites with no recent refresh_at update

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

πŸ’¬

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