Skip to main content

⭐ Review Request Notifications

📖 Overview

The Review Request Notifications module provides an automated system for sending review requests to customers via email and SMS. It supports configurable delays, repeating reminders, and multiple review platforms (Google, Facebook, custom sites).

Environment Flag: AUTO_REVIEW_REQUEST_ENABLED=true
Trigger Type: Cron Job (every 30 seconds)
Notification Types: email, SMS
Location: notifications/services/reviews/reviews.auto.request.js

🏗️ Architecture

System Flow

graph TB
subgraph "Integration Layer"
STRIPE[Stripe Webhooks]
SQUARE[Square Webhooks]
SHOPIFY[Shopify Integration]
QB[QuickBooks Integration]
end

subgraph "Database"
RR[ReviewRequest<br/>Collection]
end

subgraph "Processing"
CRON[Cron Job<br/>Every 30s]
PROCESS[Process Review<br/>Requests]
end

subgraph "Configuration"
CONFIG[AutoReviewRequest<br/>Settings]
end

subgraph "Delivery"
QUEUE[NotificationQueue]
BULL[Bull Queue]
end

STRIPE --> RR
SQUARE --> RR
SHOPIFY --> RR
QB --> RR

RR --> CRON
CRON --> PROCESS
PROCESS --> CONFIG
CONFIG --> QUEUE
QUEUE --> BULL

⚙️ Configuration

Environment Variables

# Module flag
AUTO_REVIEW_REQUEST_ENABLED=true # Enable auto review requests

# External dependencies (inherited)
SENDGRID_API_KEY=your_key
TWILIO_ACCOUNT_SID=your_sid
TWILIO_AUTH_TOKEN=your_token

AutoReviewRequest Configuration

Each account can configure review request behavior:

// AutoReviewRequest document
{
account_id: ObjectId,

// Sending behavior
send_only_once: Boolean, // true = single request, false = with reminders

// Timing
delay: Number, // Days to delay initial request (e.g., 1 = send 1 day after purchase)
reminder: Number, // Days between reminders (e.g., 7 = weekly reminders)
retry: Number, // Number of reminders to send (e.g., 3 = 3 reminders)

// Templates
email: {
enabled: Boolean,
subject: String, // Custom email subject
title: String, // Email headline
message: String // Email body message
},
sms: {
enabled: Boolean,
message: String // Custom SMS text
},

// Review platforms
platforms: {
google: Boolean,
facebook: Boolean,
custom: Boolean
}
}

Example Configuration:

{
account_id: ObjectId("507f1f77bcf86cd799439011"),
send_only_once: false, // Enable reminders
delay: 1, // Send 1 day after purchase
reminder: 7, // Send reminder every 7 days
retry: 3, // Send 3 reminders
email: {
enabled: true,
subject: "How was your experience?",
title: "We'd love your feedback!",
message: "Thank you for choosing us. Please share your experience."
},
sms: {
enabled: true,
message: "Thanks for your purchase! Please leave us a review: {link}"
}
}

// Results in:
// Day 1: Initial request (email + SMS)
// Day 8: Reminder 1 (if no response)
// Day 15: Reminder 2 (if no response)
// Day 22: Reminder 3 (if no response)

🔄 Complete Flow

Step-by-Step Process

  1. Integration Creates Request:

    • Stripe/Square/Shopify webhook receives purchase event
    • Creates ReviewRequest document with customer info
    • Sets notification_processed: false
  2. Cron Job Detects Request:

    • Runs every 30 seconds
    • Queries for unprocessed review requests
    • Filters by source (stripe, squareUp, shopify, etc.)
  3. Configuration Lookup:

    • Fetches AutoReviewRequest for account
    • Determines if email, SMS, or both enabled
    • Gets custom templates if configured
  4. Data Preparation:

    • Fetches account business info (logo, branding, social links)
    • Calculates average rating and total reviews
    • Generates review landing page URL
    • Prepares template variables
  5. Notification Creation:

    • Creates email notification (if enabled)
    • Creates SMS notification (if enabled)
    • Includes review request ID for tracking
    • Sets check_credits: true (deducts credits)
  6. Queue Processing:

    • Queue processor adds to Bull with delay
    • If send_only_once: false, configures repeating job
    • Bull handles delivery and retries
  7. Completion:

    • Marks ReviewRequest as notification_processed: true
    • Prevents duplicate processing

📋 Cron Job Pattern

