Skip to main content

📝 Forms Notifications

📖 Overview

The Forms Notifications module handles form submission alerts and automated follow-up reminders for forms that haven't received responses within 7 days.

Environment Flags:

  • FORMS_ENABLED=true - Form submission notifications
  • FORMS_NO_RESPONSE_ENABLED=true - 7-day no-response follow-ups

Trigger Types:

  • Change Stream on FormsUserResponse collection (submissions)
  • Cron Job for 7-day no-response checking

Notification Types: email, bell (FCM)
Location: notifications/services/forms/

🏗️ Architecture

System Flow

graph TB
subgraph "Triggers"
SUBMIT[Form Submission<br/>Change Stream]
CRON[No Response Check<br/>Cron Job]
end

subgraph "Processing"
PROC[processNotifications]
CHECK[checkNoResponse]
end

subgraph "Validation"
USER[User Verification]
ACCOUNT[Account Check]
end

subgraph "Notifications"
QUEUE[NotificationQueue]
end

SUBMIT --> PROC
CRON --> CHECK

PROC --> USER
USER --> ACCOUNT
ACCOUNT --> QUEUE

CHECK --> QUEUE

⚙️ Configuration

Environment Variables

# Module flags
FORMS_ENABLED=true # Enable form submission notifications
FORMS_NO_RESPONSE_ENABLED=true # Enable 7-day follow-ups

# External dependencies (inherited)
SENDGRID_API_KEY=your_key
REDIS_HOST=localhost
REDIS_PORT=6379

Cron Schedule

// No response check runs every 30 seconds
cron.schedule('*/30 * * * * *', async () => {
await checkFormsNoResponse();
});

🔄 Change Stream Configuration

Form Submission Stream

Monitors for new form submissions:

const Stream = FormsUserResponse.watch(
[
{
$match: {
operationType: 'insert', // Only new submissions
},
},
{
$addFields: {
'metadata.form_recieved': true, // Flag for processing
},
},
],
{ fullDocument: 'updateLookup' },
);

Stream.on('change', async data => {
await processNotifications(data);
});

Match Conditions:

  • Operation: insert only (new form submissions)
  • Metadata: form_recieved: true flag added

📧 Notification Templates

1. New Form Submission

Trigger: New form submission received
Type: email, bell
Recipients: Form owners, assigned users

Email Content Variables:

{
form_name: "Contact Form",
respondent_name: "John Doe",
respondent_email: "john@example.com",
respondent_phone: "+1234567890",
submission_date: "2025-10-13",
submission_time: "10:30 AM",
form_url: "https://app.dashclicks.com/forms/507f1f77bcf86cd799439011",
responses: [
{ question: "How can we help?", answer: "I need more information" },
{ question: "Preferred contact method", answer: "Email" }
],
account_name: "Business Name"
}

Bell Notification:

{
title: "New Form Submission",
body: "John Doe submitted Contact Form",
click_action: "https://app.dashclicks.com/forms/507f1f77bcf86cd799439011",
module: "forms",
type: "submission",
data: {
subType: "bell",
form_id: "507f1f77bcf86cd799439011",
response_id: "507f1f77bcf86cd799439012"
}
}

2. No Response Follow-up (7 days)

Trigger: Cron job - form submitted 7 days ago without response
Type: email
Recipients: Form assignee or account owner

Email Content Variables:

{
form_name: "Contact Form",
respondent_name: "John Doe",
respondent_email: "john@example.com",
days_without_response: 7,
submission_date: "2025-10-06",
form_url: "https://app.dashclicks.com/forms/507f1f77bcf86cd799439011",
reminder_message: "This submission has not been responded to yet. Please review and respond."
}

🔍 Processing Logic

Form Submission Processing

