# 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