const autoReviewRequest = async () => {
// 1. Query unprocessed review requests
let JOB_SPECIFIC_DOCUMENTS = await ReviewRequest.find({
source: { $in: ['stripe', 'squareUp', 'shopify', 'quickbooks'] },
notification_processed: { $ne: true },
});

if (!JOB_SPECIFIC_DOCUMENTS.length) return;

for (let jobSpecificDocument of JOB_SPECIFIC_DOCUMENTS) {
const account_id = jobSpecificDocument.account_id;

// 2. Fetch account and business info
const account = await Account.findById(account_id).populate('parent_account');

const businessInfo = await fetchBusinessInfo(account_id);
const ownerInfo = await fetchOwnerInfo(account_id);

// 3. Fetch contacts (recipients)
const contactDocs = await Contact.find({
_id: { $in: jobSpecificDocument.receivers },
});

// 4. Build sender object
const sender = {
name: ownerInfo.name,
first_name: ownerInfo.first_name,
last_name: ownerInfo.last_name,
email: businessInfo.email,
account: businessInfo.name,
address: {
/* business address */
},
website: businessInfo.website,
logo: businessInfo.logo,
phone: businessInfo.phone,
social: businessInfo.social,
};

// 5. Fetch template configuration
const template = await Template.findOne({ account_id });

// 6. Prepare review data
const data = {
average_rating: await getAverageRating({ account_id }),
total_reviews: await getReviewsCount({ account_id }),
google_link: jobSpecificDocument.data.links?.google,
facebook_link: jobSpecificDocument.data.links?.facebook,
link: `${account.domain.custom || account.domain.dashclicks}/${account_id}/review`,
integration_name: 'Review',
primary_color: account.branding?.colors?.primary || '#B4d4d5',
headline: template?.email?.title || 'How was your experience?',
message: template?.email?.message || 'Please leave us a review!',
sender,
};

// 7. Send email (if enabled)
if (jobSpecificDocument.type === 'email' || jobSpecificDocument.type === 'both') {
const body = reviewRequestBody(data);

await processEmailv2({
verification: {
module: 'reviews',
type: 'requests',
subType: 'email',
},
recipient: {
accountID: account_id,
users: contactDocs,
},
content: {
sender_user: jobSpecificDocument.user_id,
subject: template?.email?.subject || 'Leave us a review',
body,
},
user_check: false,
check_credits: true,
additional_message_data: {
review_request_id: jobSpecificDocument._id,
},
});
}

// 8. Send SMS (if enabled)
if (jobSpecificDocument.type === 'sms' || jobSpecificDocument.type === 'both') {
const smsContent = template?.sms?.message || jobSpecificDocument.data.content;

await processSMSv2({
verification: {
module: 'reviews',
type: 'requests',
subType: 'sms',
},
recipient: {
accountID: account_id,
users: contactDocs,
},
data: {
content: smsContent,
},
sender_user: jobSpecificDocument.user_id,
user_check: false,
check_credits: true,
additional_message_data: {
review_request_id: jobSpecificDocument._id,
},
});
}

// 9. Mark as processed
await ReviewRequest.findByIdAndUpdate(jobSpecificDocument._id, {
notification_processed: true,
});

logger.log({
initiator: 'notifications/services/reviews/reviews.auto.request',
message: `Review request added to queue: ${jobSpecificDocument._id}`,
});
}
};

// Schedule execution
let inProgress = false;
exports.start = () => {
cron.schedule('*/30 * * * * *', async () => {
if (!inProgress) {
inProgress = true;
await autoReviewRequest();
inProgress = false;
}
});
};

📧 Notification Templates

Email Template

Type: email
Template: SendGrid dynamic template or HTML body
Recipients: Customers (contacts from purchase)

Content Structure:

{
sender: {
name: "John Doe",
first_name: "John",
last_name: "Doe",
email: "business@example.com",
account: "Business Name",
address: { street, unit, city, state, zip, country, line1, line2 },
website: "https://example.com",
logo: "https://cdn.example.com/logo.png",
phone: "(123) 456-7890",
social: {
facebook: "https://facebook.com/business",
linkedin: "https://linkedin.com/company/business",
youtube: "https://youtube.com/business",
twitter: "https://twitter.com/business",
instagram: "https://instagram.com/business"
}
},
recipient: {
name: "Customer Name",
first_name: "Customer",
last_name: "Name"
},
average_rating: 4.8,
total_reviews: 127,
google_link: "https://g.page/r/...",
facebook_link: "https://facebook.com/...",
link: "https://app.dashclicks.com/507f1f77bcf86cd799439011/review",
integration_name: "Review",
primary_color: "#B4d4d5",
headline: "How was your experience?",
message: "Thanks for choosing us! Would you take a moment and please leave us a review?",
subject: "Leave us a review"
}

HTML Body:

