Skip to main content

Affiliates - Leaderboard Generation

Overview

The Affiliates Leaderboard Generation module creates monthly rankings of top-performing affiliates based on commission earnings. It runs on the 1st of each month at 1:00 PM, calculating both all-time and last-month rankings using MongoDB aggregation pipelines with transaction safety. The system maintains rank history by tracking previous positions, enabling rank change visualization in the dashboard.

Key Features:

  • Monthly Execution: Runs on 1st of each month at 13:00 (1:00 PM)
  • Dual Rankings: All-time totals and last-month performance
  • Top 10 Lists: Limits rankings to top 10 affiliates per category
  • Rank Change Tracking: Stores previous rank for comparison
  • Transaction Safety: Uses MongoDB transactions for atomic updates
  • Soft Commission Tracking: Based on amount.software field (20% commission)

Critical Business Impact:

  • Affiliate Motivation: Public rankings encourage competition
  • Performance Recognition: Highlights top performers
  • Historical Tracking: Shows rank changes over time
  • Dashboard Integration: Powers leaderboard UI components
  • Revenue Insights: Identifies highest-earning affiliates

Architecture

Execution Flow

sequenceDiagram
participant Cron as Cron Scheduler
participant Service as Leaderboard Service
participant Queue as Bull Queue
participant Processor as Leaderboard Processor
participant DB as MongoDB
participant Payout as AffiliatePayout
participant Board as AffiliatesLeaderboard

Note over Cron,Board: 1st of Month at 13:00

Cron->>Service: Trigger leaderboard update
Service->>Queue: Add job to queue
Note over Queue: 4 attempts, exponential backoff

Queue->>Processor: Process leaderboard job
Processor->>DB: Start transaction

Processor->>Payout: Aggregate all-time rankings
Note over Payout: Sum amount.software<br/>Sort by total DESC<br/>Limit 10
Payout-->>Processor: Top 10 all-time affiliates

Processor->>Payout: Aggregate last-month rankings
Note over Payout: Filter cutoff_date >= last month<br/>Sum amount.software<br/>Sort DESC, Limit 10
Payout-->>Processor: Top 10 last-month affiliates

Processor->>Board: Find previous rankings
Board-->>Processor: Previous rank data

Processor->>Processor: Calculate rank changes
Note over Processor: Compare current vs previous<br/>Set previous_rank field

Processor->>Board: Delete all existing rankings
Processor->>Board: Insert new rankings (20 docs)
Note over Board: 10 all_time + 10 month

Processor->>DB: Commit transaction
DB-->>Processor: Transaction complete
Processor->>Processor: Log completion

Component Structure

queue-manager/
├── crons/
│ └── affiliates/
│ └── leaderboard.js # Cron scheduler (monthly)
├── services/
│ └── affiliates/
│ └── leaderboard.js # Service logic (queue addition)
└── queues/
└── affiliates/
└── leaderboard.js # Processor (aggregation + ranking)

Cron Schedule

File: queue-manager/crons/affiliates/leaderboard.js

'0 13 1 * *'; // 1st of every month at 13:00 (1:00 PM)

Pattern: Monthly execution with specific timing

  • Day: 1st of month
  • Hour: 13:00 (1:00 PM)
  • In-Progress Locking: Prevents concurrent executions
  • Purpose: Generate monthly leaderboard after previous month completes

Configuration

Queue Retry Configuration

Pattern: queue-manager/services/affiliates/leaderboard.js

{
attemps: 4, // Note: typo in source code (should be "attempts")
backoff: 'exponential',
delay: 60000 // 60 seconds base delay
}

Retry Schedule:

AttemptDelayTotal Wait
10s0s
260s60s
3120s180s
4240s420s

Total: ~7 minutes of retry attempts


Service Implementation

Queue Addition

File: queue-manager/services/affiliates/leaderboard.js

module.exports = async Queue => {
try {
await Queue.add(
{}, // Empty payload - no job data needed
{
attemps: 4, // Typo in source
backoff: 'exponential',
delay: 60000,
},
);
} catch (err) {
console.log(err.message, err.stack);
}
};

Payload: Empty object (no data needed)

  • Self-Contained: Processor calculates rankings from database
  • Stateless: No input parameters required

Queue Processor

