Files
docs/business/balance-calculation-rules.md
T

1485 lines
45 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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-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
---
# ضمیمه: مثال‌های محاسبه ۵ سطحی
# 📊 مثال‌های عملی محاسبه تعادل - 5 لول عمقی
**تاریخ**: 2025-12-09
**وضعیت**: مثال‌های کامل و تایید شده
**هدف**: نمایش محاسبات واقعی برای درخت باینری تا 5 لول
---
## 🌳 ساختار درخت نمونه
```
User1 (Level 0)
/ \
User2 (L1-L) User3 (L1-R)
/ \ / \
User4(L2-LL) User5(L2-LR) User6(L2-RL) User7(L2-RR)
/ \ / \ / \ / \
U8(L3) U9(L3) U10(L3) U11(L3) U12(L3) U13(L3) U14(L3) U15(L3)
/ \ / \ / \ / \ / \ / \ / \ / \
U16-U31 (Level 4 - 16 users)
/\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\
U32-U63 (Level 5 - 32 users)
```
---
## 📋 داده‌های ورودی
### فرضیات:
- **هفته فعلی**: 2025-W50
- **سقف امتیاز**: 300
- **تعداد کل کاربران**: 63 نفر (6 لول: 1+2+4+8+16+32)
- **وضعیت**: همه کاربران فعال هستند (عضو باشگاه)
---
## 🎯 محاسبات Level 5 (پایین‌ترین سطح)
### User 32-63 (32 کاربر Leaf):
```
هیچ زیرمجموعه‌ای ندارند
چپ = 0، راست = 0
تعادل = MIN(0, 0) = 0
امتیاز = 0
باقیمانده چپ = 0
باقیمانده راست = 0
فلش = 0
```
**خلاصه Level 5**: تمام 32 کاربر → 0 امتیاز
---
## 🎯 محاسبات Level 4 (User 16-31)
### User 16:
**زیرمجموعه**:
- چپ: User 32 (1 نفر)
- راست: User 33 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل اولیه = MIN(1, 1) = 1
باقیمانده چپ = 1 - 1 = 0
باقیمانده راست = 1 - 1 = 0
امتیاز نهایی = MIN(1, 300) = 1 ✅
فلش = 0
```
### User 17:
**زیرمجموعه**:
- چپ: User 34 (1 نفر)
- راست: User 35 (1 نفر)
**محاسبات**: مشابه User 16
```
امتیاز = 1 ✅
```
### User 18-31 (14 کاربر دیگه):
همه مشابه User 16 → هر کدام 1 امتیاز
**خلاصه Level 4**: تمام 16 کاربر → هر کدام 1 امتیاز = **16 امتیاز**
---
## 🎯 محاسبات Level 3 (User 8-15)
### User 8:
**زیرمجموعه**:
- چپ: User 16 (1 نفر)
- راست: User 17 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 9:
**زیرمجموعه**:
- چپ: User 18 (1 نفر)
- راست: User 19 (1 نفر)
**محاسبات**: مشابه User 8
```
امتیاز = 1 ✅
```
### User 10-15 (6 کاربر دیگه):
همه مشابه → هر کدام 1 امتیاز
**خلاصه Level 3**: تمام 8 کاربر → هر کدام 1 امتیاز = **8 امتیاز**
---
## 🎯 محاسبات Level 2 (User 4-7)
### User 4:
**زیرمجموعه**:
- چپ: User 8 (1 نفر)
- راست: User 9 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 5:
**زیرمجموعه**:
- چپ: User 10 (1 نفر)
- راست: User 11 (1 نفر)
**محاسبات**: مشابه User 4
```
امتیاز = 1 ✅
```
### User 6, 7:
همه مشابه → هر کدام 1 امتیاز
**خلاصه Level 2**: تمام 4 کاربر → هر کدام 1 امتیاز = **4 امتیاز**
---
## 🎯 محاسبات Level 1 (User 2-3)
### User 2:
**زیرمجموعه**:
- چپ: User 4 (1 نفر)
- راست: User 5 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 3:
**زیرمجموعه**:
- چپ: User 6 (1 نفر)
- راست: User 7 (1 نفر)
**محاسبات**: مشابه User 2
```
امتیاز = 1 ✅
```
**خلاصه Level 1**: تمام 2 کاربر → هر کدام 1 امتیاز = **2 امتیاز**
---
## 🎯 محاسبات Level 0 (User 1 - Root)
### User 1:
**زیرمجموعه**:
- چپ: User 2 (1 نفر)
- راست: User 3 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
**خلاصه Level 0**: User 1 → **1 امتیاز**
---
## 📊 جمع کل سیستم
| Level | تعداد کاربران | امتیاز هر کاربر | جمع امتیازهای Level |
|-------|---------------|-----------------|---------------------|
| 5 | 32 | 0 | 0 |
| 4 | 16 | 1 | 16 |
| 3 | 8 | 1 | 8 |
| 2 | 4 | 1 | 4 |
| 1 | 2 | 1 | 2 |
| 0 | 1 | 1 | 1 |
| **جمع** | **63** | - | **31 امتیاز** |
---
## 💰 محاسبه صندوق
### داده‌های ورودی:
```
تعداد کاربران فعال شده این هفته: 63 نفر
هزینه فعال‌سازی هر نفر: 25,000,000 ریال
درصد سهم استخر: 20%
جمع ورودی استخر = 63 × 25,000,000 × 20%
= 63 × 5,000,000
= 315,000,000 ریال
```
### محاسبه ارزش هر امتیاز:
```
مجموع امتیازهای سیستم = 31
جمع استخر = 315,000,000 ریال
ارزش هر امتیاز = 315,000,000 ÷ 31
= 10,161,290 ریال (تقریباً)
```
### توزیع کمیسیون:
```
User 1: 1 × 10,161,290 = 10,161,290 ریال
User 2: 1 × 10,161,290 = 10,161,290 ریال
User 3: 1 × 10,161,290 = 10,161,290 ریال
User 4-7: 4 × 10,161,290 = 40,645,160 ریال
User 8-15: 8 × 10,161,290 = 81,290,320 ریال
User 16-31: 16 × 10,161,290 = 162,580,640 ریال
User 32-63: 0 ریال (امتیازی ندارند)
جمع کل پرداختی = 315,000,000 ریال ✅
```
---
## 🔥 مثال پیچیده‌تر: سناریو نامتعادل
### تغییر ساختار:
```
User 1:
چپ: 500 نفر (عمق زیاد)
راست: 600 نفر (عمق بیشتر)
```
### محاسبات User 1:
```
مرحله 1️⃣: تعادل اولیه
چپ = 500، راست = 600
تعادل = MIN(500, 600) = 500
مرحله 2️⃣: باقیمانده
باقی چپ = 500 - 500 = 0
باقی راست = 600 - 500 = 100 → هفته بعد
مرحله 3️⃣: اعمال سقف
امتیاز = MIN(500, 300) = 300 ✅
مرحله 4️⃣: فلش
فلش از چپ = 500 - 300 = 200
فلش از راست = 500 - 300 = 200
جمع فلش = 400 (از بین می‌رود)
```
### نتیجه:
```
✅ امتیاز User 1: 300
✅ باقیمانده راست: 100 (می‌رود هفته بعد)
✅ باقیمانده چپ: 0
✅ فلش شده: 400 (از بین رفته)
```
---
## 🔄 مثال با Carryover (هفته بعد)
### فرض: User 1 در هفته 2025-W51:
```
باقیمانده هفته قبل:
چپ: 0
راست: 100
جدیدهای این هفته:
چپ: 250
راست: 150
```
### محاسبات:
```
مرحله 1️⃣: جمع با هفته قبل
چپ کل = 0 + 250 = 250
راست کل = 100 + 150 = 250
مرحله 2️⃣: تعادل
تعادل = MIN(250, 250) = 250
مرحله 3️⃣: باقیمانده
باقی چپ = 250 - 250 = 0
باقی راست = 250 - 250 = 0
مرحله 4️⃣: امتیاز
امتیاز = MIN(250, 300) = 250 ✅
مرحله 5️⃣: فلش
فلش = 0 (چون 250 < 300)
```
---
## 📈 مثال سقف: User با شبکه بزرگ
### User A:
```
چپ: 800 نفر
راست: 900 نفر
```
### محاسبات:
```
تعادل = MIN(800, 900) = 800
باقی چپ = 800 - 800 = 0
باقی راست = 900 - 800 = 100
امتیاز = MIN(800, 300) = 300 ✅
فلش:
از چپ: 800 - 300 = 500
از راست: 800 - 300 = 500
جمع: 1000 (از بین می‌رود)
```
**نتیجه**: حتی با 800 تعادل، فقط **300 امتیاز** می‌گیرد!
---
## 🎯 جمع‌بندی قوانین
### ✅ قوانین کلیدی:
1. **تعادل** = MIN(چپ، راست)
2. **باقیمانده** = طرفی که بیشتر است (قبل از سقف)
3. **امتیاز** = MIN(تعادل، 300)
4. **فلش** = (تعادل - 300) از هر دو طرف (اگر > 300)
5. **محاسبه مستقل** = هر کاربر جداگانه
6. **جمع صندوق** = مجموع امتیازهای همه
### ✅ نکات مهم:
- باقیمانده **جداگانه** ذخیره می‌شود (چپ و راست)
- فلش از **هر دو طرف** اتفاق می‌افتد
- سقف 300 روی **امتیاز نهایی** اعمال می‌شود
- هر کاربر مستقل از دیگران محاسبه می‌شود
---
## 📊 جدول مقایسه سناریوها
| سناریو | چپ | راست | تعادل | امتیاز | باقی چپ | باقی راست | فلش کل |
|--------|-----|-------|--------|--------|---------|-----------|---------|
| متعادل کوچک | 50 | 50 | 50 | 50 | 0 | 0 | 0 |
| متعادل متوسط | 200 | 200 | 200 | 200 | 0 | 0 | 0 |
| نامتعادل کوچک | 100 | 150 | 100 | 100 | 0 | 50 | 0 |
| نامتعادل متوسط | 250 | 350 | 250 | 250 | 0 | 100 | 0 |
| **سقف ساده** | **350** | **350** | **350** | **300** | **0** | **0** | **100** |
| **سقف نامتعادل** | **500** | **600** | **500** | **300** | **0** | **100** | **400** |
| سقف بزرگ | 800 | 900 | 800 | 300 | 0 | 100 | 1000 |
---
**پایان مثال‌های عملی**
این مستند تمام حالات ممکن محاسبه تعادل را با مثال‌های عددی واقعی نشان می‌دهد.
---
# ضمیمه: فرمول‌های محاسبه باینری (Excel)
# محاسبات پلن باینری (Binary Plan Calculations)
## مستندات فرمول‌های محاسبه کمیسیون باینری
این سند فرمول‌های محاسباتی سیستم کمیسیون باینری را که از فایل اکسل استخراج شده، توضیح می‌دهد.
---
## متغیرها و تعاریف
### ورودی‌های هفته قبل (Last Week Remainders)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **باقیمانده هفته قبل چپ** | `LL` (Last Left) | باقیمانده‌ای که از هفته قبل در پای چپ باقی مانده |
| **باقیمانده هفته قبل راست** | `LR` (Last Right) | باقیمانده‌ای که از هفته قبل در پای راست باقی مانده |
**مثال از اکسل:**
- `LL = 200` (میلیون ریال)
- `LR = 0`
---
### ورودی‌های هفته جدید (New Week Values)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **هفته جدید چپ** | `NL` (New Left) | مجموع فروش/شارژ پای چپ در هفته جاری |
| **هفته جدید راست** | `NR` (New Right) | مجموع فروش/شارژ پای راست در هفته جاری |
**مثال از اکسل:**
- `NL = 400` (میلیون ریال)
- `NR = 500` (میلیون ریال)
---
### پارامتر سیستم (System Parameter)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **ماکسیمم تعادل** | `MX` (Maximum Balance) | حداکثر مقداری که در یک هفته می‌تواند به عنوان تعادل (کمیسیون) محاسبه شود |
**مثال از اکسل:**
- `MX = 300` (میلیون ریال)
**نکته مهم:** این مقدار معمولاً بر اساس سطح کاربر یا پکیج خریداری شده تعیین می‌شود.
---
## فرمول‌های محاسباتی
### 1️⃣ محاسبه مجموع پا چپ (Sum Left Total)
```
SLT = LL + NL
```
**توضیح:**
- `SLT` (Sum Left Total) = مجموع کل پای چپ
- باقیمانده هفته قبل + فروش هفته جدید
**مثال:**
```
SLT = 200 + 400 = 600
```
---
### 2️⃣ محاسبه مجموع پا راست (Sum Right Total)
```
SRT = LR + NR
```
**توضیح:**
- `SRT` (Sum Right Total) = مجموع کل پای راست
- باقیمانده هفته قبل + فروش هفته جدید
**مثال:**
```
SRT = 0 + 500 = 500
```
---
### 3️⃣ محاسبه کمترین کل (Minimum Total)
```
MinT = MIN(SLT, SRT)
```
**توضیح:**
- `MinT` = کوچکترین مقدار بین دو پا
- این مقدار نشان‌دهنده حداکثر تعادل بالقوه است
**مثال:**
```
MinT = MIN(600, 500) = 500
```
---
### 4️⃣ محاسبه باقیمانده هفته بعد چپ (Remainder Next Week Left)
```
RNWL = SLT - MinT
```
**توضیح:**
- `RNWL` (Remainder Next Week Left) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
- مازاد پای چپ که برای تعادل استفاده نشد
**مثال:**
```
RNWL = 600 - 500 = 100
```
---
### 5️⃣ محاسبه باقیمانده هفته بعد راست (Remainder Next Week Right)
```
RNWR = SRT - MinT
```
**توضیح:**
- `RNWR` (Remainder Next Week Right) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
- مازاد پای راست که برای تعادل استفاده نشد
**مثال:**
```
RNWR = 500 - 500 = 0
```
**نکته:** یکی از دو باقیمانده همیشه صفر است (چون MinT کوچکترین است).
---
### 6️⃣ محاسبه فلش چپ (Flush Left)
```
FL = SLT - MX - RNWL
```
**توضیح:**
- `FL` (Flush Left) = مقداری که از ماکسیمم هم بیشتر بود و باید دور ریخته شود
- این مقدار نشان‌دهنده سرریز (overflow) است که نمی‌تواند به هفته بعد منتقل شود
**مثال:**
```
FL = 600 - 300 - 100 = 200
```
**معنی:** از 600 میلیون پای چپ:
- 300 به عنوان کمیسیون استفاده شد (تا حد MX)
- 100 به هفته بعد منتقل شد
- **200 فلش شد (از دست رفت)** ❌
---
### 7️⃣ محاسبه فلش راست (Flush Right)
```
FR = SRT - MX - RNWR
```
**توضیح:**
- `FR` (Flush Right) = مقداری که از پای راست دور ریخته می‌شود
**مثال:**
```
FR = 500 - 300 - 0 = 200
```
**معنی:** از 500 میلیون پای راست:
- 300 به عنوان کمیسیون استفاده شد
- 0 به هفته بعد منتقل شد
- **200 فلش شد (از دست رفت)** ❌
---
### 8️⃣ محاسبه کل تعادل (Total Balance / Commission)
```
TB = IF(MinT > MX, MX, MinT)
```
یا به زبان ساده‌تر:
```
TB = MIN(MinT, MX)
```
**توضیح:**
- `TB` (Total Balance) = مقدار واقعی کمیسیونی که به کاربر تعلق می‌گیرد
- نمی‌تواند از ماکسیمم تعادل (`MX`) بیشتر شود
**مثال:**
```
TB = MIN(500, 300) = 300
```
**معنی:** هرچند تعادل واقعی 500 بود، اما به دلیل محدودیت `MX`، فقط 300 به عنوان کمیسیون پرداخت می‌شود.
---
## خلاصه جریان محاسبات
```
┌─────────────────────────────────────────────────────────────┐
│ ورودی‌ها │
├─────────────────────────────────────────────────────────────┤
│ LL = 200 باقیمانده هفته قبل چپ │
│ LR = 0 باقیمانده هفته قبل راست │
│ NL = 400 هفته جدید چپ │
│ NR = 500 هفته جدید راست │
│ MX = 300 ماکسیمم تعادل │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 1: محاسبه مجموع دو پا │
├─────────────────────────────────────────────────────────────┤
│ SLT = LL + NL = 200 + 400 = 600 │
│ SRT = LR + NR = 0 + 500 = 500 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 2: محاسبه کمترین کل │
├─────────────────────────────────────────────────────────────┤
│ MinT = MIN(SLT, SRT) = MIN(600, 500) = 500 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 3: محاسبه کمیسیون واقعی (با اعمال Cap) │
├─────────────────────────────────────────────────────────────┤
│ TB = MIN(MinT, MX) = MIN(500, 300) = 300 ✅ کمیسیون │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 4: محاسبه باقیمانده هفته بعد │
├─────────────────────────────────────────────────────────────┤
│ RNWL = SLT - MinT = 600 - 500 = 100 → هفته بعد │
│ RNWR = SRT - MinT = 500 - 500 = 0 → هفته بعد │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 5: محاسبه فلش (از دست رفته) │
├─────────────────────────────────────────────────────────────┤
│ FL = SLT - MX - RNWL = 600 - 300 - 100 = 200 ❌ فلش │
│ FR = SRT - MX - RNWR = 500 - 300 - 0 = 200 ❌ فلش │
└─────────────────────────────────────────────────────────────┘
```
---
## تحلیل نتایج
### 📊 خروجی‌های نهایی
| مقدار | توضیح | وضعیت |
|-------|-------|-------|
| **TB = 300** | کمیسیون پرداختی این هفته | ✅ پرداخت می‌شود |
| **RNWL = 100** | باقیمانده پای چپ برای هفته بعد | ⏭️ منتقل می‌شود |
| **RNWR = 0** | باقیمانده پای راست برای هفته بعد | ⏭️ منتقل می‌شود |
| **FL = 200** | فلش پای چپ | ❌ از دست می‌رود |
| **FR = 200** | فلش پای راست | ❌ از دست می‌رود |
---
### 🔍 تفسیر کسب‌وکار
#### کمیسیون محاسبه شده
```
کمیسیون = 300 میلیون ریال
```
- به دلیل محدودیت `MX = 300`، از تعادل بالقوه 500، فقط 300 قابل برداشت است
- این یک مکانیزم کنترل هزینه است
#### باقیمانده به هفته بعد
```
هفته بعد LL = 100 (از پای چپ)
هفته بعد LR = 0 (از پای راست)
```
- 100 میلیون از پای چپ به هفته بعد منتقل می‌شود
- این باقیمانده در محاسبات هفته آینده دوباره استفاده خواهد شد
#### فلش (Flush) - نکته مهم ⚠️
```
فلش کل = 400 میلیون ریال (200 چپ + 200 راست)
```
**چرا فلش رخ می‌دهد؟**
1. مجموع دو پا = 1100 میلیون (600 + 500)
2. کمیسیون محاسبه شده = 300 میلیون
3. باقیمانده منتقل شده = 100 میلیون
4. فلش = 1100 - 300 - 100 = 700 میلیون ❌
**توضیح:**
- فلش نشان‌دهنده مقداری است که به دلیل **عدم تعادل** و **محدودیت Cap** از دست می‌رود
- این یک ضرر برای کاربر است که می‌تواند با متعادل کردن دو پا کاهش یابد
---
## پیاده‌سازی در C#
### کلاس مدل
```csharp
public class BinaryPlanCalculationInput
{
// ورودی‌های هفته قبل
public decimal LastLeftRemainder { get; set; } // LL
public decimal LastRightRemainder { get; set; } // LR
// ورودی‌های هفته جاری
public decimal NewLeftVolume { get; set; } // NL
public decimal NewRightVolume { get; set; } // NR
// تنظیمات سیستم
public decimal MaximumBalance { get; set; } // MX
}
public class BinaryPlanCalculationResult
{
// محاسبات واسط
public decimal SumLeftTotal { get; set; } // SLT
public decimal SumRightTotal { get; set; } // SRT
public decimal MinimumTotal { get; set; } // MinT
// باقیمانده‌ها
public decimal RemainderNextWeekLeft { get; set; } // RNWL
public decimal RemainderNextWeekRight { get; set; } // RNWR
// فلش
public decimal FlushLeft { get; set; } // FL
public decimal FlushRight { get; set; } // FR
// نتیجه نهایی
public decimal TotalBalance { get; set; } // TB - کمیسیون واقعی
public decimal TotalFlush { get; set; } // مجموع فلش
}
```
---
### متد محاسبه
```csharp
public static BinaryPlanCalculationResult Calculate(BinaryPlanCalculationInput input)
{
var result = new BinaryPlanCalculationResult();
// گام 1: محاسبه مجموع دو پا
result.SumLeftTotal = input.LastLeftRemainder + input.NewLeftVolume;
result.SumRightTotal = input.LastRightRemainder + input.NewRightVolume;
// گام 2: محاسبه کمترین کل
result.MinimumTotal = Math.Min(result.SumLeftTotal, result.SumRightTotal);
// گام 3: محاسبه کمیسیون واقعی (با اعمال Cap)
result.TotalBalance = Math.Min(result.MinimumTotal, input.MaximumBalance);
// گام 4: محاسبه باقیمانده هفته بعد
result.RemainderNextWeekLeft = result.SumLeftTotal - result.MinimumTotal;
result.RemainderNextWeekRight = result.SumRightTotal - result.MinimumTotal;
// گام 5: محاسبه فلش
result.FlushLeft = result.SumLeftTotal - input.MaximumBalance - result.RemainderNextWeekLeft;
result.FlushRight = result.SumRightTotal - input.MaximumBalance - result.RemainderNextWeekRight;
// محاسبه مجموع فلش
result.TotalFlush = result.FlushLeft + result.FlushRight;
// اطمینان از عدم منفی شدن فلش
result.FlushLeft = Math.Max(0, result.FlushLeft);
result.FlushRight = Math.Max(0, result.FlushRight);
result.TotalFlush = Math.Max(0, result.TotalFlush);
return result;
}
```
---
### مثال استفاده
```csharp
var input = new BinaryPlanCalculationInput
{
LastLeftRemainder = 200_000_000, // 200 میلیون
LastRightRemainder = 0,
NewLeftVolume = 400_000_000, // 400 میلیون
NewRightVolume = 500_000_000, // 500 میلیون
MaximumBalance = 300_000_000 // 300 میلیون
};
var result = Calculate(input);
Console.WriteLine($"کمیسیون قابل پرداخت: {result.TotalBalance:N0} ریال");
// Output: کمیسیون قابل پرداخت: 300,000,000 ریال
Console.WriteLine($"باقیمانده چپ هفته بعد: {result.RemainderNextWeekLeft:N0} ریال");
// Output: باقیمانده چپ هفته بعد: 100,000,000 ریال
Console.WriteLine($"باقیمانده راست هفته بعد: {result.RemainderNextWeekRight:N0} ریال");
// Output: باقیمانده راست هفته بعد: 0 ریال
Console.WriteLine($"فلش کل: {result.TotalFlush:N0} ریال");
// Output: فلش کل: 400,000,000 ریال
```
---
## نکات مهم برای پیاده‌سازی
### 1️⃣ ذخیره باقیمانده‌ها
```csharp
// باید در دیتابیس ذخیره شود
await SaveWeeklyRemainders(userId, weekId, new WeeklyRemainders
{
LeftRemainder = result.RemainderNextWeekLeft,
RightRemainder = result.RemainderNextWeekRight
});
```
### 2️⃣ لاگ فلش برای تحلیل
```csharp
if (result.TotalFlush > 0)
{
await LogFlush(userId, weekId, new FlushLog
{
FlushLeft = result.FlushLeft,
FlushRight = result.FlushRight,
Reason = "Cap limitation and imbalance"
});
}
```
### 3️⃣ تعیین MaximumBalance
```csharp
// بر اساس سطح کاربر
decimal GetMaximumBalance(User user)
{
return user.MembershipLevel switch
{
MembershipLevel.Bronze => 100_000_000,
MembershipLevel.Silver => 300_000_000,
MembershipLevel.Gold => 500_000_000,
MembershipLevel.Platinum => 1_000_000_000,
_ => 50_000_000
};
}
```
### 4️⃣ واحد پول
```csharp
// همه مقادیر باید در واحد ریال ذخیره شوند
// برای نمایش می‌توان به میلیون یا تومان تبدیل کرد
decimal DisplayInMillions(decimal rials) => rials / 1_000_000;
decimal DisplayInTomans(decimal rials) => rials / 10;
```
---
## سناریوهای مختلف
### سناریو 1: تعادل کامل
```
LL = 0, LR = 0, NL = 300, NR = 300, MX = 500
→ TB = 300, RNWL = 0, RNWR = 0, FL = 0, FR = 0
```
**نتیجه:** کمیسیون کامل بدون فلش ✅
---
### سناریو 2: یک پا خیلی بیشتر
```
LL = 0, LR = 0, NL = 1000, NR = 100, MX = 500
→ TB = 100, RNWL = 900, RNWR = 0, FL = 400, FR = 0
```
**نتیجه:** کمیسیون کم + فلش زیاد ❌
---
### سناریو 3: باقیمانده قبلی موثر
```
LL = 400, LR = 0, NL = 100, NR = 400, MX = 300
→ SLT = 500, SRT = 400
→ TB = 300, RNWL = 100, RNWR = 0, FL = 100, FR = 100
```
**نتیجه:** باقیمانده قبلی در محاسبه کمیسیون موثر است ✅
---
## تفاوت با کد فعلی
### در کد فعلی (`CalculateWeeklyBalancesCommandHandler.cs`):
```csharp
// 1. ابتدا Cap اعمال می‌شود
var cappedLeft = Math.Min(leftLegTotal, maxBalance);
var cappedRight = Math.Min(rightLegTotal, maxBalance);
// 2. سپس تعادل محاسبه می‌شود
var balance = Math.Min(cappedLeft, cappedRight);
// 3. باقیمانده‌ها محاسبه می‌شوند
var leftRemainder = leftLegTotal - balance;
var rightRemainder = rightLegTotal - balance;
```
### در فرمول اکسل:
```csharp
// 1. ابتدا تعادل کامل محاسبه می‌شود
var minTotal = Math.Min(leftLegTotal, rightLegTotal);
// 2. سپس Cap اعمال می‌شود
var balance = Math.Min(minTotal, maxBalance);
// 3. باقیمانده‌ها بر اساس minTotal محاسبه می‌شوند
var leftRemainder = leftLegTotal - minTotal;
var rightRemainder = rightLegTotal - minTotal;
// 4. فلش محاسبه می‌شود
var flushLeft = leftLegTotal - maxBalance - leftRemainder;
var flushRight = rightLegTotal - maxBalance - rightRemainder;
```
**تفاوت کلیدی:**
- کد فعلی Cap را ابتدا اعمال می‌کند (می‌تواند باقیمانده‌های بیشتری ایجاد کند)
- فرمول اکسل ابتدا تعادل را محاسبه می‌کند، سپس Cap اعمال می‌شود (فلش دقیق‌تر محاسبه می‌شود)
---
## نتیجه‌گیری
این فرمول‌ها نشان می‌دهند که:
1.**تعادل اهمیت دارد** - هرچه دو پا متعادل‌تر باشند، فلش کمتر است
2.**Cap محدودیت ایجاد می‌کند** - حتی با تعادل کامل، بیش از MX کمیسیون داده نمی‌شود
3.**باقیمانده‌ها منتقل می‌شوند** - برای هفته بعد ذخیره می‌شوند
4.**فلش ضرر است** - مقداری که به دلیل عدم تعادل یا Cap از دست می‌رود
**توصیه:** برای افزایش کمیسیون، کاربران باید:
- دو پای خود را متعادل نگه دارند
- سطح عضویت خود را ارتقا دهند (برای افزایش MX)
- از باقیمانده‌ها در هفته‌های بعد استفاده کنند