// From services/forms/processNotification.js
async function processNotifications(data) {
const response = data.fullDocument;

// 1. Fetch form details
const form = await Forms.findById(response.form_id).populate('account_id').lean();

if (!form) {
logger.warn({
message: 'Form not found for submission',
response_id: response._id,
});
return;
}

// 2. Determine recipients
let recipients = [];

if (form.assigned_to) {
// Send to assigned user
const assignedUser = await User.findById(form.assigned_to).lean();
if (assignedUser) recipients.push(assignedUser);
} else {
// Send to account owner
const owner = await User.findOne({
account: form.account_id,
is_owner: true,
}).lean();
if (owner) recipients.push(owner);
}

// 3. Verify user preferences
for (const recipient of recipients) {
const canNotify = await NotificationUtil.verify({
userID: recipient._id,
accountID: form.account_id,
module: 'forms',
type: 'submission',
subType: 'email',
});

if (!canNotify) continue;

// 4. Create email notification
await NotificationQueue.create({
type: 'email',
origin: 'forms',
sender_account: form.account_id,
recipient: {
name: recipient.name,
first_name: recipient.first_name,
last_name: recipient.last_name,
email: recipient.email,
},
content: {
template_id: 'd-form-submission-template',
additional_data: {
form_name: form.name,
respondent_name: response.user_name || 'Anonymous',
respondent_email: response.user_email,
respondent_phone: response.user_phone,
submission_date: formatDate(response.created_at),
submission_time: formatTime(response.created_at),
form_url: `https://app.dashclicks.com/forms/${form._id}`,
responses: response.responses,
},
},
check_credits: false,
internal_sender: false,
});

// 5. Create bell notification
await NotificationQueue.create({
type: 'fcm',
origin: 'forms',
sender_account: form.account_id,
recipient: {
user_id: recipient._id,
},
content: {
title: 'New Form Submission',
body: `${response.user_name || 'Someone'} submitted ${form.name}`,
click_action: `https://app.dashclicks.com/forms/${form._id}`,
module: 'forms',
type: 'submission',
data: {
subType: 'bell',
form_id: form._id.toString(),
response_id: response._id.toString(),
},
},
});
}

logger.log({
initiator: 'notifications/forms/processNotification',
message: 'Form submission notifications queued',
form_id: form._id,
response_id: response._id,
});
}

No Response Follow-up Processing

// From services/forms/no-response-7-days.js
async function checkFormsNoResponse() {
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

// 1. Find forms submitted 7 days ago without sent requests
const unrepliedForms = await FormsSentRequests.find({
created_at: {
$gte: sevenDaysAgo,
$lt: new Date(sevenDaysAgo.getTime() + 24 * 60 * 60 * 1000),
},
'metadata.no_response_reminder_sent': { $ne: true },
}).lean();

for (const sentRequest of unrepliedForms) {
// 2. Check if form has been responded to
const response = await FormsUserResponse.findOne({
form_id: sentRequest.form_id,
user_email: sentRequest.recipient_email,
created_at: { $gte: sentRequest.created_at },
}).lean();

if (response) {
// Already responded, mark as such
await FormsSentRequests.updateOne(
{ _id: sentRequest._id },
{ 'metadata.no_response_reminder_sent': true },
);
continue;
}

// 3. Fetch form details
const form = await Forms.findById(sentRequest.form_id).populate('account_id').lean();

if (!form) continue;

// 4. Determine recipient (assignee or owner)
let recipient;
if (form.assigned_to) {
recipient = await User.findById(form.assigned_to).lean();
} else {
recipient = await User.findOne({
account: form.account_id,
is_owner: true,
}).lean();
}

if (!recipient) continue;

// 5. Verify user preferences
const canNotify = await NotificationUtil.verify({
userID: recipient._id,
accountID: form.account_id,
module: 'forms',
type: 'no_response',
subType: 'email',
});

if (!canNotify) continue;

// 6. Create reminder notification
await NotificationQueue.create({
type: 'email',
origin: 'forms',
sender_account: form.account_id,
recipient: {
name: recipient.name,
email: recipient.email,
},
content: {
template_id: 'd-form-no-response-template',
additional_data: {
form_name: form.name,
respondent_name: sentRequest.recipient_name,
respondent_email: sentRequest.recipient_email,
days_without_response: 7,
submission_date: formatDate(sentRequest.created_at),
form_url: `https://app.dashclicks.com/forms/${form._id}`,
reminder_message: 'This submission has not been responded to yet.',
},
},
check_credits: false,
internal_sender: false,
});

// 7. Mark reminder as sent
await FormsSentRequests.updateOne(
{ _id: sentRequest._id },
{ 'metadata.no_response_reminder_sent': true },
);

logger.log({
initiator: 'notifications/forms/no-response-7-days',
message: 'No response reminder sent',
form_id: form._id,
sent_request_id: sentRequest._id,
});
}
}

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

🔍 Data Sources

Models Used

Primary Models:

  • Forms - Form definitions and configuration
  • FormsUserResponse - Form submissions
  • FormsSentRequests - Tracking sent form requests

Related Models:

  • Account - Business information
  • User - Recipient information
  • UserConfig - Notification preferences

Database Queries

Find Form by ID:

const form = await Forms.findById(form_id).populate('account_id').populate('assigned_to').lean();

Find Unreplied Forms (7 days old):

const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const unrepliedForms = await FormsSentRequests.find({
created_at: {
$gte: sevenDaysAgo,
$lt: new Date(sevenDaysAgo.getTime() + 24 * 60 * 60 * 1000),
},
'metadata.no_response_reminder_sent': { $ne: true },
});

Check for Response:

const response = await FormsUserResponse.findOne({
form_id: form_id,
user_email: recipient_email,
created_at: { $gte: sent_request_date },
});

🚨 Error Handling

Common Errors

Error 1: Form not found
Cause: Form deleted after submission
Resolution: Log warning and skip notification

