Files
docs/business/balance-calculation-rules.md
T

45 KiB
Raw Blame History

Balance Calculation with Carryover Logic - Complete Guide

Date: 2025-12-01
Last Updated: 2025-12-09 ( اصلاح نهایی: محاسبات تعادل و فلش)
Status: Fully Implemented & Verified
Migration: UpdateNetworkWeeklyBalanceWithCarryover


آخرین به‌روزرسانی (2025-12-09)

تغییرات اعمال شده:

کد محاسبه تعادل با توضیحات دقیق بیزینس تطبیق داده شد:

  1. ترتیب محاسبات اصلاح شد:

    • اول تعادل اولیه محاسبه می‌شود
    • بعد باقیمانده (برای هفته بعد)
    • سپس سقف 300 اعمال می‌شود
    • در نهایت فلش محاسبه می‌شود
  2. فلش از هر دو طرف:

    • اگر تعادل > 300 باشد
    • از چپ: (تعادل - 300) فلش می‌شود
    • از راست: (تعادل - 300) فلش می‌شود
    • جمع فلش = (تعادل - 300) × 2
  3. باقیمانده جداگانه ذخیره می‌شود:

    • LeftLegRemainder: باقیمانده دست چپ
    • RightLegRemainder: باقیمانده دست راست

📋 قوانین اصلی بیزینس

توضیح منطق فعلی (اشتباه) منطق صحیح
سقف 300 کل 300 برای هر دست
حداکثر تعادل 300 MIN(300, 300) = 300
حداکثر کل 300 300 + 300 = 600 (مجموع دو دست)

تفاوت در محاسبه:

منطق فعلی (اشتباه):

totalBalances = MIN(leftTotal, rightTotal)
cappedBalances = MIN(totalBalances, 300)  // ← سقف روی کل

منطق صحیح:

cappedLeftTotal = MIN(leftTotal, 300)   // ← سقف روی هر دست
cappedRightTotal = MIN(rightTotal, 300)
totalBalances = MIN(cappedLeftTotal, cappedRightTotal)

مثال عملی:

سناریو چپ راست منطق فعلی منطق صحیح
1 200 250 200 200
2 350 400 300 300
3 500 600 300 300

توجه: در مثال‌های بالا نتیجه یکسان است چون حداکثر یک تعادل همیشه MIN(300,300)=300 است. تفاوت در باقیمانده است:

مثال با چپ=500، راست=600:

روش تعادل باقیمانده چپ باقیمانده راست
فعلی 300 500 - 150 = 350 600 - 150 = 450
صحیح 300 200 (500-300) 300 (600-300)

تغییرات Configuration:

// ✅ تغییر نام و مقدار:
// قدیمی:
Key = "Commission.MaxWeeklyBalancesPerUser", Value = "300"

// جدید:
Key = "Commission.MaxWeeklyBalancesPerLeg", Value = "300"

📋 Configuration-Based Calculation

System Configurations Used:

// تمام مقادیر از جدول SystemConfigurations خوانده می‌شوند
Club.ActivationFee = 25,000,000 ریال (هزینه فعال‌سازی)
Commission.WeeklyPoolContributionPercent = 20% (سهم استخر)
Commission.MaxWeeklyBalancesPerLeg = 300 ( سقف امتیاز نهایی)

نکته مهم: سقف 300 روی امتیاز نهایی اعمال می‌شود، نه روی تعادل اولیه!

Pool Contribution Calculation:

totalNewMembers = leftNewMembers + rightNewMembers
weeklyPoolContribution = totalNewMembers × activationFee × poolPercent
                       = totalNewMembers × 25,000,000 × 20%
                       = totalNewMembers × 5,000,000

مثال:
اگر 10 نفر جدید جذب شوند: 10 × 5,000,000 = 50,000,000 ریال به استخر اضافه می‌شود.


🚫 MaxWeeklyBalances Cap (محدودیت سقف 300)

Logic صحیح (به‌روز شده 2025-12-09):

// ✅ مرحله 1: محاسبه تعادل اولیه (بدون سقف)
totalBalances = MIN(leftTotal, rightTotal)

// ✅ مرحله 2: محاسبه باقیمانده برای هفته بعد
leftRemainder = leftTotal - totalBalances
rightRemainder = rightTotal - totalBalances

// ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
cappedBalances = MIN(totalBalances, 300)

// ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
flushedPerSide = totalBalances - cappedBalances
totalFlushed = flushedPerSide × 2

Example (مثال کامل):

leftTotal = 500, rightTotal = 600

مرحله 1️⃣: تعادل اولیه
  totalBalances = MIN(500, 600) = 500 ✅

مرحله 2️⃣: باقیمانده برای هفته بعد
  leftRemainder = 500 - 500 = 0 ✅
  rightRemainder = 600 - 500 = 100 ✅

مرحله 3️⃣: اعمال سقف
  cappedBalances = MIN(500, 300) = 300 ✅

مرحله 4️⃣: محاسبه فلش
  flushedPerSide = 500 - 300 = 200
  از چپ: 200 فلش می‌شود
  از راست: 200 فلش می‌شود
  totalFlushed = 200 × 2 = 400 ✅

نتیجه نهایی:
  ✅ امتیاز این هفته: 300
  ✅ باقیمانده چپ: 0
  ✅ باقیمانده راست: 100
  ✅ جمع فلش: 400 (از بین می‌رود)

مقایسه منطق قدیم vs جدید:

// ❌ منطق قدیم (اشتباه):
cappedBalances = MIN(totalBalances, 300)  // سقف روی کل
balancesConsumedPerSide = cappedBalances / 2
leftRemainder = leftTotal - balancesConsumedPerSide

