# Balance Calculation with Carryover Logic - Complete Guide **Date**: 2025-12-01 **Last Updated**: 2025-12-04 (⚠️ تغییر مهم: سقف 300 برای هر دست، نه کل) **Status**: ✅ Implemented (نیاز به اصلاح سقف دارد) **Migration**: `UpdateNetworkWeeklyBalanceWithCarryover` --- ## ⚠️ اصلاحیه مهم بیزینس (2025-12-04) ### مشکل شناسایی شده: در پیاده‌سازی فعلی، سقف تعادل هفتگی **300 کل** در نظر گرفته شده بود. اما طبق قانون صحیح بیزینس: | توضیح | منطق فعلی (اشتباه) | منطق صحیح | |-------|---------------------|-----------| | سقف | 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 (⚠️ سقف تعادل هفتگی - هر دست) ``` **توجه:** کلید قدیمی `MaxWeeklyBalancesPerUser` باید به `MaxWeeklyBalancesPerLeg` تغییر کند. ### **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 (محدودیت سقف - هر دست) ### **Logic (صحیح):** ```csharp // ⚠️ سقف روی هر دست جداگانه اعمال می‌شود cappedLeftTotal = MIN(leftTotal, maxBalancesPerLeg) // 300 cappedRightTotal = MIN(rightTotal, maxBalancesPerLeg) // 300 // تعادل = کمترین مقدار بعد از اعمال سقف totalBalances = MIN(cappedLeftTotal, cappedRightTotal) // باقیمانده = مقدار قبل از سقف - سقف (نه از totalBalances) leftRemainder = leftTotal - cappedLeftTotal rightRemainder = rightTotal - cappedRightTotal ``` ### **Example (جدید):** ``` Week 5: leftTotal = 350, rightTotal = 400 maxBalancesPerLeg = 300 cappedLeftTotal = MIN(350, 300) = 300 cappedRightTotal = MIN(400, 300) = 300 totalBalances = MIN(300, 300) = 300 ✅ // باقیمانده = اضافه‌ای که از سقف رد شده leftRemainder = 350 - 300 = 50 rightRemainder = 400 - 300 = 100 ``` ### **مقایسه منطق قدیم 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 // محاسبه تعداد کل اعضا در هر پا leftLegBalances = CountAllMembers(userId, Left); rightLegBalances = CountAllMembers(userId, Right); // تعادل = کمترین مقدار TotalBalances = MIN(leftLegBalances, rightLegBalances); ``` **مشکلات:** 1. تعداد کل اعضا را می‌شمارد (نه فقط جدیدها) 2. باقیمانده هفته قبل را نادیده می‌گیرد 3. هر هفته از صفر شروع می‌کند --- ## ✅ **Current (Correct) Logic:** ### **Formula:** ``` 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