<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { text-align: center; margin-bottom: 30px; }
.logo { max-width: 200px; }
.headline { color: {{primary_color}}; font-size: 24px; margin: 20px 0; }
.message { font-size: 16px; line-height: 1.6; }
.rating { font-size: 48px; color: #FFD700; }
.buttons { text-align: center; margin: 30px 0; }
.button { display: inline-block; padding: 15px 30px; margin: 10px; background: {{primary_color}}; color: white; text-decoration: none; border-radius: 5px; }
.footer { text-align: center; font-size: 12px; color: #666; margin-top: 40px; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<img src="{{sender.logo}}" alt="{{sender.account}}" class="logo" />
</div>

<h1 class="headline">{{headline}}</h1>

<p class="message">{{message}}</p>

<div class="rating">⭐ {{average_rating}} ({{total_reviews}} reviews)</div>

<div class="buttons">
<a href="{{google_link}}" class="button">Review on Google</a>
<a href="{{facebook_link}}" class="button">Review on Facebook</a>
<a href="{{link}}" class="button">Leave a Review</a>
</div>

<div class="footer">
<p>{{sender.account}}</p>
<p>{{sender.address.line1}}<br />{{sender.address.line2}}</p>
<p>{{sender.phone}} | {{sender.email}}</p>
<p>{{sender.website}}</p>
</div>
</div>
</body>
</html>

SMS Template

Type: SMS
Recipients: Customers with phone numbers

Default Content:

Thanks for choosing {{sender.account}}! Please take a moment to leave us a review: {{link}}

Custom Content (if configured):

Hi {{recipient.first_name}}! We'd love your feedback. Leave us a review here: {{link}}

Character Limit: 160 characters (standard SMS) or 1600 (concatenated)

🔍 Data Sources

Models Used

Primary Models:

  • ReviewRequest - Pending review requests from integrations
  • AutoReviewRequest - Account-specific configuration
  • reviewTemplate - Custom email/SMS templates

Related Models:

  • Account - Business info, branding, domain
  • Contact - Customer information
  • User - Account owner info
  • Reviews - Existing reviews for average rating

Database Queries

Find Unprocessed Requests:

const pendingRequests = await ReviewRequest.find({
source: { $in: ['stripe', 'squareUp', 'shopify', 'quickbooks'] },
notification_processed: { $ne: true },
});

Fetch Configuration:

const config = await AutoReviewRequest.findOne(
{ account_id: account_id },
{ send_only_once: 1, delay: 1, reminder: 1, retry: 1 },
).lean();

Calculate Average Rating:

const getAverageRating = async ({ account_id }) => {
const reviews = await Reviews.find({ account_id });
if (!reviews.length) return 0;

const sum = reviews.reduce((acc, r) => acc + r.rating, 0);
return (sum / reviews.length).toFixed(1);
};

Count Total Reviews:

const getReviewsCount = async ({ account_id }) => {
return await Reviews.countDocuments({ account_id });
};

🚨 Error Handling

Common Errors

Error 1: Missing business information
Cause: Account doesn't have business contact configured
Resolution: Skip notification, log error, mark as processed

if (!businessData?.[0]?.businessInfo?.[0]) {
logger.error({
message: 'Missing business info for review request',
account_id,
review_request_id: jobSpecificDocument._id
});

await ReviewRequest.findByIdAndUpdate(jobSpecificDocument._id, {
notification_processed: true,
error: 'Missing business info'
});

continue;
}

Error 2: No recipients found
Cause: Contact IDs in receivers array don't exist
Resolution: Log error, mark as processed

if (!contactDocs.length) {
logger.error({
message: 'No recipients found for review request',
receiver_ids: jobSpecificDocument.receivers
});

await ReviewRequest.findByIdAndUpdate(jobSpecificDocument._id, {
notification_processed: true,
error: 'No valid recipients'
});

continue;
}

Error 3: Template rendering error
Cause: Invalid template variables or missing data
Resolution: Use default template, log warning

try {
const body = reviewRequestBody(data);
} catch (err) {
logger.warn({
message: 'Template rendering failed, using default',
error: err,
});

// Use simple default template
const body = `Please leave us a review: ${data.link}`;
}

Retry Logic

The module itself doesn't retry - it relies on the queue processor's retry logic:

  • Mark notification_processed: true immediately after queuing
  • If queue processor fails, it will retry delivery
  • No need to reprocess ReviewRequest document

💡 Examples

Example 1: Single Review Request

Configuration:

{
send_only_once: true, // No reminders
delay: 1, // Send 1 day after purchase
email: { enabled: true },
sms: { enabled: false }
}

ReviewRequest Document:

{
_id: ObjectId("507f1f77bcf86cd799439011"),
account_id: ObjectId("507f1f77bcf86cd799439012"),
source: "stripe",
type: "email",
receivers: [ObjectId("507f1f77bcf86cd799439013")],
data: {
content: "Please leave us a review!",
links: {
google: "https://g.page/r/...",
facebook: "https://facebook.com/..."
}
},
notification_processed: false,
created_at: new Date()
}

Resulting Notification:

  • Day 1: Email sent with review link
  • No reminders: Single request only

Example 2: Multiple Reminders

Configuration:

{
send_only_once: false, // Enable reminders
delay: 0, // Send immediately
reminder: 7, // Weekly reminders
retry: 3, // 3 reminders
email: { enabled: true },
sms: { enabled: true }
}

ReviewRequest Document:

{
_id: ObjectId("507f1f77bcf86cd799439011"),
account_id: ObjectId("507f1f77bcf86cd799439012"),
source: "squareUp",
type: "both", // Email + SMS
receivers: [ObjectId("507f1f77bcf86cd799439013")],
data: { /* ... */ },
notification_processed: false
}

Resulting Notifications:

  • Day 0: Email + SMS sent
  • Day 7: Reminder 1 (email + SMS)
  • Day 14: Reminder 2 (email + SMS)
  • Day 21: Reminder 3 (email + SMS)
  • Total: 4 notifications (initial + 3 reminders)

Queue Configuration:

{
jobId: "507f1f77bcf86cd799439011",
attempts: 10,
backoff: { type: 'exponential', delay: 10000 },
delay: 0, // Immediate
repeat: {
every: 7 * 24 * 60 * 60 * 1000, // 7 days
limit: 3 // 3 reminders
}
}

Example 3: Custom Template

Configuration:

{
send_only_once: false,
delay: 2,
reminder: 14,
retry: 2
}

Custom Template:

{
account_id: ObjectId("507f1f77bcf86cd799439012"),
email: {
subject: "We'd love your feedback! 🌟",
title: "How did we do?",
message: "Your opinion matters to us. Please take 2 minutes to share your experience with our service. We read every review!"
},
sms: {
message: "Hi {{recipient.first_name}}! Thanks for your business. Mind leaving us a quick review? {{link}}"
}
}

Resulting Email:

  • Subject: "We'd love your feedback! 🌟"
  • Headline: "How did we do?"
  • Message: Custom message about opinion mattering
  • Delay: 2 days after purchase
  • Reminders: 14 and 28 days later

🐛 Troubleshooting

Issue: Review Requests Not Sent

Symptoms: ReviewRequest documents created but no notifications

Check:

  1. Environment flag:

    echo $AUTO_REVIEW_REQUEST_ENABLED
    # Should output: true
  2. Cron job running:

    grep "AUTO REVIEW REQUEST SERVICE RUNNING" notifications.log
  3. ReviewRequest documents:

    db.getCollection('reviews.requests').find({
    notification_processed: { $ne: true },
    });
  4. AutoReviewRequest configuration:

    db.getCollection('reviews.auto_requests').findOne({
    account_id: ObjectId('account_id'),
    });

Issue: Reminders Not Repeating

Symptoms: Initial request sent but no reminders

Check Bull Repeatable Jobs:

// In Node.js console
const Queue = require('bull')('NotificationQueue', 'redis://...');
const repeatable = await Queue.getRepeatableJobs();

// Look for review request jobs
const reviewJobs = repeatable.filter(j => j.id.includes('review_request_id'));

Verify Configuration:

const config = await AutoReviewRequest.findOne({ account_id });

console.log({
send_only_once: config.send_only_once, // Should be false
reminder: config.reminder, // Days between reminders
retry: config.retry, // Number of reminders
});

Issue: Wrong Template Used

Symptoms: Default template sent instead of custom

Check Template Document:

const template = await mongoose.model('reviewTemplate').findOne({
account_id: ObjectId('account_id'),
});

if (!template) {
console.log('No custom template found - using defaults');
} else {
console.log('Custom template:', template);
}

📈 Metrics

Key Metrics:

  • Review requests sent per day: ~200
  • Email open rate: ~35%
  • SMS response rate: ~15%
  • Conversion rate (request → review): ~8%
  • Average time to review: 3-5 days

Monitoring:

// Review requests processed
db.getCollection('reviews.requests').aggregate([
{
$match: {
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
},
{
$group: {
_id: {
source: '$source',
processed: '$notification_processed',
},
count: { $sum: 1 },
},
},
]);

// Notification delivery
db.getCollection('communications').aggregate([
{
$match: {
module: 'reviews',
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
},
},
{
$group: {
_id: '$success',
count: { $sum: 1 },
},
},
]);

Module Type: Cron Job (every 30 seconds)
Environment Flag: AUTO_REVIEW_REQUEST_ENABLED
Dependencies: Stripe, Square, Shopify, QuickBooks integrations
Status: Active - marketing automation

💬

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