// ✅ منطق جدید (صحیح):
cappedLeftTotal = MIN(leftTotal, 300)     // سقف روی هر دست
cappedRightTotal = MIN(rightTotal, 300)
totalBalances = MIN(cappedLeftTotal, cappedRightTotal)
leftRemainder = leftTotal - cappedLeftTotal  // باقیمانده از سقف هر دست

📊 Problem Statement

Previous (Incorrect) Logic:

// محاسبه تعداد کل اعضا در هر پا
##  **Current (Correct) Logic - Updated 2025-12-09:**

### **Formula (4 مرحله):**

// مرحله 1: جمع با هفته قبل leftTotal = leftNewMembers + leftCarryover rightTotal = rightNewMembers + rightCarryover

// مرحله 2: محاسبه تعادل اولیه totalBalances = MIN(leftTotal, rightTotal)

// مرحله 3: محاسبه باقیمانده برای هفته بعد leftRemainder = leftTotal - totalBalances rightRemainder = rightTotal - totalBalances

// مرحله 4: اعمال سقف 300 cappedBalances = MIN(totalBalances, 300) flushedPerSide = totalBalances - cappedBalances totalFlushed = flushedPerSide × 2


### **Key Principles:**
1. **Only count NEW members** activated in current week
2. **Add carryover** from previous week (جداگانه چپ و راست)
3. **Calculate remainder** for next week (قبل از سقف)
4. **Apply cap 300** on final score (بعد از تعادل)
5. **Flush from both sides** if balance > 300
6. **Recursive counting** through entire tree structure
leftTotal = leftNewMembers + leftCarryover
rightTotal = rightNewMembers + rightCarryover

TotalBalances = MIN(leftTotal, rightTotal)

leftRemainder = leftTotal - TotalBalances
rightRemainder = rightTotal - TotalBalances

Key Principles:

  1. Only count NEW members activated in current week
  2. Add carryover from previous week
  3. Calculate remainder for next week
  4. Recursive counting through entire tree structure

🔢 Example Calculations

Week 1 (2025-W48):

Tree Structure:

User A (Activated this week - 25M to pool)
├─ Left: User B (Activated this week - 25M)
└─ Right: User C (Activated this week - 25M)

Calculations:

User A:
  leftNewMembers = 1 (User B activated)
  rightNewMembers = 1 (User C activated)
  leftCarryover = 0 (first week)
  rightCarryover = 0 (first week)
  
  leftTotal = 1 + 0 = 1
  rightTotal = 1 + 0 = 1
  
  TotalBalances = MIN(1, 1) = 1
  
  leftRemainder = 1 - 1 = 0
  rightRemainder = 1 - 1 = 0

User B: TotalBalances = 0 (no children)
User C: TotalBalances = 0 (no children)

Pool Calculation:

Total Pool = 75M (3 activations × 25M)
Total Balances = 1 (only User A)
Value Per Balance = 75M ÷ 1 = 75M

Commission:
  User A = 1 × 75M = 75M

Week 2 (2025-W49):

Tree Structure:

User A
├─ Left: User B
│   ├─ Left: User D (NEW - activated this week - 25M)
│   └─ Right: User E (NEW - activated this week - 25M)
└─ Right: User C
    ├─ Left: User F (NEW - activated this week - 25M)
    └─ Right: User G (NEW - activated this week - 25M)

Calculations:

User B:
  leftNewMembers = 1 (User D)
  rightNewMembers = 1 (User E)
  leftCarryover = 0
  rightCarryover = 0
  
  leftTotal = 1 + 0 = 1
  rightTotal = 1 + 0 = 1
  TotalBalances = MIN(1, 1) = 1

User C:
  leftNewMembers = 1 (User F)
  rightNewMembers = 1 (User G)
  leftCarryover = 0
  rightCarryover = 0
  
  leftTotal = 1 + 0 = 1
  rightTotal = 1 + 0 = 1
  TotalBalances = MIN(1, 1) = 1

User A:
  leftNewMembers = 2 (D & E through B)
  rightNewMembers = 2 (F & G through C)
  leftCarryover = 0 (from week 1)
  rightCarryover = 0 (from week 1)
  
  leftTotal = 2 + 0 = 2
  rightTotal = 2 + 0 = 2
  TotalBalances = MIN(2, 2) = 2 ✅
  
  leftRemainder = 2 - 2 = 0
  rightRemainder = 2 - 2 = 0

Pool Calculation:

Total Pool = 100M (4 new activations × 25M)
Total Balances = 4 (A=2, B=1, C=1)
Value Per Balance = 100M ÷ 4 = 25M

Commission:
  User A = 2 × 25M = 50M ✅ (not 33.33M!)
  User B = 1 × 25M = 25M
  User C = 1 × 25M = 25M

Week 3 (2025-W50) - With Carryover:

Tree Structure:

User A
├─ Left: User B
│   ├─ Left: User D
│   │   └─ Left: User H (NEW - 25M)
│   └─ Right: User E
└─ Right: User C
    ├─ Left: User F
    └─ Right: User G

Calculations:

User D:
  leftNewMembers = 1 (User H)
  rightNewMembers = 0
  leftCarryover = 0
  rightCarryover = 0
  
  leftTotal = 1 + 0 = 1
  rightTotal = 0 + 0 = 0
  TotalBalances = MIN(1, 0) = 0
  
  leftRemainder = 1 - 0 = 1 ⚠️ (saved for next week)
  rightRemainder = 0 - 0 = 0

