380 lines
12 KiB
Markdown
380 lines
12 KiB
Markdown
# ✅ اصلاح محاسبه کمیسیون هفتگی - تحلیل و پیادهسازی
|
||
|
||
**تاریخ شروع**: ۱۴ آذر ۱۴۰۴ (2025-12-04)
|
||
**تاریخ تکمیل**: ۱۴ آذر ۱۴۰۴
|
||
**وضعیت**: ✅ تکمیل شد
|
||
**اولویت**: 🔴 بحرانی - تأثیر مستقیم بر بیزینس
|
||
|
||
---
|
||
|
||
## 📊 خلاصه مشکلات
|
||
|
||
### مشکل ۱: محدودیت لول (Max Network Level) پیادهسازی نشده
|
||
- **مشکل**: شمارش اعضا بدون محدودیت عمق انجام میشود
|
||
- **انتظار**: فقط تا ۱۵ لول پایینتر باید شمارش شود
|
||
- **راهحل**: اضافه کردن پارامتر `maxLevel` به متد بازگشتی و خواندن از Config
|
||
|
||
### مشکل ۲: تعادل شخص vs تعادل شبکه (بحرانی)
|
||
- **مشکل**: کمیسیون بر اساس تعادل شخصی محاسبه میشود (نه مجموع زیرمجموعه)
|
||
- **انتظار**: کمیسیون = (تعادل شخص + تعادل زیرمجموعه تا ۱۵ لول) × ارزش هر تعادل
|
||
- **راهحل**: محاسبه تعادلهای زیرمجموعه در ProcessUserPayouts
|
||
|
||
---
|
||
|
||
## 🎯 قانون صحیح کمیسیون (بیزینس)
|
||
|
||
### فرمول محاسبه کمیسیون هفتگی:
|
||
|
||
```
|
||
1️⃣ محاسبه تعادل هر شخص:
|
||
- تعادل_شخص = MIN(چپ، راست)
|
||
- سقف هر دست = 300
|
||
- حداکثر تعادل شخصی = 300
|
||
|
||
2️⃣ محاسبه کل تعادلهای شبکه:
|
||
- کل_تعادل_شبکه = SUM(تعادل_شخصی همه اعضا)
|
||
|
||
3️⃣ محاسبه صندوق:
|
||
- صندوق_هفتگی = SUM(سهم_استخر همه اعضا)
|
||
- سهم_استخر هر عضو = تعداد_زیرمجموعه_جدید × هزینه_فعالسازی × ۲۰%
|
||
|
||
4️⃣ ارزش هر تعادل:
|
||
- ارزش_هر_تعادل = صندوق_هفتگی ÷ کل_تعادل_شبکه
|
||
|
||
5️⃣ کمیسیون هر شخص:
|
||
- مجموع_تعادل = تعادل_شخص + SUM(تعادل_زیرمجموعه تا 15 لول)
|
||
- کمیسیون = مجموع_تعادل × ارزش_هر_تعادل
|
||
```
|
||
|
||
### مثال عملی:
|
||
|
||
```
|
||
شبکه:
|
||
User A
|
||
├─ Left: User B (تعادل: 5)
|
||
│ ├─ Left: User D (تعادل: 2)
|
||
│ └─ Right: User E (تعادل: 1)
|
||
└─ Right: User C (تعادل: 3)
|
||
└─ Left: User F (تعادل: 1)
|
||
|
||
فرض: تعادل شخصی User A = 10
|
||
|
||
محاسبه مجموع تعادل User A (تا 15 لول):
|
||
= 10 + 5 + 2 + 1 + 3 + 1 = 22 تعادل
|
||
|
||
اگر ارزش هر تعادل = 1,000,000 ریال:
|
||
کمیسیون User A = 22 × 1,000,000 = 22,000,000 ریال
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 تحلیل کد فعلی
|
||
|
||
### فایلهای تأثیرپذیر:
|
||
|
||
| # | فایل | وضعیت فعلی | نیاز به تغییر |
|
||
|---|------|------------|---------------|
|
||
| 1 | `ApplicationDbContextInitialiser.cs` | ندارد `MaxNetworkLevel` | ✅ اضافه Config |
|
||
| 2 | `CalculateWeeklyBalancesCommandHandler.cs` | بدون محدودیت لول | ✅ اضافه maxLevel |
|
||
| 3 | `ProcessUserPayoutsCommandHandler.cs` | فقط تعادل شخص | ✅ جمع زیرمجموعه |
|
||
| 4 | `NetworkWeeklyBalance.cs` | Entity | ⚪ نیاز ندارد |
|
||
| 5 | `UserCommissionPayout.cs` | Entity | 🟡 شاید فیلد جدید |
|
||
|
||
### کد فعلی `ProcessUserPayoutsCommandHandler`:
|
||
|
||
```csharp
|
||
// ❌ مشکل: فقط تعادل شخصی
|
||
foreach (var balance in weeklyBalances)
|
||
{
|
||
var totalAmount = (long)(balance.TotalBalances * pool.ValuePerBalance);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### کد صحیح باید باشد:
|
||
|
||
```csharp
|
||
// ✅ صحیح: تعادل شخصی + زیرمجموعه تا 15 لول
|
||
foreach (var balance in weeklyBalances)
|
||
{
|
||
// محاسبه مجموع تعادلهای زیرمجموعه
|
||
var subordinateBalances = await CalculateSubordinateBalances(
|
||
balance.UserId,
|
||
request.WeekNumber,
|
||
maxNetworkLevel, // از Config
|
||
cancellationToken
|
||
);
|
||
|
||
var totalBalancesWithSubordinates = balance.TotalBalances + subordinateBalances;
|
||
var totalAmount = (long)(totalBalancesWithSubordinates * pool.ValuePerBalance);
|
||
// ...
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 تسکهای اجرایی
|
||
|
||
### فاز ۱: Configuration (نیم روز)
|
||
|
||
#### تسک ۱.۱: اضافه کردن MaxNetworkLevel به Seed Data
|
||
```csharp
|
||
// ApplicationDbContextInitialiser.cs
|
||
new SystemConfiguration
|
||
{
|
||
Key = "Commission.MaxNetworkLevel",
|
||
Value = "15",
|
||
Description = "حداکثر عمق شبکه برای محاسبه کمیسیون (تعداد لول)",
|
||
Scope = ConfigurationScope.Commission,
|
||
IsActive = true
|
||
}
|
||
```
|
||
|
||
#### تسک ۱.۲: Migration (در صورت نیاز)
|
||
- اگر دیتابیس موجود دارید، یک SQL Script یا Migration
|
||
|
||
---
|
||
|
||
### فاز ۲: اصلاح CalculateWeeklyBalances (نیم روز)
|
||
|
||
#### تسک ۲.۱: خواندن MaxNetworkLevel از Config
|
||
```csharp
|
||
// در Handle method
|
||
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
|
||
```
|
||
|
||
#### تسک ۲.۲: اضافه کردن محدودیت لول به متد بازگشتی
|
||
```csharp
|
||
private async Task<int> CountNewMembersRecursive(
|
||
long userId,
|
||
NetworkLeg leg,
|
||
DateTime startDate,
|
||
DateTime endDate,
|
||
int currentLevel, // ← جدید
|
||
int maxLevel, // ← جدید
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// ⬅️ محدودیت عمق
|
||
if (currentLevel >= maxLevel)
|
||
return 0;
|
||
|
||
var child = await _context.Users
|
||
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
|
||
|
||
if (child == null)
|
||
return 0;
|
||
|
||
// ... محاسبه count ...
|
||
|
||
// ⬅️ افزایش سطح
|
||
var childLeft = await CountNewMembersRecursive(child.Id, NetworkLeg.Left, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||
var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||
|
||
return count + childLeft + childRight;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### فاز ۳: اصلاح ProcessUserPayouts (۱ روز)
|
||
|
||
#### تسک ۳.۱: اضافه کردن متد محاسبه تعادل زیرمجموعه
|
||
```csharp
|
||
/// <summary>
|
||
/// محاسبه مجموع تعادلهای زیرمجموعه یک کاربر تا N لول
|
||
/// </summary>
|
||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||
long userId,
|
||
string weekNumber,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var totalSubordinateBalances = 0;
|
||
|
||
// پیدا کردن همه زیرمجموعهها تا maxLevel
|
||
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
|
||
|
||
// جمع تعادلهای آنها
|
||
foreach (var subordinateId in subordinates)
|
||
{
|
||
var balance = await _context.NetworkWeeklyBalances
|
||
.Where(x => x.UserId == subordinateId && x.WeekNumber == weekNumber)
|
||
.Select(x => x.TotalBalances)
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
|
||
totalSubordinateBalances += balance;
|
||
}
|
||
|
||
return totalSubordinateBalances;
|
||
}
|
||
|
||
/// <summary>
|
||
/// پیدا کردن بازگشتی زیرمجموعهها
|
||
/// </summary>
|
||
private async Task<List<long>> GetSubordinatesRecursive(
|
||
long userId,
|
||
int currentLevel,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
if (currentLevel > maxLevel)
|
||
return new List<long>();
|
||
|
||
var result = new List<long>();
|
||
|
||
// پیدا کردن فرزندان مستقیم
|
||
var children = await _context.Users
|
||
.Where(x => x.NetworkParentId == userId)
|
||
.Select(x => x.Id)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
result.AddRange(children);
|
||
|
||
// بازگشت برای هر فرزند
|
||
foreach (var childId in children)
|
||
{
|
||
var grandChildren = await GetSubordinatesRecursive(childId, currentLevel + 1, maxLevel, cancellationToken);
|
||
result.AddRange(grandChildren);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
#### تسک ۳.۲: اصلاح Handle method
|
||
```csharp
|
||
public async Task<int> Handle(ProcessUserPayoutsCommand request, CancellationToken cancellationToken)
|
||
{
|
||
// ... کدهای موجود ...
|
||
|
||
// خواندن MaxNetworkLevel از Config
|
||
var maxNetworkLevel = await _context.SystemConfigurations
|
||
.Where(x => x.Key == "Commission.MaxNetworkLevel" && x.IsActive)
|
||
.Select(x => x.Value)
|
||
.FirstOrDefaultAsync(cancellationToken);
|
||
var maxLevel = int.Parse(maxNetworkLevel ?? "15");
|
||
|
||
foreach (var balance in weeklyBalances)
|
||
{
|
||
// ✅ محاسبه تعادل شخص + زیرمجموعه
|
||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||
balance.UserId,
|
||
request.WeekNumber,
|
||
maxLevel,
|
||
cancellationToken
|
||
);
|
||
|
||
var totalBalancesWithSubordinates = balance.TotalBalances + subordinateBalances;
|
||
var totalAmount = (long)(totalBalancesWithSubordinates * pool.ValuePerBalance);
|
||
|
||
var payout = new UserCommissionPayout
|
||
{
|
||
UserId = balance.UserId,
|
||
WeekNumber = request.WeekNumber,
|
||
WeeklyPoolId = pool.Id,
|
||
BalancesEarned = totalBalancesWithSubordinates, // ← شامل زیرمجموعه
|
||
ValuePerBalance = pool.ValuePerBalance,
|
||
TotalAmount = totalAmount,
|
||
// ...
|
||
};
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
#### تسک ۳.۳ (اختیاری): اضافه کردن فیلد به Entity
|
||
```csharp
|
||
// UserCommissionPayout.cs
|
||
/// <summary>
|
||
/// تعادل شخصی (بدون زیرمجموعه)
|
||
/// </summary>
|
||
public int PersonalBalances { get; set; }
|
||
|
||
/// <summary>
|
||
/// تعادل زیرمجموعهها
|
||
/// </summary>
|
||
public int SubordinateBalances { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع (PersonalBalances + SubordinateBalances)
|
||
/// </summary>
|
||
public int BalancesEarned { get; set; } // ← قبلاً هم بود
|
||
```
|
||
|
||
---
|
||
|
||
### فاز ۴: تست و Build (نیم روز)
|
||
|
||
#### تسک ۴.۱: Build و رفع خطاها
|
||
```bash
|
||
cd CMS/src && dotnet build
|
||
```
|
||
|
||
#### تسک ۴.۲: تست با سناریوهای مختلف
|
||
- کاربر بدون زیرمجموعه
|
||
- کاربر با ۵ لول زیرمجموعه
|
||
- کاربر با ۲۰ لول (باید ۱۵ تا بشمارد)
|
||
- کاربر با سقف ۳۰۰ در هر دست
|
||
|
||
---
|
||
|
||
## ⏱️ زمانبندی
|
||
|
||
| فاز | تسک | زمان | مجموع |
|
||
|-----|-----|------|-------|
|
||
| ۱ | Config + Seed | 0.5 روز | 0.5 روز |
|
||
| ۲ | اصلاح CalculateWeeklyBalances | 0.5 روز | 1 روز |
|
||
| ۳ | اصلاح ProcessUserPayouts | 1 روز | 2 روز |
|
||
| ۴ | تست و Build | 0.5 روز | 2.5 روز |
|
||
|
||
**مجموع**: ۲.۵ روز کاری
|
||
|
||
---
|
||
|
||
## ⚠️ نکات مهم
|
||
|
||
1. **تغییرات Breaking نیست**: ساختار Entity تغییر نمیکند (فقط مقادیر)
|
||
2. **Backward Compatible**: فیلد `BalancesEarned` قبلاً هم بود
|
||
3. **Idempotent**: با `ForceRecalculate` میتوان دوباره حساب کرد
|
||
4. **Performance**: متد بازگشتی ممکن است کند باشد - بهینهسازی در فاز بعد
|
||
5. **Migration**: فقط اگر فیلد جدید به Entity اضافه شود
|
||
|
||
---
|
||
|
||
## ✅ وضعیت پیادهسازی - تکمیل شده
|
||
|
||
**تاریخ تکمیل**: ۱۴ آذر ۱۴۰۴
|
||
|
||
| فاز | شرح | وضعیت |
|
||
|-----|-----|------|
|
||
| 1 | Config: `Commission.MaxNetworkLevel = 15` | ✅ تکمیل |
|
||
| 2 | CalculateWeeklyBalances: محدودیت ۱۵ لول | ✅ تکمیل |
|
||
| 3 | ProcessUserPayouts: جمع تعادل زیرمجموعه | ✅ تکمیل |
|
||
| 4 | Build Test: 0 Errors | ✅ تکمیل |
|
||
|
||
### تغییرات انجام شده:
|
||
|
||
**Seed Data:**
|
||
- ✅ `Commission.MaxNetworkLevel = 15`
|
||
|
||
**CalculateWeeklyBalancesCommandHandler:**
|
||
- ✅ خواندن `maxNetworkLevel` از Config
|
||
- ✅ پارامتر `maxLevel` به `CountNewMembersInLeg`
|
||
- ✅ پارامتر `currentLevel` و `maxLevel` به `CountNewMembersRecursive`
|
||
- ✅ شرط توقف در عمق ۱۵
|
||
|
||
**ProcessUserPayoutsCommandHandler:**
|
||
- ✅ متد جدید `SumSubordinateBalancesAsync`
|
||
- ✅ متد کمکی `GetChildUserIdAsync`
|
||
- ✅ محاسبه `subordinateBalances` برای هر کاربر
|
||
- ✅ کمیسیون = (شخص + زیرمجموعه) × ارزش هر تعادل
|
||
|
||
---
|
||
|
||
## 🎉 نتیجه نهایی
|
||
|
||
```
|
||
✅ Build Succeeded - 0 Errors
|
||
✅ همه فازها تکمیل شدند
|
||
✅ منطق کمیسیون اصلاح شد
|
||
``` |