Leaderboard Calculation

File: queue-manager/queues/affiliates/leaderboard.js

Transaction Wrapper

const session = await mongoose.startSession();
await session.withTransaction(async session => {
// All ranking operations within transaction
});

Purpose: Atomic updates to leaderboard

  • Consistency: All-or-nothing update
  • Rollback: Failures don't leave partial rankings
  • Isolation: No partial state visible to readers

All-Time Rankings Aggregation

let rankings = await Payout.aggregate([
{
$match: {}, // Match all payouts
},
{
$facet: {
all_time: [
{
$group: {
_id: '$account',
amount: {
$sum: '$amount.software',
},
},
},
{
$sort: {
amount: -1, // Descending order
},
},
{
$limit: 10,
},
],
// ... last_month facet
},
},
]);

Pipeline Stages:

  1. $match: Match all payout documents (no filter)
  2. $facet: Parallel aggregation pipelines
  3. $group: Sum amount.software by account
  4. $sort: Order by total amount descending
  5. $limit: Top 10 only

Output Structure:

[
{
all_time: [
{ _id: ObjectId('account1'), amount: 15000 },
{ _id: ObjectId('account2'), amount: 12000 },
// ... up to 10
],
last_month: [
// ... similar structure
],
},
];

Last-Month Rankings Aggregation

let lastMonth = moment().subtract(1, 'M');

last_month: [
{
$match: {
cutoff_date: { $gte: lastMonth.toDate() },
},
},
{
$group: {
_id: '$account',
amount: {
$sum: '$amount.software',
},
},
},
{
$sort: {
amount: -1,
},
},
{
$limit: 10,
},
];

Time Filter: cutoff_date >= (current month - 1 month)

Purpose: Ranks based on last month's commissions only

  • Rolling Window: Always looks at previous month
  • Execution Time: Runs on 1st, so looks at completed previous month

Example: If run on January 1st, calculates December rankings

Previous Rank Lookup

let previousRankings = await Leaderboard.find();

Purpose: Fetch all existing rankings before deletion

  • Rank Changes: Compare new rank to old rank
  • Dashboard Display: Show "↑3" or "↓2" indicators

Rank Document Construction

let allTimeDocs = (rankings[0]?.all_time || []).map((r, i) => {
return {
account: r._id,
rank: i + 1, // Position in top 10
previous_rank:
previousRankings.find(
r2 => r2.account.toString() == r._id.toString() && r2?._doc?.type == 'all_time',
)?.rank || 9999, // Default: unranked
type: 'all_time',
};
});

let lastMonthDocs = (rankings[0]?.last_month || []).map((r, i) => {
return {
account: r._id,
rank: i + 1,
previous_rank:
previousRankings.find(
r2 => r2.account.toString() == r._id.toString() && r2?._doc?.type == 'month',
)?.rank || 9999,
type: 'month',
};
});

Rank Calculation:

  • rank: Current position (1-10)
  • previous_rank: Previous month's position
    • If found: Previous position
    • If not found: 9999 (indicates "new to leaderboard")

Type Field:

  • all_time: Lifetime total commissions
  • month: Last month's commissions

Leaderboard Update

await Leaderboard.deleteMany({}, { session });

let promises = [...allTimeDocs, ...lastMonthDocs].map(d => {
return new Leaderboard(d).save({ session });
});

await Promise.all(promises);

Update Pattern:

  1. Delete All: Clear existing leaderboard
  2. Insert New: Save 20 documents (10 all-time + 10 monthly)
  3. Transaction: Both operations succeed or both rollback

Document Count: Exactly 20 documents after each run

  • 10 all-time rankings
  • 10 last-month rankings

Data Models

AffiliatePayout Collection

Collection: affiliate-payout

{
_id: ObjectId,
account: ObjectId, // Affiliate account ID
amount: {
software: Number, // 20% commission on software sales
managed: Number // 10% commission on managed services
},
status: String, // 'PENDING' | 'PAID' | 'EXPIRED'
cutoff_date: Date, // Payout period end date
created_at: Date,
// ... other fields
}

Key Fields for Leaderboard:

  • account: Groups by affiliate account
  • amount.software: Used for ranking (20% commission rate)
  • cutoff_date: Filters last month's commissions