User B:
  leftNewMembers = 1 (H through D)
  rightNewMembers = 0
  leftCarryover = 0 (from week 2)
  rightCarryover = 0
  
  leftTotal = 1 + 0 = 1
  rightTotal = 0 + 0 = 0
  TotalBalances = MIN(1, 0) = 0
  
  leftRemainder = 1 - 0 = 1 ⚠️ (saved for next week)
  rightRemainder = 0 - 0 = 0

User A:
  leftNewMembers = 1 (H through B→D)
  rightNewMembers = 0
  leftCarryover = 0 (from week 2)
  rightCarryover = 0
  
  leftTotal = 1 + 0 = 1
  rightTotal = 0 + 0 = 0
  TotalBalances = MIN(1, 0) = 0
  
  leftRemainder = 1 - 0 = 1 ⚠️ (saved for next week)
  rightRemainder = 0 - 0 = 0

Pool Calculation:

Total Pool = 25M (1 new activation)
Total Balances = 0 (no balanced pairs)
Value Per Balance = N/A

Commission: None this week
Carryover: User A, B, D each have 1 leftRemainder for week 4

🔄 Database Schema

NetworkWeeklyBalance Table:

ALTER TABLE NetworkWeeklyBalances ADD:
  -- New members this week
  LeftLegNewMembers INT NOT NULL DEFAULT 0,
  RightLegNewMembers INT NOT NULL DEFAULT 0,
  
  -- Carryover from previous week
  LeftLegCarryover INT NOT NULL DEFAULT 0,
  RightLegCarryover INT NOT NULL DEFAULT 0,
  
  -- Totals (new + carryover)
  LeftLegTotal INT NOT NULL DEFAULT 0,
  RightLegTotal INT NOT NULL DEFAULT 0,
  
  -- Remainder for next week
  LeftLegRemainder INT NOT NULL DEFAULT 0,
  RightLegRemainder INT NOT NULL DEFAULT 0

Deprecated Fields:

  • LeftLegBalances (still exists for backward compatibility)
  • RightLegBalances (still exists for backward compatibility)

💻 Implementation

Handler: CalculateWeeklyBalancesCommandHandler.cs

public async Task<int> Handle(CalculateWeeklyBalancesCommand request, CancellationToken cancellationToken)
{
    // 1. Load previous week's carryover
    var previousWeekNumber = GetPreviousWeekNumber(request.WeekNumber);
    var previousWeekCarryovers = await _context.NetworkWeeklyBalances
        .Where(x => x.WeekNumber == previousWeekNumber)
        .ToDictionaryAsync(x => x.UserId, x => new { x.LeftLegRemainder, x.RightLegRemainder });

    // 2. For each user in network
    foreach (var user in usersInNetwork)
    {
        // Get carryover
        var leftCarryover = previousWeekCarryovers.ContainsKey(user.Id) 
            ? previousWeekCarryovers[user.Id].LeftLegRemainder : 0;
        var rightCarryover = previousWeekCarryovers.ContainsKey(user.Id) 
            ? previousWeekCarryovers[user.Id].RightLegRemainder : 0;

        // Count NEW members (activated in this week)
        var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekNumber);
        var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekNumber);

        // Calculate totals
        var leftTotal = leftNewMembers + leftCarryover;
        var rightTotal = rightNewMembers + rightCarryover;

        // Calculate balance (min)
        var totalBalances = Math.Min(leftTotal, rightTotal);

        // Calculate remainder
        var leftRemainder = leftTotal - totalBalances;
        var rightRemainder = rightTotal - totalBalances;

        // Save to database
        var balance = new NetworkWeeklyBalance
        {
            UserId = user.Id,
            WeekNumber = request.WeekNumber,
            LeftLegNewMembers = leftNewMembers,
            RightLegNewMembers = rightNewMembers,
            LeftLegCarryover = leftCarryover,
            RightLegCarryover = rightCarryover,
            LeftLegTotal = leftTotal,
            RightLegTotal = rightTotal,
            TotalBalances = totalBalances,
            LeftLegRemainder = leftRemainder,
            RightLegRemainder = rightRemainder,
            // ...
        };
    }
}

private async Task<int> CountNewMembersRecursive(long userId, NetworkLeg leg, DateTime startDate, DateTime endDate)
{
    var child = await _context.Users
        .FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg);

    if (child == null) return 0;

    var count = 0;

    // Check if activated in this week
    var membership = await _context.ClubMemberships
        .FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive);

    if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate)
    {
        count = 1;
    }

    // Recursively count children
    var childLeft = await CountNewMembersRecursive(child.Id, NetworkLeg.Left, startDate, endDate);
    var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate);

    return count + childLeft + childRight;
}

📝 Key Points

  1. Only NEW activations count - filtered by ActivatedAt date
  2. Carryover persists - unused balances roll over to next week
  3. Recursive counting - includes entire subtree under each leg
  4. Week date ranges - ISO 8601 week format (Saturday to Friday)
  5. Idempotent - can recalculate with ForceRecalculate flag

🚀 Benefits

  1. Fair commission distribution - rewards balanced growth
  2. No lost balances - carryover ensures nothing is wasted
  3. Accurate tracking - distinguishes new vs existing members
  4. Scalable - works for large networks with recursive algorithm
  5. Auditable - full history of calculations in database

