⭐ 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
-
Integration Creates Request:
- Stripe/Square/Shopify webhook receives purchase event
- Creates
ReviewRequestdocument with customer info - Sets
notification_processed: false
-
Cron Job Detects Request:
- Runs every 30 seconds
- Queries for unprocessed review requests
- Filters by
source(stripe, squareUp, shopify, etc.)
-
Configuration Lookup:
- Fetches
AutoReviewRequestfor account - Determines if email, SMS, or both enabled
- Gets custom templates if configured
- Fetches
-
Data Preparation:
- Fetches account business info (logo, branding, social links)
- Calculates average rating and total reviews
- Generates review landing page URL
- Prepares template variables
-
Notification Creation:
- Creates email notification (if enabled)
- Creates SMS notification (if enabled)
- Includes review request ID for tracking
- Sets
check_credits: true(deducts credits)
-
Queue Processing:
- Queue processor adds to Bull with delay
- If
send_only_once: false, configures repeating job - Bull handles delivery and retries
-
Completion:
- Marks
ReviewRequestasnotification_processed: true - Prevents duplicate processing
- Marks
📋 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 integrationsAutoReviewRequest- Account-specific configurationreviewTemplate- Custom email/SMS templates
Related Models:
Account- Business info, branding, domainContact- Customer informationUser- Account owner infoReviews- 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: trueimmediately 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:
-
Environment flag:
echo $AUTO_REVIEW_REQUEST_ENABLED
# Should output: true -
Cron job running:
grep "AUTO REVIEW REQUEST SERVICE RUNNING" notifications.log -
ReviewRequest documents:
db.getCollection('reviews.requests').find({
notification_processed: { $ne: true },
}); -
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