002e99f6bf
- Added PersianDateTimeService for converting Gregorian dates to Persian format in the BackOffice frontend. - Updated multiple frontend pages (Dashboard, UserPayouts, WorkerControl, UserNetworkInfo) to utilize the new Persian date service. - Enhanced GetUserNetworkPositionDto with 28+ new fields for comprehensive user network data. - Updated GetUserNetworkPositionQueryHandler to include new methods for calculating network statistics. - Modified Protobuf messages to accommodate the new fields, increasing from 14 to 42. - Refined week number calculation algorithm to ensure consistency across C# and SQL implementations. - Created new CSV and Excel files for binary plan calculations. - Ensured all changes are tested and validated for accuracy and performance.
547 lines
16 KiB
Markdown
547 lines
16 KiB
Markdown
# 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<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
|