📞 Reference

  • Source Code: CMSMicroservice.Application/CommissionCQ/Commands/CalculateWeeklyBalances/
  • Migration: 20251201144400_UpdateNetworkWeeklyBalanceWithCarryover
  • Entity: CMSMicroservice.Domain/Entities/Network/NetworkWeeklyBalance.cs
  • Discussion: Telegram chat with Dr. Seif (2025-12-01)

Status: Production Ready
Last Updated: 2025-12-01


ضمیمه: مثال‌های محاسبه ۵ سطحی

📊 مثال‌های عملی محاسبه تعادل - 5 لول عمقی

تاریخ: 2025-12-09
وضعیت: مثال‌های کامل و تایید شده
هدف: نمایش محاسبات واقعی برای درخت باینری تا 5 لول


🌳 ساختار درخت نمونه

                    User1 (Level 0)
                    /              \
            User2 (L1-L)          User3 (L1-R)
            /         \           /         \
    User4(L2-LL)  User5(L2-LR)  User6(L2-RL)  User7(L2-RR)
    /     \       /     \       /     \        /     \
U8(L3)  U9(L3) U10(L3) U11(L3) U12(L3) U13(L3) U14(L3) U15(L3)
/  \    /  \   /  \    /  \    /  \    /  \    /  \    /  \
U16-U31 (Level 4 - 16 users)
/\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\  /\
U32-U63 (Level 5 - 32 users)

📋 داده‌های ورودی

فرضیات:

  • هفته فعلی: 2025-W50
  • سقف امتیاز: 300
  • تعداد کل کاربران: 63 نفر (6 لول: 1+2+4+8+16+32)
  • وضعیت: همه کاربران فعال هستند (عضو باشگاه)

🎯 محاسبات Level 5 (پایین‌ترین سطح)

User 32-63 (32 کاربر Leaf):

هیچ زیرمجموعه‌ای ندارند
چپ = 0، راست = 0
تعادل = MIN(0, 0) = 0
امتیاز = 0
باقیمانده چپ = 0
باقیمانده راست = 0
فلش = 0

خلاصه Level 5: تمام 32 کاربر → 0 امتیاز


🎯 محاسبات Level 4 (User 16-31)

User 16:

زیرمجموعه:

  • چپ: User 32 (1 نفر)
  • راست: User 33 (1 نفر)

محاسبات:

چپ = 1، راست = 1
تعادل اولیه = MIN(1, 1) = 1
باقیمانده چپ = 1 - 1 = 0
باقیمانده راست = 1 - 1 = 0
امتیاز نهایی = MIN(1, 300) = 1 ✅
فلش = 0

User 17:

زیرمجموعه:

  • چپ: User 34 (1 نفر)
  • راست: User 35 (1 نفر)

محاسبات: مشابه User 16

امتیاز = 1 ✅

User 18-31 (14 کاربر دیگه):

همه مشابه User 16 → هر کدام 1 امتیاز

خلاصه Level 4: تمام 16 کاربر → هر کدام 1 امتیاز = 16 امتیاز


🎯 محاسبات Level 3 (User 8-15)

User 8:

زیرمجموعه:

  • چپ: User 16 (1 نفر)
  • راست: User 17 (1 نفر)

محاسبات:

چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅

User 9:

زیرمجموعه:

  • چپ: User 18 (1 نفر)
  • راست: User 19 (1 نفر)

محاسبات: مشابه User 8

امتیاز = 1 ✅

User 10-15 (6 کاربر دیگه):

همه مشابه → هر کدام 1 امتیاز

خلاصه Level 3: تمام 8 کاربر → هر کدام 1 امتیاز = 8 امتیاز


🎯 محاسبات Level 2 (User 4-7)

User 4:

زیرمجموعه:

  • چپ: User 8 (1 نفر)
  • راست: User 9 (1 نفر)

محاسبات:

چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅

User 5:

زیرمجموعه:

  • چپ: User 10 (1 نفر)
  • راست: User 11 (1 نفر)

محاسبات: مشابه User 4

امتیاز = 1 ✅

User 6, 7:

همه مشابه → هر کدام 1 امتیاز

خلاصه Level 2: تمام 4 کاربر → هر کدام 1 امتیاز = 4 امتیاز


🎯 محاسبات Level 1 (User 2-3)

User 2:

زیرمجموعه:

  • چپ: User 4 (1 نفر)
  • راست: User 5 (1 نفر)

محاسبات:

چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅

User 3:

زیرمجموعه:

  • چپ: User 6 (1 نفر)
  • راست: User 7 (1 نفر)

محاسبات: مشابه User 2

امتیاز = 1 ✅

خلاصه Level 1: تمام 2 کاربر → هر کدام 1 امتیاز = 2 امتیاز


🎯 محاسبات Level 0 (User 1 - Root)

User 1:

زیرمجموعه:

  • چپ: User 2 (1 نفر)
  • راست: User 3 (1 نفر)

محاسبات:

چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅

خلاصه Level 0: User 1 → 1 امتیاز


📊 جمع کل سیستم

Level تعداد کاربران امتیاز هر کاربر جمع امتیازهای Level
5 32 0 0
4 16 1 16
3 8 1 8
2 4 1 4
1 2 1 2
0 1 1 1
جمع 63 - 31 امتیاز

💰 محاسبه صندوق

داده‌های ورودی:

تعداد کاربران فعال شده این هفته: 63 نفر
هزینه فعال‌سازی هر نفر: 25,000,000 ریال
درصد سهم استخر: 20%

جمع ورودی استخر = 63 × 25,000,000 × 20%
                  = 63 × 5,000,000
                  = 315,000,000 ریال

محاسبه ارزش هر امتیاز:

مجموع امتیازهای سیستم = 31
جمع استخر = 315,000,000 ریال

