Skip to content

Движок комиссий (Дифференциальная модель)

Платформа IWM использует Дифференциальную комиссионную модель с 7 типами дохода. Партнёры получают комиссии на основе разницы между своей ставкой ранга и ставкой ранга нижестоящего партнёра, с неограниченной глубиной.


Обзор комиссионной модели

СвойствоЗначение
Тип моделиДифференциальная (не Unilevel)
ГлубинаНеограниченная
Типы дохода7 (3 Активных + 3 Пассивных + 1 Пул)
Основная валютаUSD
Диапазон ставок3% - 20% в зависимости от ранга

Основная формула расчёта

ЕСЛИ ранг_консультанта > ранг_партнёра_источника:
    комиссия = (ставка_консультанта - ставка_источника) × сумма
ИНАЧЕ:
    комиссия = 0  // Нет комиссии, если ранг равен или ниже

7 типов дохода

Активный доход (Типы 1-3)

ТипНазваниеТриггерРасчёт
1Личные продажиПрямая продажа своему клиентуличная_ставка × сумма
2Командные продажиНижестоящий партнёр совершает продажуДифференциал от суммы продажи
3Повторные продажиСуществующий клиент делает повторную покупкуАналогично личным продажам

Пассивный доход (Типы 4-6)

ТипНазваниеТриггерРасчёт
4Доход портфеляСобственная инвестиция приносит прибыльДоходность по стратегии
5Прибыль клиентовЛичный клиент получает прибыльпассивная_ставка × прибыль_клиента
6Прибыль сетиКлиент нижестоящего получает прибыльДифференциал от суммы прибыли

Лидерский доход (Тип 7)

ТипНазваниеТриггерРасчёт
7Лидерский пулЕженедельное/ежемесячное распределениеРавная доля среди квалифицированных

Поток расчёта

                    Транзакция завершена


                    ┌─────────────────────┐
                    │ Определить тип      │
                    │ дохода (1-7)        │
                    └──────────┬──────────┘


                    ┌─────────────────────┐
                    │ Получить реферальную│
                    │ атрибуцию           │
                    └──────────┬──────────┘


                    ┌─────────────────────┐
                    │ Получить верхнюю    │
                    │ линию (без лимита)  │
                    └──────────┬──────────┘


              ┌────────────────────────────────┐
              │ Для каждого партнёра в линии:  │
              │  1. Получить ранг и ставку     │
              │  2. Сравнить со ставкой источн.│
              │  3. Рассчитать дифференциал    │
              │  4. Создать запись транзакции  │
              │  5. Обновить ожидающий баланс  │
              └────────────────────────────────┘


                    ┌─────────────────────┐
                    │ Отправить события   │
                    │ расчёта комиссий    │
                    └─────────────────────┘

Процессор дифференциальных комиссий

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;
}

async function processDifferentialCommission(
  job: DifferentialCommissionPayload,
) {
  const {
    idempotencyKey,
    sourceId,
    sourcePartnerId,
    sourcePartnerRate,
    amountUsd,
  } = job;

  // 1. Проверка идемпотентности
  const existing = await db.idempotencyKey.findUnique({
    where: { key: idempotencyKey },
  });
  if (existing) return existing.response;

  // 2. Обработка с правильной блокировкой
  return await db.$transaction(
    async (tx) => {
      // 2a. Получить ВСЮ верхнюю линию (неограниченная глубина)
      const upline = await tx.$queryRaw`
      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 = ${sourcePartnerId}::uuid
        AND ptp.depth >= 1
      ORDER BY ptp.depth ASC
    `;

      if (upline.length === 0) {
        await saveIdempotencyKey(tx, idempotencyKey, { commissions: [] });
        return { commissions: [] };
      }

      // 2b. Заблокировать все балансы партнёров в согласованном порядке
      const partnerIds = upline.map((u) => u.partner_id).sort();
      await tx.$queryRaw`
      SELECT id FROM mlm.partner_balances
      WHERE partner_id = ANY(${partnerIds}::uuid[])
      ORDER BY partner_id
      FOR UPDATE
    `;

      // 2c. Рассчитать дифференциальные комиссии
      const commissions = [];
      let currentSourceRate = sourcePartnerRate;

      for (const ancestor of upline) {
        if (ancestor.status !== "ACTIVE") continue;

        // Получить применимую ставку в зависимости от типа дохода
        const ancestorRate = getApplicableRate(job.incomeType, ancestor);

        // Расчёт дифференциала
        const differentialRate = ancestorRate - currentSourceRate;

        if (differentialRate > 0) {
          const grossAmountUsd = amountUsd * (differentialRate / 100);
          const netAmountUsd = grossAmountUsd;

          // Создать запись комиссии
          const commission = await tx.commissionTransaction.create({
            data: {
              partnerId: ancestor.partner_id,
              incomeType: job.incomeType,
              sourceType: job.sourceType,
              sourceId,
              sourcePartnerId,
              ownRate: ancestorRate,
              sourceRate: currentSourceRate,
              differentialRate,
              grossAmountUsd,
              netAmountUsd,
              currency: "USD",
              status: "PENDING",
              idempotencyKey: `${idempotencyKey}:${ancestor.partner_id}`,
            },
          });

          // Обновить ожидающий баланс
          await tx.partnerBalance.update({
            where: { partnerId: ancestor.partner_id },
            data: {
              pendingBalanceUsd: { increment: netAmountUsd },
              version: { increment: 1 },
              lastCalculatedAt: new Date(),
              updatedAt: new Date(),
            },
          });

          commissions.push(commission);

          // Обновить текущую ставку источника для следующей итерации
          currentSourceRate = ancestorRate;
        }

        // Остановиться, если достигли максимальной ставки (20%)
        if (currentSourceRate >= 20) break;
      }

      await saveIdempotencyKey(tx, idempotencyKey, {
        commissionIds: commissions.map((c) => c.id),
      });

      return { commissions };
    },
    {
      isolationLevel: "Serializable",
      timeout: 30000,
    },
  );
}

