Skip to content

Commission Lifecycle Flow

Complete lifecycle of commission calculation using the Differential Commission Model, from triggering event to payout.

Overview

The commission lifecycle includes:

  1. Triggering events (7 income types)
  2. Job creation and queuing
  3. Idempotency and duplicate prevention
  4. Upline retrieval (unlimited depth)
  5. Differential commission calculation
  6. Balance updates and transaction recording
  7. Pending to available transition
  8. Commission reversal handling
  9. Leadership pool distribution
  10. Payout eligibility and processing

Commission Model Summary

PropertyValue
Model TypeDifferential (not Unilevel)
DepthUnlimited
Income Types7 (3 Active + 3 Passive + 1 Pool)
Primary CurrencyUSD
Rate Range3% - 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 lower

Main Flow Diagram


Step Details

1. Triggering Events (7 Income Types)

Events that trigger commission calculation:

Income TypeTrigger EventSource TypeCalculation
1. Personal SalesDirect sale to own clientORDERpersonal_rate × amount
2. Team SalesDownline partner makes saleORDERDifferential on sale amount
3. Repeat SalesExisting client repurchasesORDERSame as Personal Sales
4. Portfolio ReturnsOwn investment profitINVESTMENT_PROFITStrategy returns
5. Client ProfitsPersonal client earns profitINVESTMENT_PROFITpassive_rate × client_profit
6. Network ProfitsDownline's client earns profitINVESTMENT_PROFITDifferential on profit
7. Leadership PoolWeekly/Monthly distributionPOOL_DISTRIBUTIONEqual share

Job Payload Structure:

typescript
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:

typescript
await boss.send("differential-commission", payload, {
  retryLimit: 5,
  retryDelay: 30000,
  retryBackoff: true,
  expireInMinutes: 60,
  singletonKey: payload.idempotencyKey,
});

Retry Schedule:

AttemptDelayTotal Time
1Immediate0
230 seconds30s
32 minutes2.5m
410 minutes12.5m
530 minutes42.5m
FailedN/AMove to dead-letter queue

3. Idempotency Check

Double-Check Pattern:

typescript
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:

sql
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_idrank_codepersonal_sales_ratedepth
partner-A5141
partner-B3102
partner-C6163

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:

typescript
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)

DepthPartnerRankRateDifferentialCommission
1Alice514%14% - 8% = 6%$600
2Bob310%0% (10% < 14%)$0
3Carol717%17% - 14% = 3%$300
4Dave717%0% (same rate)$0
5Eve1019.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):

json
{
  "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:

sql
-- 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 TypeHolding PeriodReason
Product Order14 daysRefund/return window
Investment7 daysFund verification
Passive Income7 daysProfit confirmation
Leadership Pool0 daysImmediate distribution

Confirmation Job (Scheduled Daily):

typescript
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:

TriggerSourceAction
Order refundCustomer requestedReverse all related commissions
ChargebackPayment providerReverse + flag partner
Fraud detectionAdmin/SystemReverse and flag
Investment cancellationCustomer/AdminReverse all related

Reversal Process:

typescript
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:

PoolRanks% of TurnoverFrequencyQualification
POOL_55, 5_PRO1%Weekly$5,000 / $10,000 weekly volume
POOL_66, 6_PRO0.5%Weekly$20,000 / $30,000 weekly volume
POOL_77, 7_PRO0.5%Weekly$45,000 / $60,000 weekly volume
POOL_88, 8_PRO0.5%Weekly$90,000 / $120,000 weekly volume
POOL_99, 9_PRO1%MonthlyRank achievement only
POOL_1010, 10_PRO1%MonthlyRank achievement only
POOL_1111, 11_PRO1%MonthlyRank 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):

typescript
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:

typescript
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:

CriterionConditionError Code
KYC Status= 'APPROVED'KYC_REQUIRED
Available Balance>= requested amountINSUFFICIENT_BALANCE
Min Payoutamount >= $100 USDBELOW_MINIMUM
Pending PayoutsNo pending/processing payoutsPAYOUT_PENDING
Partner Status= 'ACTIVE'PARTNER_INACTIVE
Payout MethodAt least one configuredNO_PAYOUT_METHOD

Payout Creation:

sql
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 restored

Commission Transaction States

StateDescriptionBalance Location
PENDINGCalculated, awaiting holding periodpending_balance_usd
APPROVEDHolding period completeavailable_balance_usd
PAIDIncluded in completed payouttotal_withdrawn_usd
HELDOn hold for reviewpending_balance_usd
REVERSEDReversed due to refundDeducted
CLAWBACKNegative transactionDeducted from available
CANCELLEDCancelled before confirmationN/A

Error Scenarios

Job Processing Errors

ErrorHandlingRetry
Source not foundLog and fail permanentlyNo
Partner not foundLog and fail permanentlyNo
Database connection lostRetry with backoffYes
Lock timeoutRetry immediatelyYes
Rate not found for rankLog and fail permanentlyNo

Balance Operation Errors

ErrorHandling
Negative balance attemptTransaction rollback, alert
Version mismatchRetry with fresh data
Deadlock detectedRetry (ordered locking prevents)

Batch Processing Schedule

JobFrequencyTimeDescription
Commission ApprovalDaily02:00 UTCMove pending to approved
Balance SettlementDaily03:00 UTCMove approved to available
Weekly Pool DistributionSunday23:00 UTCDistribute Pools 5-8
Monthly Pool Distribution1st of month01:00 UTCDistribute Pools 9-11
Rank RecalculationHourly:00Check for rank promotions

Monitoring and Alerts

Key Metrics:

MetricAlert 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:

sql
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

AspectOld SystemNew System
Commission ModelUnilevel (level-based %)Differential (rank-based)
Depth10 levels maxUnlimited
Income Types37
CurrencyRUBUSD
Minimum Payout1,000 RUB$100 USD
Leadership PoolsNone7 pools (Rank 5+)
Rate CalculationFixed % per levelDifferential between ranks