ارزش هر امتیاز = 315,000,000 ÷ 31
               = 10,161,290 ریال (تقریباً)

توزیع کمیسیون:

User 1: 1 × 10,161,290 = 10,161,290 ریال
User 2: 1 × 10,161,290 = 10,161,290 ریال
User 3: 1 × 10,161,290 = 10,161,290 ریال
User 4-7: 4 × 10,161,290 = 40,645,160 ریال
User 8-15: 8 × 10,161,290 = 81,290,320 ریال
User 16-31: 16 × 10,161,290 = 162,580,640 ریال
User 32-63: 0 ریال (امتیازی ندارند)

جمع کل پرداختی = 315,000,000 ریال ✅

🔥 مثال پیچیده‌تر: سناریو نامتعادل

تغییر ساختار:

User 1:
  چپ: 500 نفر (عمق زیاد)
  راست: 600 نفر (عمق بیشتر)

محاسبات User 1:

مرحله 1️⃣: تعادل اولیه
  چپ = 500، راست = 600
  تعادل = MIN(500, 600) = 500

مرحله 2️⃣: باقیمانده
  باقی چپ = 500 - 500 = 0
  باقی راست = 600 - 500 = 100 → هفته بعد

مرحله 3️⃣: اعمال سقف
  امتیاز = MIN(500, 300) = 300 ✅

مرحله 4️⃣: فلش
  فلش از چپ = 500 - 300 = 200
  فلش از راست = 500 - 300 = 200
  جمع فلش = 400 (از بین می‌رود)

نتیجه:

✅ امتیاز User 1: 300
✅ باقیمانده راست: 100 (می‌رود هفته بعد)
✅ باقیمانده چپ: 0
✅ فلش شده: 400 (از بین رفته)

🔄 مثال با Carryover (هفته بعد)

فرض: User 1 در هفته 2025-W51:

باقیمانده هفته قبل:
  چپ: 0
  راست: 100

جدیدهای این هفته:
  چپ: 250
  راست: 150

محاسبات:

مرحله 1️⃣: جمع با هفته قبل
  چپ کل = 0 + 250 = 250
  راست کل = 100 + 150 = 250

مرحله 2️⃣: تعادل
  تعادل = MIN(250, 250) = 250

مرحله 3️⃣: باقیمانده
  باقی چپ = 250 - 250 = 0
  باقی راست = 250 - 250 = 0

مرحله 4️⃣: امتیاز
  امتیاز = MIN(250, 300) = 250 ✅

مرحله 5️⃣: فلش
  فلش = 0 (چون 250 < 300)

📈 مثال سقف: User با شبکه بزرگ

User A:

چپ: 800 نفر
راست: 900 نفر

محاسبات:

تعادل = MIN(800, 900) = 800
باقی چپ = 800 - 800 = 0
باقی راست = 900 - 800 = 100

امتیاز = MIN(800, 300) = 300 ✅

فلش:
  از چپ: 800 - 300 = 500
  از راست: 800 - 300 = 500
  جمع: 1000 (از بین می‌رود)

نتیجه: حتی با 800 تعادل، فقط 300 امتیاز می‌گیرد!


🎯 جمع‌بندی قوانین

قوانین کلیدی:

  1. تعادل = MIN(چپ، راست)
  2. باقیمانده = طرفی که بیشتر است (قبل از سقف)
  3. امتیاز = MIN(تعادل، 300)
  4. فلش = (تعادل - 300) از هر دو طرف (اگر > 300)
  5. محاسبه مستقل = هر کاربر جداگانه
  6. جمع صندوق = مجموع امتیازهای همه

نکات مهم:

  • باقیمانده جداگانه ذخیره می‌شود (چپ و راست)
  • فلش از هر دو طرف اتفاق می‌افتد
  • سقف 300 روی امتیاز نهایی اعمال می‌شود
  • هر کاربر مستقل از دیگران محاسبه می‌شود

📊 جدول مقایسه سناریوها

سناریو چپ راست تعادل امتیاز باقی چپ باقی راست فلش کل
متعادل کوچک 50 50 50 50 0 0 0
متعادل متوسط 200 200 200 200 0 0 0
نامتعادل کوچک 100 150 100 100 0 50 0
نامتعادل متوسط 250 350 250 250 0 100 0
سقف ساده 350 350 350 300 0 0 100
سقف نامتعادل 500 600 500 300 0 100 400
سقف بزرگ 800 900 800 300 0 100 1000

پایان مثال‌های عملی

این مستند تمام حالات ممکن محاسبه تعادل را با مثال‌های عددی واقعی نشان می‌دهد.


ضمیمه: فرمول‌های محاسبه باینری (Excel)

محاسبات پلن باینری (Binary Plan Calculations)

مستندات فرمول‌های محاسبه کمیسیون باینری

این سند فرمول‌های محاسباتی سیستم کمیسیون باینری را که از فایل اکسل استخراج شده، توضیح می‌دهد.


متغیرها و تعاریف

ورودی‌های هفته قبل (Last Week Remainders)

نام فارسی نماد توضیحات
باقیمانده هفته قبل چپ LL (Last Left) باقیمانده‌ای که از هفته قبل در پای چپ باقی مانده
باقیمانده هفته قبل راست LR (Last Right) باقیمانده‌ای که از هفته قبل در پای راست باقی مانده

مثال از اکسل:

  • LL = 200 (میلیون ریال)
  • LR = 0

ورودی‌های هفته جدید (New Week Values)

