# 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 (مجموع دو دست) | ### تفاوت در محاسبه: **منطق فعلی (اشتباه):** ```csharp totalBalances = MIN(leftTotal, rightTotal) cappedBalances = MIN(totalBalances, 300) // ← سقف روی کل ``` **منطق صحیح:** ```csharp 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: ```csharp // ✅ تغییر نام و مقدار: // قدیمی: Key = "Commission.MaxWeeklyBalancesPerUser", Value = "300" // جدید: Key = "Commission.MaxWeeklyBalancesPerLeg", Value = "300" ``` --- ## 📋 Configuration-Based Calculation ### **System Configurations Used:** ```csharp // تمام مقادیر از جدول SystemConfigurations خوانده می‌شوند Club.ActivationFee = 25,000,000 ریال (هزینه فعال‌سازی) Commission.WeeklyPoolContributionPercent = 20% (سهم استخر) Commission.MaxWeeklyBalancesPerLeg = 300 (✅ سقف امتیاز نهایی) ``` **نکته مهم**: سقف 300 روی **امتیاز نهایی** اعمال می‌شود، نه روی تعادل اولیه! ### **Pool Contribution Calculation:** ```csharp 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):** ```csharp // ✅ مرحله 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:** ```csharp // محاسبه تعداد کل اعضا در هر پا ## ✅ **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:** ```sql 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** ```csharp public async Task 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 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# ### کلاس مدل ```csharp 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; } // مجموع فلش } ``` --- ### متد محاسبه ```csharp 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; } ``` --- ### مثال استفاده ```csharp 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️⃣ ذخیره باقیمانده‌ها ```csharp // باید در دیتابیس ذخیره شود await SaveWeeklyRemainders(userId, weekId, new WeeklyRemainders { LeftRemainder = result.RemainderNextWeekLeft, RightRemainder = result.RemainderNextWeekRight }); ``` ### 2️⃣ لاگ فلش برای تحلیل ```csharp if (result.TotalFlush > 0) { await LogFlush(userId, weekId, new FlushLog { FlushLeft = result.FlushLeft, FlushRight = result.FlushRight, Reason = "Cap limitation and imbalance" }); } ``` ### 3️⃣ تعیین MaximumBalance ```csharp // بر اساس سطح کاربر 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️⃣ واحد پول ```csharp // همه مقادیر باید در واحد ریال ذخیره شوند // برای نمایش می‌توان به میلیون یا تومان تبدیل کرد 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`): ```csharp // 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; ``` ### در فرمول اکسل: ```csharp // 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) - از باقیمانده‌ها در هفته‌های بعد استفاده کنند