AffiliatesLeaderboard Collection

Collection: affiliates-leaderboard

{
_id: ObjectId,
account: ObjectId, // Affiliate account ID
rank: Number, // Current rank (1-10)
previous_rank: Number, // Previous month's rank (or 9999)
type: String, // 'all_time' | 'month'
created_at: Date,
updated_at: Date
}

Type Values:

  • all_time: Lifetime commission totals
  • month: Last month's commission totals

Rank Change Calculation:

const change = previous_rank - rank;
// Positive: Moved up (was #5, now #2 = +3)
// Negative: Moved down (was #2, now #5 = -3)
// 9999 - rank: New to leaderboard

Leaderboard Logic

Ranking Algorithm

All-Time Rankings:

  1. Sum amount.software for each account across all payouts
  2. Sort accounts by total descending
  3. Take top 10
  4. Assign ranks 1-10

Last-Month Rankings:

  1. Filter payouts with cutoff_date >= lastMonth
  2. Sum amount.software for each account in filtered set
  3. Sort accounts by total descending
  4. Take top 10
  5. Assign ranks 1-10

Rank Change Examples

Scenario 1: Moved Up

{
account: "affiliate_123",
rank: 2, // Current: #2
previous_rank: 5, // Previous: #5
type: "all_time"
}
// Rank change: +3 (moved up 3 positions)

Scenario 2: New to Leaderboard

{
account: "affiliate_456",
rank: 7, // Current: #7
previous_rank: 9999, // Previous: Not ranked
type: "month"
}
// Display: "NEW" or "↑" indicator

Scenario 3: Dropped Out

  • If previously ranked but not in new top 10
  • Document not created (only top 10 stored)
  • Dashboard shows as "Not Ranked" if queried

Error Handling

Transaction Failures

Causes:

  • MongoDB connection loss during transaction
  • Aggregation errors
  • Document save failures

Behavior:

await session.withTransaction(async session => {
// If any operation fails, entire transaction rolls back
});

Recovery: Job retries with exponential backoff

Aggregation Errors

Potential Issues:

  1. Empty AffiliatePayout collection
  2. All payouts have zero amount.software
  3. Invalid cutoff_date values

Handling:

let allTimeDocs = (rankings[0]?.all_time || []).map(...)

Fallback: Empty array if aggregation returns no results

  • Creates 0 all-time documents
  • Creates 0-10 month documents (depending on last month)

Previous Rank Lookup Failures

Scenario: Leaderboard.find() fails

Impact: No previous rank data available

Default Behavior:

previous_rank: previousRankings.find(...) || 9999

Result: All affiliates treated as "new to leaderboard"


Testing Scenarios

1. First Leaderboard Generation

Setup:

// No existing leaderboard documents
await Leaderboard.deleteMany({});

// Create payouts for 15 affiliates
await AffiliatePayout.insertMany([
{ account: affiliate1._id, amount: { software: 5000 }, cutoff_date: lastMonth },
{ account: affiliate2._id, amount: { software: 4500 }, cutoff_date: lastMonth },
// ... 13 more
]);

Expected:

  • All previous_rank: 9999
  • Top 10 ranked 1-10
  • 5 affiliates not included (only top 10)

2. Rank Changes

Setup:

// Previous leaderboard
await Leaderboard.insertMany([
{ account: affiliate1._id, rank: 3, type: 'all_time' },
{ account: affiliate2._id, rank: 1, type: 'all_time' },
]);

// New payouts push affiliate1 to #1
await AffiliatePayout.create({
account: affiliate1._id,
amount: { software: 10000 },
});

Expected:

  • affiliate1: rank: 1, previous_rank: 3 (+2)
  • affiliate2: rank: 2, previous_rank: 1 (-1)

3. Monthly vs All-Time Divergence

Setup:

// affiliate1: High all-time, low last month
await AffiliatePayout.insertMany([
{ account: affiliate1._id, amount: { software: 50000 }, cutoff_date: '2024-01-01' }, // Old
{ account: affiliate1._id, amount: { software: 100 }, cutoff_date: lastMonth }, // Recent
]);

// affiliate2: Low all-time, high last month
await AffiliatePayout.insertMany([
{ account: affiliate2._id, amount: { software: 1000 }, cutoff_date: '2024-01-01' },
{ account: affiliate2._id, amount: { software: 8000 }, cutoff_date: lastMonth },
]);