نام فارسی نماد توضیحات
هفته جدید چپ NL (New Left) مجموع فروش/شارژ پای چپ در هفته جاری
هفته جدید راست NR (New Right) مجموع فروش/شارژ پای راست در هفته جاری

مثال از اکسل:

  • NL = 400 (میلیون ریال)
  • NR = 500 (میلیون ریال)

پارامتر سیستم (System Parameter)

نام فارسی نماد توضیحات
ماکسیمم تعادل MX (Maximum Balance) حداکثر مقداری که در یک هفته می‌تواند به عنوان تعادل (کمیسیون) محاسبه شود

مثال از اکسل:

  • MX = 300 (میلیون ریال)

نکته مهم: این مقدار معمولاً بر اساس سطح کاربر یا پکیج خریداری شده تعیین می‌شود.


فرمول‌های محاسباتی

1️⃣ محاسبه مجموع پا چپ (Sum Left Total)

SLT = LL + NL

توضیح:

  • SLT (Sum Left Total) = مجموع کل پای چپ
  • باقیمانده هفته قبل + فروش هفته جدید

مثال:

SLT = 200 + 400 = 600

2️⃣ محاسبه مجموع پا راست (Sum Right Total)

SRT = LR + NR

توضیح:

  • SRT (Sum Right Total) = مجموع کل پای راست
  • باقیمانده هفته قبل + فروش هفته جدید

مثال:

SRT = 0 + 500 = 500

3️⃣ محاسبه کمترین کل (Minimum Total)

MinT = MIN(SLT, SRT)

توضیح:

  • MinT = کوچکترین مقدار بین دو پا
  • این مقدار نشان‌دهنده حداکثر تعادل بالقوه است

مثال:

MinT = MIN(600, 500) = 500

4️⃣ محاسبه باقیمانده هفته بعد چپ (Remainder Next Week Left)

RNWL = SLT - MinT

توضیح:

  • RNWL (Remainder Next Week Left) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
  • مازاد پای چپ که برای تعادل استفاده نشد

مثال:

RNWL = 600 - 500 = 100

5️⃣ محاسبه باقیمانده هفته بعد راست (Remainder Next Week Right)

RNWR = SRT - MinT

توضیح:

  • RNWR (Remainder Next Week Right) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
  • مازاد پای راست که برای تعادل استفاده نشد

مثال:

RNWR = 500 - 500 = 0

نکته: یکی از دو باقیمانده همیشه صفر است (چون MinT کوچکترین است).


6️⃣ محاسبه فلش چپ (Flush Left)

FL = SLT - MX - RNWL

توضیح:

  • FL (Flush Left) = مقداری که از ماکسیمم هم بیشتر بود و باید دور ریخته شود
  • این مقدار نشان‌دهنده سرریز (overflow) است که نمی‌تواند به هفته بعد منتقل شود

مثال:

FL = 600 - 300 - 100 = 200

معنی: از 600 میلیون پای چپ:

  • 300 به عنوان کمیسیون استفاده شد (تا حد MX)
  • 100 به هفته بعد منتقل شد
  • 200 فلش شد (از دست رفت)

7️⃣ محاسبه فلش راست (Flush Right)

FR = SRT - MX - RNWR

توضیح:

  • FR (Flush Right) = مقداری که از پای راست دور ریخته می‌شود

مثال:

FR = 500 - 300 - 0 = 200

معنی: از 500 میلیون پای راست:

  • 300 به عنوان کمیسیون استفاده شد
  • 0 به هفته بعد منتقل شد
  • 200 فلش شد (از دست رفت)

8️⃣ محاسبه کل تعادل (Total Balance / Commission)

TB = IF(MinT > MX, MX, MinT)

یا به زبان ساده‌تر:

TB = MIN(MinT, MX)

توضیح:

  • TB (Total Balance) = مقدار واقعی کمیسیونی که به کاربر تعلق می‌گیرد
  • نمی‌تواند از ماکسیمم تعادل (MX) بیشتر شود

مثال:

TB = MIN(500, 300) = 300

معنی: هرچند تعادل واقعی 500 بود، اما به دلیل محدودیت MX، فقط 300 به عنوان کمیسیون پرداخت می‌شود.


خلاصه جریان محاسبات

┌─────────────────────────────────────────────────────────────┐
│ ورودی‌ها                                                     │
├─────────────────────────────────────────────────────────────┤
│ LL = 200     باقیمانده هفته قبل چپ                         │
│ LR = 0       باقیمانده هفته قبل راست                       │
│ NL = 400     هفته جدید چپ                                  │
│ NR = 500     هفته جدید راست                                │
│ MX = 300     ماکسیمم تعادل                                  │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│ گام 1: محاسبه مجموع دو پا                                  │
├─────────────────────────────────────────────────────────────┤
│ SLT = LL + NL = 200 + 400 = 600                             │
│ SRT = LR + NR = 0 + 500 = 500                               │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│ گام 2: محاسبه کمترین کل                                    │
├─────────────────────────────────────────────────────────────┤
│ MinT = MIN(SLT, SRT) = MIN(600, 500) = 500                  │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│ گام 3: محاسبه کمیسیون واقعی (با اعمال Cap)                │
├─────────────────────────────────────────────────────────────┤
│ TB = MIN(MinT, MX) = MIN(500, 300) = 300  ✅ کمیسیون        │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│ گام 4: محاسبه باقیمانده هفته بعد                           │
├─────────────────────────────────────────────────────────────┤
│ RNWL = SLT - MinT = 600 - 500 = 100  → هفته بعد            │
│ RNWR = SRT - MinT = 500 - 500 = 0    → هفته بعد            │
└─────────────────────────────────────────────────────────────┘
                          ↓
