Commission Lifecycle Flow
Complete lifecycle of commission calculation using the Differential Commission Model, from triggering event to payout.
Overview
The commission lifecycle includes:
- Triggering events (7 income types)
- Job creation and queuing
- Idempotency and duplicate prevention
- Upline retrieval (unlimited depth)
- Differential commission calculation
- Balance updates and transaction recording
- Pending to available transition
- Commission reversal handling
- Leadership pool distribution
- Payout eligibility and processing
Commission Model Summary
| Property | Value |
|---|---|
| Model Type | Differential (not Unilevel) |
| Depth | Unlimited |
| Income Types | 7 (3 Active + 3 Passive + 1 Pool) |
| Primary Currency | USD |
| Rate Range | 3% - 20% based on rank |
Core Calculation Formula:
IF consultant_rank > source_partner_rank:
commission = (consultant_rate - source_rate) × amount
ELSE:
commission = 0 // No commission when rank is equal or lowerMain Flow Diagram
Step Details
1. Triggering Events (7 Income Types)
Events that trigger commission calculation:
| Income Type | Trigger Event | Source Type | Calculation |
|---|---|---|---|
| 1. Personal Sales | Direct sale to own client | ORDER | personal_rate × amount |
| 2. Team Sales | Downline partner makes sale | ORDER | Differential on sale amount |
| 3. Repeat Sales | Existing client repurchases | ORDER | Same as Personal Sales |
| 4. Portfolio Returns | Own investment profit | INVESTMENT_PROFIT | Strategy returns |
| 5. Client Profits | Personal client earns profit | INVESTMENT_PROFIT | passive_rate × client_profit |
| 6. Network Profits | Downline's client earns profit | INVESTMENT_PROFIT | Differential on profit |
| 7. Leadership Pool | Weekly/Monthly distribution | POOL_DISTRIBUTION | Equal share |
Job Payload Structure:
interface DifferentialCommissionPayload {
idempotencyKey: string;
incomeType:
| "PERSONAL_SALES"
| "TEAM_SALES"
| "REPEAT_SALES"
| "CLIENT_PROFITS"
| "NETWORK_PROFITS"
| "LEADERSHIP_POOL";
sourceType:
| "ORDER"
| "INVESTMENT"
| "INVESTMENT_PROFIT"
| "POOL_DISTRIBUTION";
sourceId: string;
amountUsd: number;
sourcePartnerId: string;
sourcePartnerRank: string;
sourcePartnerRate: number;
}Idempotency Key Format:
commission:{incomeType}:{sourceId}:{timestamp}Example: commission:TEAM_SALES:550e8400-e29b-41d4-a716-446655440000:1705315200
2. Job Creation and Queuing
pg-boss Job Configuration:
await boss.send("differential-commission", payload, {
retryLimit: 5,
retryDelay: 30000,
retryBackoff: true,
expireInMinutes: 60,
singletonKey: payload.idempotencyKey,
});Retry Schedule:
| Attempt | Delay | Total Time |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2.5m |
| 4 | 10 minutes | 12.5m |
| 5 | 30 minutes | 42.5m |
| Failed | N/A | Move to dead-letter queue |
3. Idempotency Check
Double-Check Pattern:
async function processDifferentialCommission(
job: DifferentialCommissionPayload,
) {
const { idempotencyKey } = job;
// 1. Fast path: Check outside transaction
const existing = await db.idempotencyKey.findUnique({
where: { key: idempotencyKey },
});
if (existing) {
return existing.response;
}
// 2. Process in transaction with re-check
return await db.$transaction(
async (tx) => {
const existingInTx = await tx.idempotencyKey.findUnique({
where: { key: idempotencyKey },
});
if (existingInTx) {
return existingInTx.response;
}
// ... process differential commission ...
await tx.idempotencyKey.create({
data: {
key: idempotencyKey,
response: result,
expiresAt: addDays(new Date(), 30),
},
});
return result;
},
{
isolationLevel: "Serializable",
timeout: 30000,
},
);
}4. Upline Retrieval (Unlimited Depth)
Query:
SELECT
p.id as partner_id,
p.status,
r.code as rank_code,
r.personal_sales_rate,
r.passive_income_rate,
r.entrance_fee_rate,
ptp.depth
FROM mlm.partner_tree_paths ptp
JOIN mlm.partners p ON p.id = ptp.ancestor_id
JOIN mlm.ranks r ON r.id = p.current_rank_id
WHERE ptp.descendant_id = $1::uuid
AND ptp.depth >= 1
ORDER BY ptp.depth ASC;Result Example:
| partner_id | rank_code | personal_sales_rate | depth |
|---|---|---|---|
| partner-A | 5 | 14 | 1 |
| partner-B | 3 | 10 | 2 |
| partner-C | 6 | 16 | 3 |
Key Difference from Unilevel:
- No level limit - retrieves ENTIRE upline chain
- Processing stops when max rate (20%) is reached
- Commission based on rate differential, not level percentage
5. Differential Commission Calculation
Core Algorithm:
async function calculateDifferentialCommissions(
tx: Transaction,
job: DifferentialCommissionPayload,
upline: UplinePartner[],
) {
const commissions = [];
let currentSourceRate = job.sourcePartnerRate;
for (const ancestor of upline) {
if (ancestor.status !== "ACTIVE") continue;
// Get applicable rate based on income type
const ancestorRate = getApplicableRate(job.incomeType, ancestor);
// Calculate differential
const differentialRate = ancestorRate - currentSourceRate;
if (differentialRate > 0) {
const grossAmountUsd = job.amountUsd * (differentialRate / 100);
const netAmountUsd = grossAmountUsd;
// Create commission record
const commission = await tx.commissionTransaction.create({
data: {
partnerId: ancestor.partner_id,
incomeType: job.incomeType,
sourceType: job.sourceType,
sourceId: job.sourceId,
sourcePartnerId: job.sourcePartnerId,
ownRate: ancestorRate,
sourceRate: currentSourceRate,
differentialRate,
grossAmountUsd,
netAmountUsd,
currency: "USD",
status: "PENDING",
idempotencyKey: `${job.idempotencyKey}:${ancestor.partner_id}`,
},
});
// Update pending balance
await tx.partnerBalance.update({
where: { partnerId: ancestor.partner_id },
data: {
pendingBalanceUsd: { increment: netAmountUsd },
version: { increment: 1 },
lastCalculatedAt: new Date(),
},
});
commissions.push(commission);
// Update current source rate for next iteration
currentSourceRate = ancestorRate;
}
// Stop if max rate (20%) reached
if (currentSourceRate >= 20) break;
}
return commissions;
}
function getApplicableRate(incomeType: string, partner: UplinePartner): number {
switch (incomeType) {
case "PERSONAL_SALES":
case "TEAM_SALES":
case "REPEAT_SALES":
return partner.personal_sales_rate;
case "CLIENT_PROFITS":
case "NETWORK_PROFITS":
return partner.passive_income_rate;
default:
return partner.personal_sales_rate;
}
}Example Differential Calculation:
Sale Amount: $10,000 USD Source Partner: Rank 2 (8% rate)
| Depth | Partner | Rank | Rate | Differential | Commission |
|---|---|---|---|---|---|
| 1 | Alice | 5 | 14% | 14% - 8% = 6% | $600 |
| 2 | Bob | 3 | 10% | 0% (10% < 14%) | $0 |
| 3 | Carol | 7 | 17% | 17% - 14% = 3% | $300 |
| 4 | Dave | 7 | 17% | 0% (same rate) | $0 |
| 5 | Eve | 10 | 19.5% | 19.5% - 17% = 2.5% | $250 |
Total distributed: $1,150 (11.5%)
Key Points:
- Bob and Dave earn $0 because their rates are not higher than the previous earning partner
- Commission "jumps" to higher-ranked partners in the upline
- Processing continues until 20% max rate is reached or upline ends
6. Balance Updates
Balance Record Structure (USD):
{
"partnerId": "uuid",
"availableBalanceUsd": 4500.0,
"pendingBalanceUsd": 1500.0,
"totalEarnedUsd": 12000.0,
"totalWithdrawnUsd": 6000.0,
"incomePersonalSalesUsd": 3000.0,
"incomeTeamSalesUsd": 2500.0,
"incomeRepeatSalesUsd": 1000.0,
"incomePortfolioReturnsUsd": 800.0,
"incomeClientProfitsUsd": 1200.0,
"incomeNetworkProfitsUsd": 900.0,
"incomeLeadershipPoolUsd": 2600.0,
"currency": "USD",
"version": 42
}Locking Pattern:
-- Lock ALL affected balances in consistent order
SELECT id FROM mlm.partner_balances
WHERE partner_id = ANY($1::uuid[])
ORDER BY partner_id
FOR UPDATE;7. Pending to Available Transition
Holding Periods by Source Type:
| Source Type | Holding Period | Reason |
|---|---|---|
| Product Order | 14 days | Refund/return window |
| Investment | 7 days | Fund verification |
| Passive Income | 7 days | Profit confirmation |
| Leadership Pool | 0 days | Immediate distribution |
Confirmation Job (Scheduled Daily):
async function confirmPendingCommissions() {
const eligibleCommissions = await db.commissionTransaction.findMany({
where: {
status: "PENDING",
createdAt: { lt: getHoldingPeriodCutoff() },
},
});
const byPartner = groupBy(eligibleCommissions, "partnerId");
for (const [partnerId, commissions] of Object.entries(byPartner)) {
await db.$transaction(async (tx) => {
await tx.$queryRaw`
SELECT 1 FROM mlm.partner_balances
WHERE partner_id = ${partnerId}::uuid
FOR UPDATE
`;
const totalAmountUsd = commissions.reduce(
(sum, c) => sum + c.netAmountUsd,
0,
);
await tx.commissionTransaction.updateMany({
where: { id: { in: commissions.map((c) => c.id) } },
data: { status: "APPROVED", processedAt: new Date() },
});
await tx.partnerBalance.update({
where: { partnerId },
data: {
pendingBalanceUsd: { decrement: totalAmountUsd },
availableBalanceUsd: { increment: totalAmountUsd },
version: { increment: 1 },
},
});
});
}
}8. Commission Reversal
Reversal Triggers:
| Trigger | Source | Action |
|---|---|---|
| Order refund | Customer requested | Reverse all related commissions |
| Chargeback | Payment provider | Reverse + flag partner |
| Fraud detection | Admin/System | Reverse and flag |
| Investment cancellation | Customer/Admin | Reverse all related |
Reversal Process:
async function reverseCommissions(
sourceType: string,
sourceId: string,
reason: string,
adminId: string,
) {
const commissions = await db.commissionTransaction.findMany({
where: {
sourceType,
sourceId,
status: { in: ["PENDING", "APPROVED", "PAID"] },
},
});
for (const commission of commissions) {
await db.$transaction(async (tx) => {
await tx.$queryRaw`
SELECT 1 FROM mlm.partner_balances
WHERE partner_id = ${commission.partnerId}::uuid
FOR UPDATE
`;
if (commission.status === "PENDING") {
await tx.partnerBalance.update({
where: { partnerId: commission.partnerId },
data: {
pendingBalanceUsd: { decrement: commission.netAmountUsd },
},
});
} else {
await tx.commissionTransaction.create({
data: {
partnerId: commission.partnerId,
incomeType: commission.incomeType,
sourceType: "CLAWBACK",
sourceId,
grossAmountUsd: -commission.grossAmountUsd,
netAmountUsd: -commission.netAmountUsd,
status: "CLAWBACK",
reversedFromId: commission.id,
reversalReason: reason,
reversedBy: adminId,
},
});
await tx.partnerBalance.update({
where: { partnerId: commission.partnerId },
data: {
availableBalanceUsd: { decrement: commission.netAmountUsd },
},
});
}
await tx.commissionTransaction.update({
where: { id: commission.id },
data: {
status: "REVERSED",
reversalReason: reason,
reversedAt: new Date(),
reversedBy: adminId,
},
});
});
}
}9. Leadership Pool Distribution
Pool Configuration:
| Pool | Ranks | % of Turnover | Frequency | Qualification |
|---|---|---|---|---|
| POOL_5 | 5, 5_PRO | 1% | Weekly | $5,000 / $10,000 weekly volume |
| POOL_6 | 6, 6_PRO | 0.5% | Weekly | $20,000 / $30,000 weekly volume |
| POOL_7 | 7, 7_PRO | 0.5% | Weekly | $45,000 / $60,000 weekly volume |
| POOL_8 | 8, 8_PRO | 0.5% | Weekly | $90,000 / $120,000 weekly volume |
| POOL_9 | 9, 9_PRO | 1% | Monthly | Rank achievement only |
| POOL_10 | 10, 10_PRO | 1% | Monthly | Rank achievement only |
| POOL_11 | 11, 11_PRO | 1% | Monthly | Rank achievement only |
Note: Pools 9-11 do not require separate volume qualification. Partners who achieve and maintain these ranks automatically participate in monthly distribution.
50% Branch Rule (Pools 5-8 only):
async function findQualifiedPartners(
pool: LeadershipPool,
periodStart: Date,
periodEnd: Date,
) {
const candidates = await db.partner.findMany({
where: {
status: "ACTIVE",
rank: { code: { in: pool.eligibleRanks } },
},
});
const qualifiedPartners = [];
for (const partner of candidates) {
const requiredVolume = pool.qualificationVolumes[partner.rank.code];
const branchVolumes = await calculateBranchVolumes(
partner.id,
periodStart,
periodEnd,
);
// Apply 50% cap per branch
const maxPerBranch = requiredVolume * 0.5;
const cappedTotal = Object.values(branchVolumes).reduce(
(sum, vol) => sum + Math.min(vol, maxPerBranch),
0,
);
if (cappedTotal >= requiredVolume) {
qualifiedPartners.push({
...partner,
qualificationVolume: cappedTotal,
branchVolumes,
});
}
}
return qualifiedPartners;
}Distribution Process:
async function distributeLeadershipPool(
poolCode: string,
periodStart: Date,
periodEnd: Date,
) {
const pool = await db.leadershipPool.findUnique({ where: { poolCode } });
const totalTurnover = await calculatePeriodTurnover(periodStart, periodEnd);
const poolAmountUsd = totalTurnover * pool.percentageOfTurnover;
const qualifiedPartners = await findQualifiedPartners(
pool,
periodStart,
periodEnd,
);
if (qualifiedPartners.length === 0) return { distributed: false };
const perPersonAmountUsd = poolAmountUsd / qualifiedPartners.length;
for (const partner of qualifiedPartners) {
await db.$transaction(async (tx) => {
await tx.commissionTransaction.create({
data: {
partnerId: partner.id,
incomeType: "LEADERSHIP_POOL",
sourceType: "POOL_DISTRIBUTION",
sourceId: distribution.id,
grossAmountUsd: perPersonAmountUsd,
netAmountUsd: perPersonAmountUsd,
currency: "USD",
status: "APPROVED", // Immediate for pools
},
});
// Add directly to available balance (no pending period)
await tx.partnerBalance.update({
where: { partnerId: partner.id },
data: {
availableBalanceUsd: { increment: perPersonAmountUsd },
totalEarnedUsd: { increment: perPersonAmountUsd },
incomeLeadershipPoolUsd: { increment: perPersonAmountUsd },
},
});
});
}
return {
distributed: true,
poolAmountUsd,
participants: qualifiedPartners.length,
};
}10. Payout Eligibility
Eligibility Criteria:
| Criterion | Condition | Error Code |
|---|---|---|
| KYC Status | = 'APPROVED' | KYC_REQUIRED |
| Available Balance | >= requested amount | INSUFFICIENT_BALANCE |
| Min Payout | amount >= $100 USD | BELOW_MINIMUM |
| Pending Payouts | No pending/processing payouts | PAYOUT_PENDING |
| Partner Status | = 'ACTIVE' | PARTNER_INACTIVE |
| Payout Method | At least one configured | NO_PAYOUT_METHOD |
Payout Creation:
SELECT * FROM mlm.create_payout_request(
p_partner_id := $1,
p_amount_usd := $2,
p_method_type := $3,
p_payout_details := $4
);Payout Status Flow:
PENDING -> APPROVED -> PROCESSING -> COMPLETED
| | |
v v v
CANCELLED REJECTED FAILED
|
v
Balance restoredCommission Transaction States
| State | Description | Balance Location |
|---|---|---|
| PENDING | Calculated, awaiting holding period | pending_balance_usd |
| APPROVED | Holding period complete | available_balance_usd |
| PAID | Included in completed payout | total_withdrawn_usd |
| HELD | On hold for review | pending_balance_usd |
| REVERSED | Reversed due to refund | Deducted |
| CLAWBACK | Negative transaction | Deducted from available |
| CANCELLED | Cancelled before confirmation | N/A |
Error Scenarios
Job Processing Errors
| Error | Handling | Retry |
|---|---|---|
| Source not found | Log and fail permanently | No |
| Partner not found | Log and fail permanently | No |
| Database connection lost | Retry with backoff | Yes |
| Lock timeout | Retry immediately | Yes |
| Rate not found for rank | Log and fail permanently | No |
Balance Operation Errors
| Error | Handling |
|---|---|
| Negative balance attempt | Transaction rollback, alert |
| Version mismatch | Retry with fresh data |
| Deadlock detected | Retry (ordered locking prevents) |
Batch Processing Schedule
| Job | Frequency | Time | Description |
|---|---|---|---|
| Commission Approval | Daily | 02:00 UTC | Move pending to approved |
| Balance Settlement | Daily | 03:00 UTC | Move approved to available |
| Weekly Pool Distribution | Sunday | 23:00 UTC | Distribute Pools 5-8 |
| Monthly Pool Distribution | 1st of month | 01:00 UTC | Distribute Pools 9-11 |
| Rank Recalculation | Hourly | :00 | Check for rank promotions |
Monitoring and Alerts
Key Metrics:
| Metric | Alert Threshold |
|---|---|
| Job queue depth | > 1000 jobs |
| Failed jobs (24h) | > 10 |
| Dead letter queue | > 0 |
| Processing time | > 30 seconds per job |
| Reversal rate | > 5% of commissions |
Audit Log:
CREATE TABLE mlm.financial_audit_log (
id UUID PRIMARY KEY,
event_type VARCHAR(50),
partner_id UUID,
amount_usd DECIMAL(20,2),
balance_before_usd DECIMAL(20,2),
balance_after_usd DECIMAL(20,2),
income_type VARCHAR(50),
source_type VARCHAR(50),
source_id UUID,
checksum VARCHAR(64),
previous_checksum VARCHAR(64),
created_at TIMESTAMP DEFAULT NOW()
);Key Differences from Old System
| Aspect | Old System | New System |
|---|---|---|
| Commission Model | Unilevel (level-based %) | Differential (rank-based) |
| Depth | 10 levels max | Unlimited |
| Income Types | 3 | 7 |
| Currency | RUB | USD |
| Minimum Payout | 1,000 RUB | $100 USD |
| Leadership Pools | None | 7 pools (Rank 5+) |
| Rate Calculation | Fixed % per level | Differential between ranks |