Skip to main content

Quarterly Check-In Pulse Processor

Overview

The Quarterly Check-In Pulse processor creates pulses every 3 months for agencies with active managed subscriptions. It uses modulo arithmetic to determine if an account is in the first 2 months of a quarterly cycle and prevents duplicate pulses within 90 days.

Source File: queue-manager/services/projects/quarterlyCheckinPulse.js
Queue File: queue-manager/queues/projects/quarterlycheckinpulse.js
Execution: Part of projects cron (every 5 minutes)
Business Impact: MEDIUM-HIGH - Customer engagement

Processing Flow

sequenceDiagram
participant CRON as Projects Cron
participant SVC as Check-In Service
participant SUBS as Subscriptions
participant PULSE as Projects Pulse
participant QUEUE as Check-In Queue

CRON->>SVC: quarterlyCheckInPulse()
SVC->>SUBS: Find active subscriptions<br/>(90+ days old)
SUBS-->>SVC: Account list

loop For Each Account
SVC->>SVC: Calculate months since oldest subscription
SVC->>SVC: Calculate remainder (monthsDiff % 3)

alt Remainder <= 1 (first 2 months of quarter)
SVC->>PULSE: Check last pulse<br/>(completed >90 days ago or none)

alt No Recent Pulse
SVC->>QUEUE: Add to queue
QUEUE->>PULSE: Create quarterly check-in
end
end
end

Key Logic

Quarterly Cycle Calculation

// Calculate months since oldest subscription
monthsDiff = dateDiff(oldestSubscription, now, 'month');

// Calculate position in quarterly cycle
remainder = monthsDiff % 3;

// Examples:
// 0 months old: 0 % 3 = 0 ✅ (first month)
// 1 month old: 1 % 3 = 1 ✅ (second month)
// 2 months old: 2 % 3 = 2 ❌ (third month - skip)
// 3 months old: 3 % 3 = 0 ✅ (new quarter starts)
// 4 months old: 4 % 3 = 1 ✅

Eligibility Criteria

Accounts must meet ALL criteria:

  • Subscription Status: Active
  • Product Type: Managed subscriptions (excluding sites and listings)
  • Age: At least 90 days old
  • Quarterly Position: remainder less than or equal to 1 (first 2 months of quarter)
  • Last Pulse: No pending pulse AND (no pulse OR completed more than 90 days ago)

Development Mode

const IS_DEV_TESTING = false; // Set to true for testing

if (IS_DEV_TESTING) {
ninetyDaysAgo = (Math.floor(Date.now() / 1000) - 10 * 60) * 1000; // 10 minutes
timeUnit = 'minute'; // Use minutes instead of months
}

MongoDB Aggregation Pipeline

Stage 1: Match Active Subscriptions

{
$match: {
status: 'active',
'plan.metadata.product_type': {
$in: UPDATED_MANAGED_SUBSCRIPTIONS
}
}
}

Stage 2: Group by Account

{
$group: {
_id: '$account',
oldestSubscription: { $min: '$created_at' },
subscriptionIds: { $push: '$_id' }
}
}

Stage 3: Lookup Last Pulse

{
$lookup: {
from: 'projects.pulse',
let: { acc_id: { $toObjectId: '$_id' } },
pipeline: [
{
$match: {
type: 'quarterly_checkin',
$expr: { $eq: ['$account_id', '$$acc_id'] }
}
},
{ $sort: { created_at: -1 } },
{ $limit: 1 }
],
as: 'lastPulse'
}
}

Stage 4: Extract Pulse Data

{
$addFields: {
lastPulseStatus: { $arrayElemAt: ['$lastPulse.status', 0] },
lastPulseCompletedAt: { $arrayElemAt: ['$lastPulse.completed_at', 0] },
oldestSubscriptionDate: { $toDate: '$oldestSubscription' }
}
}

Stage 5: Calculate Time Difference

{
$addFields: {
monthsDiff: {
$dateDiff: {
startDate: '$oldestSubscriptionDate',
endDate: '$$NOW',
unit: 'month' // or 'minute' in dev mode
}
}
}
}

Stage 6: Calculate Quarterly Position

{
$addFields: {
remainder: {
$mod: ['$monthsDiff', 3];
}
}
}

Stage 7: Filter Eligible Accounts

{
$match: {
$and: [
// Old enough (90+ days)
{ oldestSubscription: { $lte: new Date(ninetyDaysAgo) } },

// In first 2 months of quarter
{ remainder: { $lte: 1 } },

// No pending pulse
{
$or: [{ lastPulseStatus: { $exists: false } }, { lastPulseStatus: { $ne: 'pending' } }],
},

// No recent completed pulse
{
$or: [
{ lastPulseStatus: { $exists: false } },
{
$and: [
{ lastPulseStatus: 'completed' },
{ lastPulseCompletedAt: { $exists: true, $ne: null } },
{ lastPulseCompletedAt: { $lte: new Date(ninetyDaysAgo) } },
],
},
],
},
];
}
}

Stage 8: Project Output

{
$project: {
_id: 0,
account_id: '$_id',
subscription_ids: '$subscriptionIds',
oldestSubscription: 1
}
}

Queue Processing

const queue = await quarterlyCheckInPulse.start();

for (const quarterlyCheckin of quarterlyCheckInPulseInfo) {
await queue.add(quarterlyCheckin, {
removeOnComplete: true,
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
}

Pulse Structure

{
account_id: ObjectId,
type: 'quarterly_checkin',
status: 'pending',
metadata: {
subscription_ids: [...],
oldest_subscription: Date,
quarter: 1,
cycle_position: 'first_two_months'
}
}

Collections Used

Input: store-subscription

  • Queries active subscriptions
  • Groups by account
  • Finds oldest subscription per account

Check: projects-pulse

  • Finds last quarterly check-in pulse
  • Prevents duplicates within 90 days

Output: projects-pulse (via queue)

  • Creates quarterly check-in pulses

Business Rules

Why Quarterly (3 Months)?

  • Aligns with standard business quarterly reviews
  • Not too frequent (avoid fatigue)
  • Not too rare (maintain engagement)
  • Industry standard for customer success check-ins

Why remainder less than or equal to 1?

Creates 2-month window for engagement:

  • Month 0-1: Pulse created
  • Month 2: Grace period (no pulse)
  • Month 3: New quarter starts (pulse created again)

Example Timeline

Month 0: ✅ Pulse created
Month 1: ✅ Still in window (no new pulse due to recent one)
Month 2: ❌ Third month of quarter (remainder = 2, skipped)
Month 3: ✅ New quarter starts (remainder = 0, pulse created)
Month 4: ✅ Still in window
Month 5: ❌ Third month
Month 6: ✅ New quarter

Why 90-Day Minimum?

  • Customers need time to establish relationship
  • Prevents premature check-ins during onboarding
  • First check-in happens at end of first quarter

Error Handling

try {
// Aggregation and queue processing
} catch (error) {
logger.error({
initiator: 'QM/projects/quarterlyCheckInPulse',
error: error,
});
throw error;
}

Development Testing

Enable Testing Mode

const IS_DEV_TESTING = true; // Enable for testing

Testing Changes

  • Time window: 90 days → 10 minutes
  • Time unit: months → minutes
  • Creates rapid cycles for testing
  • Logs development mode status

Performance

Execution Time: 3-6 seconds
Frequency: Every 5 minutes
Typical Results: 10-30 accounts per run
Peak: 50-100 accounts (beginning of quarters: Jan, Apr, Jul, Oct)


Complexity: MEDIUM-HIGH
Lines of Code: 188
Mathematical: Uses modulo arithmetic

💬

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