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