┌─────────────────────────────────────────────────────────────┐
│ گام 5: محاسبه فلش (از دست رفته)                            │
├─────────────────────────────────────────────────────────────┤
│ FL = SLT - MX - RNWL = 600 - 300 - 100 = 200  ❌ فلش        │
│ FR = SRT - MX - RNWR = 500 - 300 - 0 = 200    ❌ فلش        │
└─────────────────────────────────────────────────────────────┘

تحلیل نتایج

📊 خروجی‌های نهایی

مقدار توضیح وضعیت
TB = 300 کمیسیون پرداختی این هفته پرداخت می‌شود
RNWL = 100 باقیمانده پای چپ برای هفته بعد ⏭️ منتقل می‌شود
RNWR = 0 باقیمانده پای راست برای هفته بعد ⏭️ منتقل می‌شود
FL = 200 فلش پای چپ از دست می‌رود
FR = 200 فلش پای راست از دست می‌رود

🔍 تفسیر کسب‌وکار

کمیسیون محاسبه شده

کمیسیون = 300 میلیون ریال
  • به دلیل محدودیت MX = 300، از تعادل بالقوه 500، فقط 300 قابل برداشت است
  • این یک مکانیزم کنترل هزینه است

باقیمانده به هفته بعد

هفته بعد LL = 100 (از پای چپ)
هفته بعد LR = 0 (از پای راست)
  • 100 میلیون از پای چپ به هفته بعد منتقل می‌شود
  • این باقیمانده در محاسبات هفته آینده دوباره استفاده خواهد شد

فلش (Flush) - نکته مهم ⚠️

فلش کل = 400 میلیون ریال (200 چپ + 200 راست)

چرا فلش رخ می‌دهد؟

  1. مجموع دو پا = 1100 میلیون (600 + 500)
  2. کمیسیون محاسبه شده = 300 میلیون
  3. باقیمانده منتقل شده = 100 میلیون
  4. فلش = 1100 - 300 - 100 = 700 میلیون

توضیح:

  • فلش نشان‌دهنده مقداری است که به دلیل عدم تعادل و محدودیت Cap از دست می‌رود
  • این یک ضرر برای کاربر است که می‌تواند با متعادل کردن دو پا کاهش یابد

پیاده‌سازی در C#

کلاس مدل

public class BinaryPlanCalculationInput
{
    // ورودی‌های هفته قبل
    public decimal LastLeftRemainder { get; set; }      // LL
    public decimal LastRightRemainder { get; set; }     // LR
    
    // ورودی‌های هفته جاری
    public decimal NewLeftVolume { get; set; }          // NL
    public decimal NewRightVolume { get; set; }         // NR
    
    // تنظیمات سیستم
    public decimal MaximumBalance { get; set; }         // MX
}

public class BinaryPlanCalculationResult
{
    // محاسبات واسط
    public decimal SumLeftTotal { get; set; }           // SLT
    public decimal SumRightTotal { get; set; }          // SRT
    public decimal MinimumTotal { get; set; }           // MinT
    
    // باقیمانده‌ها
    public decimal RemainderNextWeekLeft { get; set; }  // RNWL
    public decimal RemainderNextWeekRight { get; set; } // RNWR
    
    // فلش
    public decimal FlushLeft { get; set; }              // FL
    public decimal FlushRight { get; set; }             // FR
    
    // نتیجه نهایی
    public decimal TotalBalance { get; set; }           // TB - کمیسیون واقعی
    public decimal TotalFlush { get; set; }             // مجموع فلش
}

متد محاسبه

public static BinaryPlanCalculationResult Calculate(BinaryPlanCalculationInput input)
{
    var result = new BinaryPlanCalculationResult();
    
    // گام 1: محاسبه مجموع دو پا
    result.SumLeftTotal = input.LastLeftRemainder + input.NewLeftVolume;
    result.SumRightTotal = input.LastRightRemainder + input.NewRightVolume;
    
    // گام 2: محاسبه کمترین کل
    result.MinimumTotal = Math.Min(result.SumLeftTotal, result.SumRightTotal);
    
    // گام 3: محاسبه کمیسیون واقعی (با اعمال Cap)
    result.TotalBalance = Math.Min(result.MinimumTotal, input.MaximumBalance);
    
    // گام 4: محاسبه باقیمانده هفته بعد
    result.RemainderNextWeekLeft = result.SumLeftTotal - result.MinimumTotal;
    result.RemainderNextWeekRight = result.SumRightTotal - result.MinimumTotal;
    
    // گام 5: محاسبه فلش
    result.FlushLeft = result.SumLeftTotal - input.MaximumBalance - result.RemainderNextWeekLeft;
    result.FlushRight = result.SumRightTotal - input.MaximumBalance - result.RemainderNextWeekRight;
    
    // محاسبه مجموع فلش
    result.TotalFlush = result.FlushLeft + result.FlushRight;
    
    // اطمینان از عدم منفی شدن فلش
    result.FlushLeft = Math.Max(0, result.FlushLeft);
    result.FlushRight = Math.Max(0, result.FlushRight);
    result.TotalFlush = Math.Max(0, result.TotalFlush);
    
    return result;
}

مثال استفاده

var input = new BinaryPlanCalculationInput
{
    LastLeftRemainder = 200_000_000,   // 200 میلیون
    LastRightRemainder = 0,
    NewLeftVolume = 400_000_000,       // 400 میلیون
    NewRightVolume = 500_000_000,      // 500 میلیون
    MaximumBalance = 300_000_000       // 300 میلیون
};