const form = await Forms.findById(response.form_id);
if (!form) {
logger.warn({
message: 'Form not found for submission',
response_id: response._id,
});
return;
}

Error 2: No recipients found
Cause: No assigned user and no account owner
Resolution: Log error and skip notification

if (!recipients.length) {
logger.error({
message: 'No recipients found for form submission',
form_id: form._id,
});
return;
}

Error 3: User opted out
Cause: User disabled form notifications in preferences
Resolution: Respect preference and skip notification

const canNotify = await NotificationUtil.verify({
userID: recipient._id,
accountID: form.account_id,
module: 'forms',
type: 'submission',
subType: 'email'
});

if (!canNotify) {
logger.log({
message: 'User opted out of form notifications',
user_id: recipient._id
});
continue;
}

💡 Examples

Example 1: New Form Submission

Trigger Event:

// FormsUserResponse insert
{
operationType: 'insert',
fullDocument: {
_id: ObjectId("507f1f77bcf86cd799439011"),
form_id: ObjectId("507f1f77bcf86cd799439012"),
user_name: "John Doe",
user_email: "john@example.com",
user_phone: "+1234567890",
responses: [
{
question: "What service are you interested in?",
answer: "SEO Services"
},
{
question: "Budget range",
answer: "$1,000 - $5,000"
}
],
created_at: new Date("2025-10-13T10:30:00Z")
}
}

Resulting Notifications:

  1. Email - New form submission details
  2. Bell - Real-time notification in dashboard

Delivery:

  • Sent to: Form assignee (or account owner if no assignee)
  • Using: SendGrid template + FCM
  • Result: Email delivered, bell notification shown

Example 2: No Response Follow-up

Scenario:

  • Form submitted: October 6, 2025
  • Current date: October 13, 2025 (7 days later)
  • No response from recipient yet

Query Result:

// FormsSentRequests document
{
_id: ObjectId("507f1f77bcf86cd799439011"),
form_id: ObjectId("507f1f77bcf86cd799439012"),
recipient_name: "Jane Smith",
recipient_email: "jane@example.com",
created_at: new Date("2025-10-06T14:00:00Z"),
metadata: {
no_response_reminder_sent: false
}
}

// No matching FormsUserResponse found

Resulting Notification:

Email - Reminder to follow up on unreplied form

Content:

  • Form name: "Contact Form"
  • Respondent: Jane Smith (jane@example.com)
  • Days without response: 7
  • Message: "This submission has not been responded to yet. Please review and respond."

After Sending:

// Update FormsSentRequests
{
metadata: {
no_response_reminder_sent: true;
}
}

🐛 Troubleshooting

Issue: Form Submission Notifications Not Sent

Symptoms: Forms submitted but no notifications

Check:

  1. Environment flag:

    echo $FORMS_ENABLED
    # Should output: true
  2. Change stream active:

    grep "FORMS SERVICE RUNNING" notifications.log
  3. Form assignment:

    const form = await Forms.findById(form_id);
    console.log('Assigned to:', form.assigned_to);
    console.log('Account:', form.account_id);
  4. User preferences:

    const config = await UserConfig.findOne({
    user_id: recipient_id,
    account_id: account_id,
    type: 'forms',
    });
    console.log('Preferences:', config?.preferences);

Issue: No Response Reminders Not Working

Symptoms: 7-day reminders not sent

Check Cron Job:

grep "FORMS NOT RESPONSE SERVICE RUNNING" notifications.log

Check FormsSentRequests:

const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);

const unreplied = await FormsSentRequests.find({
created_at: {
$gte: sevenDaysAgo,
$lt: new Date(sevenDaysAgo.getTime() + 24 * 60 * 60 * 1000),
},
'metadata.no_response_reminder_sent': { $ne: true },
});

console.log('Unreplied forms:', unreplied.length);

Issue: Duplicate Notifications

Symptoms: Same submission triggers multiple notifications

Causes:

  1. Multiple service instances: Same event processed twice
  2. Change stream replay: Service restart processes old events

Solutions:

  • Ensure only one notification service instance running
  • Verify resume token is working properly
  • Add idempotency checks in processing logic

📈 Metrics

Key Metrics:

  • Form submissions per day: ~500
  • Notification success rate: >98%
  • 7-day no-response rate: ~15%
  • Average response time to form: 2-3 hours

Monitoring:

// Form submissions today
db.getCollection('forms.user_responses').count({
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});

// Notifications sent
db.getCollection('notifications.queue').count({
origin: 'forms',
created_at: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});

// No response reminders
db.getCollection('forms.sent_requests').count({
'metadata.no_response_reminder_sent': true,
'metadata.reminder_sent_at': { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
});

Module Type: Change Stream + Cron Job
Environment Flags: FORMS_ENABLED, FORMS_NO_RESPONSE_ENABLED
Dependencies: MongoDB (replica set), SendGrid, Firebase
Status: Active - high engagement module

💬

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