function getApplicableRate(incomeType: string, partner: any): 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;
  }
}

Расчёт пассивного дохода

Тип 5: Прибыль клиентов

Когда прямой клиент партнёра получает прибыль от инвестиции:

typescript
async function processClientProfitCommission(
  clientId: string,
  profitAmountUsd: number,
  investmentId: string,
) {
  // Получить партнёра, который привёл этого клиента
  const attribution = await db.referralAttribution.findUnique({
    where: { userId: clientId },
  });

  if (!attribution) return;

  const partner = await db.partner.findUnique({
    where: { id: attribution.partnerId },
    include: { rank: true },
  });

  if (!partner || partner.status !== "ACTIVE") return;

  // Рассчитать пассивный доход
  const passiveRate = partner.rank.passiveIncomeRate;
  const commissionUsd = profitAmountUsd * (passiveRate / 100);

  // Создать транзакцию комиссии
  await createCommission({
    partnerId: partner.id,
    incomeType: "CLIENT_PROFITS",
    sourceType: "INVESTMENT_PROFIT",
    sourceId: investmentId,
    grossAmountUsd: commissionUsd,
    netAmountUsd: commissionUsd,
  });

  // Запустить расчёт сетевой прибыли для верхней линии
  await triggerNetworkProfitCalculation(partner.id, commissionUsd, passiveRate);
}

Тип 6: Прибыль сети (Дифференциал от пассивного дохода)

typescript
async function triggerNetworkProfitCalculation(
  sourcePartnerId: string,
  clientProfitAmount: number,
  sourceRate: number,
) {
  // Поставить задание в очередь для дифференциального расчёта сетевой прибыли
  await boss.send("network-profit-calculation", {
    idempotencyKey: `network-profit:${sourcePartnerId}:${Date.now()}`,
    incomeType: "NETWORK_PROFITS",
    sourceType: "INVESTMENT_PROFIT",
    sourcePartnerId,
    amountUsd: clientProfitAmount,
    sourcePartnerRate: sourceRate,
  });
}

Распределение лидерских пулов

Конфигурация пулов

ПулРанги% от оборотаЧастотаКвалификация
POOL_55, 5_PRO1%Еженедельно$5,000 / $10,000 недельный объём
POOL_66, 6_PRO0.5%Еженедельно$20,000 / $30,000 недельный объём
POOL_77, 7_PRO0.5%Еженедельно$45,000 / $60,000 недельный объём
POOL_88, 8_PRO0.5%Еженедельно$90,000 / $120,000 недельный объём
POOL_99, 9_PRO1%ЕжемесячноТолько достижение ранга
POOL_1010, 10_PRO1%ЕжемесячноТолько достижение ранга
POOL_1111, 11_PRO1%ЕжемесячноТолько достижение ранга

Примечание: Пулы 9-11 не требуют отдельного квалификационного объёма. Партнёры, достигшие и поддерживающие эти ранги, автоматически участвуют в ежемесячном распределении. Требование ранга по обороту ($10M-$800M) служит квалификационным порогом.

Процесс распределения

