Files
CMS/docs/balance-calculation-carryover-logic.md

421 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Balance Calculation with Carryover Logic - Complete Guide
**Date**: 2025-12-01
**Last Updated**: 2025-12-01 (Added Configuration Integration + MaxWeeklyBalances Cap)
**Status**: ✅ Implemented
**Migration**: `UpdateNetworkWeeklyBalanceWithCarryover`
---
## 📋 Configuration-Based Calculation
### **System Configurations Used:**
```csharp
// تمام مقادیر از جدول SystemConfigurations خوانده می‌شوند
Club.ActivationFee = 25,000,000 ریال (هزینه فعالسازی)
Commission.WeeklyPoolContributionPercent = 20% (سهم استخر)
Commission.MaxWeeklyBalancesPerUser = 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 (محدودیت سقف)
### **Logic:**
```csharp
totalBalances = MIN(leftTotal, rightTotal)
cappedBalances = MIN(totalBalances, maxWeeklyBalances) // 300
// اگر بیشتر از سقف بود، مازاد به remainder اضافه می‌شود
excessBalances = totalBalances - cappedBalances
```
### **Example:**
```
Week 5:
leftTotal = 350, rightTotal = 400
totalBalances = MIN(350, 400) = 350
cappedBalances = MIN(350, 300) = 300 ✅ محدود شد!
excessBalances = 350 - 300 = 50
leftRemainder = 0 + 50 = 50 (میرود برای هفته بعد)
rightRemainder = 50
```
---
## 📊 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<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