Expected:

  • All-time: affiliate1 ranked higher
  • Month: affiliate2 ranked higher

4. Transaction Rollback

Scenario: Simulate failure during save

// Mock Leaderboard.save to fail on 11th document
let saveCount = 0;
Leaderboard.prototype.save = function () {
saveCount++;
if (saveCount === 11) throw new Error('Save failed');
return originalSave.call(this);
};

Expected:

  • Transaction rolls back
  • No new leaderboard documents created
  • Old leaderboard remains intact
  • Job retries

5. Empty Payouts

Setup:

await AffiliatePayout.deleteMany({});

Expected:

  • Aggregation returns empty arrays
  • 0 documents created in leaderboard
  • No errors thrown
  • Transaction completes successfully

Performance Considerations

Aggregation Optimization

Index Requirements:

// AffiliatePayout collection
{ account: 1, cutoff_date: 1 }
{ 'amount.software': -1 }

Query Patterns:

  • Group by account
  • Filter by cutoff_date
  • Sort by computed amount

Transaction Duration

Operations:

  1. Aggregation: ~100-500ms (depending on payout count)
  2. Previous rank lookup: ~10-50ms
  3. Delete: ~10ms
  4. Insert 20 docs: ~50-100ms

Total: ~200-700ms transaction duration

  • Low Lock Time: Short transaction reduces contention
  • Monthly Frequency: Only runs once per month

Document Count

Leaderboard Size: Always exactly 20 documents

  • 10 all-time
  • 10 monthly

Storage: Minimal (~1KB per document)


Monitoring & Logging

Log Patterns

Cron Logs:

logger.log({
initiator: 'QM/affiliates/leaderboard',
message: 'Execution Started for processLeaderboard()',
});

logger.log({
initiator: 'QM/affiliates/leaderboard',
message: 'Execution Finished for processLeaderboard()',
});

Error Logs:

logger.error({
initiator: 'QM/affiliates/leaderboard',
error: err,
});

Metrics to Monitor

  1. Execution Time: Duration of leaderboard generation
  2. Affiliate Count: Number of affiliates with payouts
  3. Top 10 Threshold: Minimum amount for #10 spot
  4. Rank Changes: Count of affiliates moving up/down
  5. New Entrants: Affiliates with previous_rank: 9999
  6. Transaction Failures: Rollback count

Alerting Scenarios

  • Execution Failure: Job fails after 4 retries
  • Empty Leaderboard: 0 documents created
  • Transaction Rollback: Repeated rollbacks
  • Long Execution: Takes > 5 seconds
  • Aggregation Timeout: MongoDB query timeout

Dashboard Integration

Leaderboard Query

// All-time leaderboard
const allTimeBoard = await AffiliatesLeaderboard.find({ type: 'all_time' })
.populate('account')
.sort({ rank: 1 });

// Monthly leaderboard
const monthlyBoard = await AffiliatesLeaderboard.find({ type: 'month' })
.populate('account')
.sort({ rank: 1 });

Rank Change Display

const rankChange = board.previous_rank - board.rank;

if (board.previous_rank === 9999) {
display = 'NEW';
} else if (rankChange > 0) {
display = `${rankChange}`; // Moved up
} else if (rankChange < 0) {
display = `${Math.abs(rankChange)}`; // Moved down
} else {
display = '─'; // No change
}


Summary

The Affiliates Leaderboard Generation module provides monthly ranking updates for affiliate performance tracking. Its transaction-safe aggregation pipeline calculates both all-time and last-month rankings, maintaining rank history for change visualization. The monthly execution on the 1st ensures rankings reflect the completed previous month's performance.

Key Strengths:

  • Dual Rankings: All-time and monthly performance views
  • Transaction Safety: Atomic updates with rollback protection
  • Rank Change Tracking: Maintains previous rank for comparison
  • Top 10 Focus: Highlights best performers
  • Efficient Aggregation: $facet enables parallel pipeline execution

Critical for:

  • Affiliate motivation and competition
  • Performance recognition
  • Historical trend analysis
  • Dashboard leaderboard UI
  • Revenue attribution insights
💬

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