var result = Calculate(input);

Console.WriteLine($"کمیسیون قابل پرداخت: {result.TotalBalance:N0} ریال");
// Output: کمیسیون قابل پرداخت: 300,000,000 ریال

Console.WriteLine($"باقیمانده چپ هفته بعد: {result.RemainderNextWeekLeft:N0} ریال");
// Output: باقیمانده چپ هفته بعد: 100,000,000 ریال

Console.WriteLine($"باقیمانده راست هفته بعد: {result.RemainderNextWeekRight:N0} ریال");
// Output: باقیمانده راست هفته بعد: 0 ریال

Console.WriteLine($"فلش کل: {result.TotalFlush:N0} ریال");
// Output: فلش کل: 400,000,000 ریال

نکات مهم برای پیاده‌سازی

1️⃣ ذخیره باقیمانده‌ها

// باید در دیتابیس ذخیره شود
await SaveWeeklyRemainders(userId, weekId, new WeeklyRemainders
{
    LeftRemainder = result.RemainderNextWeekLeft,
    RightRemainder = result.RemainderNextWeekRight
});

2️⃣ لاگ فلش برای تحلیل

if (result.TotalFlush > 0)
{
    await LogFlush(userId, weekId, new FlushLog
    {
        FlushLeft = result.FlushLeft,
        FlushRight = result.FlushRight,
        Reason = "Cap limitation and imbalance"
    });
}

3️⃣ تعیین MaximumBalance

// بر اساس سطح کاربر
decimal GetMaximumBalance(User user)
{
    return user.MembershipLevel switch
    {
        MembershipLevel.Bronze => 100_000_000,
        MembershipLevel.Silver => 300_000_000,
        MembershipLevel.Gold => 500_000_000,
        MembershipLevel.Platinum => 1_000_000_000,
        _ => 50_000_000
    };
}

4️⃣ واحد پول

// همه مقادیر باید در واحد ریال ذخیره شوند
// برای نمایش می‌توان به میلیون یا تومان تبدیل کرد
decimal DisplayInMillions(decimal rials) => rials / 1_000_000;
decimal DisplayInTomans(decimal rials) => rials / 10;

سناریوهای مختلف

سناریو 1: تعادل کامل

LL = 0, LR = 0, NL = 300, NR = 300, MX = 500
→ TB = 300, RNWL = 0, RNWR = 0, FL = 0, FR = 0

نتیجه: کمیسیون کامل بدون فلش


سناریو 2: یک پا خیلی بیشتر

LL = 0, LR = 0, NL = 1000, NR = 100, MX = 500
→ TB = 100, RNWL = 900, RNWR = 0, FL = 400, FR = 0

نتیجه: کمیسیون کم + فلش زیاد


سناریو 3: باقیمانده قبلی موثر

LL = 400, LR = 0, NL = 100, NR = 400, MX = 300
→ SLT = 500, SRT = 400
→ TB = 300, RNWL = 100, RNWR = 0, FL = 100, FR = 100

نتیجه: باقیمانده قبلی در محاسبه کمیسیون موثر است


تفاوت با کد فعلی

در کد فعلی (CalculateWeeklyBalancesCommandHandler.cs):

// 1. ابتدا Cap اعمال می‌شود
var cappedLeft = Math.Min(leftLegTotal, maxBalance);
var cappedRight = Math.Min(rightLegTotal, maxBalance);

// 2. سپس تعادل محاسبه می‌شود
var balance = Math.Min(cappedLeft, cappedRight);

// 3. باقیمانده‌ها محاسبه می‌شوند
var leftRemainder = leftLegTotal - balance;
var rightRemainder = rightLegTotal - balance;

در فرمول اکسل:

// 1. ابتدا تعادل کامل محاسبه می‌شود
var minTotal = Math.Min(leftLegTotal, rightLegTotal);

// 2. سپس Cap اعمال می‌شود
var balance = Math.Min(minTotal, maxBalance);

// 3. باقیمانده‌ها بر اساس minTotal محاسبه می‌شوند
var leftRemainder = leftLegTotal - minTotal;
var rightRemainder = rightLegTotal - minTotal;

// 4. فلش محاسبه می‌شود
var flushLeft = leftLegTotal - maxBalance - leftRemainder;
var flushRight = rightLegTotal - maxBalance - rightRemainder;

تفاوت کلیدی:

  • کد فعلی Cap را ابتدا اعمال می‌کند (می‌تواند باقیمانده‌های بیشتری ایجاد کند)
  • فرمول اکسل ابتدا تعادل را محاسبه می‌کند، سپس Cap اعمال می‌شود (فلش دقیق‌تر محاسبه می‌شود)

نتیجه‌گیری

این فرمول‌ها نشان می‌دهند که:

  1. تعادل اهمیت دارد - هرچه دو پا متعادل‌تر باشند، فلش کمتر است
  2. Cap محدودیت ایجاد می‌کند - حتی با تعادل کامل، بیش از MX کمیسیون داده نمی‌شود
  3. باقیمانده‌ها منتقل می‌شوند - برای هفته بعد ذخیره می‌شوند
  4. فلش ضرر است - مقداری که به دلیل عدم تعادل یا Cap از دست می‌رود

توصیه: برای افزایش کمیسیون، کاربران باید:

  • دو پای خود را متعادل نگه دارند
  • سطح عضویت خود را ارتقا دهند (برای افزایش MX)
  • از باقیمانده‌ها در هفته‌های بعد استفاده کنند