- Added last updated date and refined project status for Backend and Frontend. - Updated completion percentages for FrontOffice UI and BFF. - Documented completed tasks for BackOffice in the current sprint. - Added new high-priority tasks related to commission calculation fixes. - Resolved blockers and minor issues, providing a clearer progress summary. docs: Create commission-calculation-fix.md for weekly commission calculation analysis - Documented critical issues affecting commission calculations. - Provided a detailed plan for fixing the commission calculation logic. - Included code analysis, affected files, and step-by-step tasks for implementation. - Outlined the expected timeline and important notes regarding changes.
14 KiB
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 (مجموع دو دست) |
تفاوت در محاسبه:
منطق فعلی (اشتباه):
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 (⚠️ سقف تعادل هفتگی - هر دست)
توجه: کلید قدیمی MaxWeeklyBalancesPerUser باید به MaxWeeklyBalancesPerLeg تغییر کند.
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 (محدودیت سقف - هر دست)
Logic (صحیح):
// ⚠️ سقف روی هر دست جداگانه اعمال میشود
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:
// محاسبه تعداد کل اعضا در هر پا
leftLegBalances = CountAllMembers(userId, Left);
rightLegBalances = CountAllMembers(userId, Right);
// تعادل = کمترین مقدار
TotalBalances = MIN(leftLegBalances, rightLegBalances);
مشکلات:
- تعداد کل اعضا را میشمارد (نه فقط جدیدها)
- باقیمانده هفته قبل را نادیده میگیرد
- هر هفته از صفر شروع میکند
✅ Current (Correct) Logic:
Formula:
leftTotal = leftNewMembers + leftCarryover
rightTotal = rightNewMembers + rightCarryover
TotalBalances = MIN(leftTotal, rightTotal)
leftRemainder = leftTotal - TotalBalances
rightRemainder = rightTotal - TotalBalances
Key Principles:
- Only count NEW members activated in current week
- Add carryover from previous week
- Calculate remainder for next week
- 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
- ✅ Only NEW activations count - filtered by
ActivatedAtdate - ✅ Carryover persists - unused balances roll over to next week
- ✅ Recursive counting - includes entire subtree under each leg
- ✅ Week date ranges - ISO 8601 week format (Saturday to Friday)
- ✅ Idempotent - can recalculate with
ForceRecalculateflag
🚀 Benefits
- Fair commission distribution - rewards balanced growth
- No lost balances - carryover ensures nothing is wasted
- Accurate tracking - distinguishes new vs existing members
- Scalable - works for large networks with recursive algorithm
- 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