typescript
async function distributeLeadershipPool(
  poolCode: string,
  periodStart: Date,
  periodEnd: Date,
) {
  const pool = await db.leadershipPool.findUnique({
    where: { poolCode },
  });

  // 1. Рассчитать общий оборот компании за период
  const totalTurnover = await calculatePeriodTurnover(periodStart, periodEnd);
  const poolAmount = totalTurnover * pool.percentageOfTurnover;

  // 2. Найти квалифицированных участников
  const qualifiedPartners = await findQualifiedPartners(
    pool,
    periodStart,
    periodEnd,
  );

  if (qualifiedPartners.length === 0) {
    return { distributed: false, reason: "Нет квалифицированных участников" };
  }

  // 3. Рассчитать долю на человека
  const perPersonAmount = poolAmount / qualifiedPartners.length;

  // 4. Создать запись о распределении
  const distribution = await db.poolDistribution.create({
    data: {
      poolId: pool.id,
      periodStart,
      periodEnd,
      totalTurnoverUsd: totalTurnover,
      poolAmountUsd: poolAmount,
      qualifiedParticipants: qualifiedPartners.length,
      perPersonAmountUsd: perPersonAmount,
      status: "PROCESSING",
    },
  });

  // 5. Распределить каждому квалифицированному партнёру
  for (const partner of qualifiedPartners) {
    await db.$transaction(async (tx) => {
      // Создать запись участия
      await tx.poolParticipation.create({
        data: {
          distributionId: distribution.id,
          partnerId: partner.id,
          qualified: true,
          qualificationVolumeUsd: partner.qualificationVolume,
          amountReceivedUsd: perPersonAmount,
          branchVolumes: partner.branchVolumes,
        },
      });

      // Создать транзакцию комиссии
      await tx.commissionTransaction.create({
        data: {
          partnerId: partner.id,
          incomeType: "LEADERSHIP_POOL",
          sourceType: "POOL_DISTRIBUTION",
          sourceId: distribution.id,
          grossAmountUsd: perPersonAmount,
          netAmountUsd: perPersonAmount,
          currency: "USD",
          status: "APPROVED", // Распределения пулов мгновенны
        },
      });

      // Добавить напрямую в доступный баланс (без периода ожидания для пулов)
      await tx.partnerBalance.update({
        where: { partnerId: partner.id },
        data: {
          availableBalanceUsd: { increment: perPersonAmount },
          totalEarnedUsd: { increment: perPersonAmount },
          incomeLeadershipPoolUsd: { increment: perPersonAmount },
          version: { increment: 1 },
          updatedAt: new Date(),
        },
      });
    });
  }

  // 6. Отметить распределение как завершённое
  await db.poolDistribution.update({
    where: { id: distribution.id },
    data: {
      status: "DISTRIBUTED",
      distributedAt: new Date(),
    },
  });

  return {
    distributed: true,
    poolAmount,
    participants: qualifiedPartners.length,
    perPerson: perPersonAmount,
  };
}

Проверка правила 50% веток

typescript
async function findQualifiedPartners(
  pool: LeadershipPool,
  periodStart: Date,
  periodEnd: Date,
) {
  const eligibleRanks = pool.eligibleRanks;
  const qualificationVolumes = pool.qualificationVolumes as Record<
    string,
    number
  >;

  // Получить всех партнёров с подходящими рангами
  const candidates = await db.partner.findMany({
    where: {
      status: "ACTIVE",
      rank: { code: { in: eligibleRanks } },
    },
    include: { rank: true },
  });

  const qualifiedPartners = [];

  for (const partner of candidates) {
    const requiredVolume = qualificationVolumes[partner.rank.code];

    // Рассчитать объёмы веток
    const branchVolumes = await calculateBranchVolumes(
      partner.id,
      periodStart,
      periodEnd,
    );

    // Применить ограничение 50% на ветку
    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;
}

async function calculateBranchVolumes(
  partnerId: string,
  periodStart: Date,
  periodEnd: Date,
): Promise<Record<string, number>> {
  // Получить прямых рефералов (корни веток)
  const directReferrals = await db.partner.findMany({
    where: { sponsorId: partnerId },
  });

  const branchVolumes: Record<string, number> = {};

  for (const branch of directReferrals) {
    // Получить всех потомков этой ветки
    const branchDescendants = await db.partnerTreePath.findMany({
      where: { ancestorId: branch.id },
    });

    const descendantIds = branchDescendants.map((d) => d.descendantId);
    descendantIds.push(branch.id);

    // Суммировать оборот по этой ветке
    const turnover = await db.commissionTransaction.aggregate({
      where: {
        sourcePartnerId: { in: descendantIds },
        createdAt: { gte: periodStart, lte: periodEnd },
        incomeType: { in: ["PERSONAL_SALES", "REPEAT_SALES"] },
      },
      _sum: { grossAmountUsd: true },
    });

    branchVolumes[branch.id] = turnover._sum.grossAmountUsd || 0;
  }

  return branchVolumes;
}

Расписание пакетной обработки

ЗаданиеЧастотаВремяОписание
Подтверждение комиссийЕжедневно02:00 UTCПеревод из ожидающих в подтверждённые
Расчёт балансаЕжедневно03:00 UTCПеревод из подтверждённых в доступные
Распределение недельных пуловВоскресенье23:00 UTCРаспределение пулов 5-8
Распределение месячных пулов1-е число месяца01:00 UTCРаспределение пулов 9-11
Пересчёт ранговЕжечасно:00Проверка повышений рангов

Поток статусов комиссий

PENDING → APPROVED → PAID

 REVERSED/CLAWBACK (при возврате/чарджбэке)
СтатусОписаниеВлияние на баланс
PENDINGРассчитано, ожидает периода удержания+ожидающий_баланс
APPROVEDПериод удержания завершёножидающий → доступный
PAIDВ доступном балансе партнёраУже в доступном
REVERSEDЗаказ возвращён до выплаты-ожидающий_баланс
CLAWBACKЗаказ возвращён после выплаты-доступный_баланс

Связанные документы