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.
1365 lines
38 KiB
Markdown
1365 lines
38 KiB
Markdown
# سیستم باشگاه مشتریان و کمیسیون شبکه - مستندات کامل و نهایی
|
||
|
||
**تاریخ آخرین بروزرسانی**: ۲۱ آذر ۱۴۰۴ (2025-12-12)
|
||
**وضعیت**: ✅ تکمیل شده و عملیاتی
|
||
**نسخه**: 2.0 (بازنگری شده)
|
||
|
||
---
|
||
|
||
## 📑 فهرست مطالب
|
||
|
||
1. [خلاصه اجرایی](#خلاصه-اجرایی)
|
||
2. [معماری سیستم](#معماری-سیستم)
|
||
3. [فرآیند فعالسازی باشگاه](#فرآیند-فعالسازی-باشگاه)
|
||
4. [محاسبات Binary Plan](#محاسبات-binary-plan)
|
||
5. [فرآیند محاسبه کمیسیون هفتگی](#فرآیند-محاسبه-کمیسیون-هفتگی)
|
||
6. [موجودیتهای دامین](#موجودیتهای-دامین)
|
||
7. [جزئیات پیادهسازی](#جزئیات-پیادهسازی)
|
||
8. [مثالهای عملی](#مثالهای-عملی)
|
||
|
||
---
|
||
|
||
## خلاصه اجرایی
|
||
|
||
### هدف سیستم
|
||
سیستم باشگاه مشتریان (Club Membership) با پلن شبکهای Binary MLM که امکانات زیر را فراهم میکند:
|
||
|
||
✅ **مدیریت سه نوع کیف پول** برای هر کاربر
|
||
✅ **فروشگاه اختصاصی** با تخفیف ویژه اعضای باشگاه
|
||
✅ **شبکه باینری** با حداکثر 2 شاخه برای هر کاربر
|
||
✅ **محاسبه خودکار کمیسیون** بر اساس تعادل شبکه (هفتگی)
|
||
✅ **توزیع عادلانه Pool** بین اعضا بر اساس امتیازات
|
||
|
||
### تغییرات اساسی نسخه 2.0
|
||
|
||
| مورد | قبل (v1.0) | بعد (v2.0) |
|
||
|------|-----------|-----------|
|
||
| **تعداد مراحل محاسبه** | 3 مرحله | 2 مرحله ✅ |
|
||
| **منبع Pool هفتگی** | از تعادلهای کاربران | از فعالسازی باشگاه ✅ |
|
||
| **شمول زیرمجموعه** | فقط تعادل شخصی | تعادل + زیرمجموعه (15 لول) ✅ |
|
||
| **فیلتر شرکتکنندگان** | همه کاربران شبکه | فقط اعضای فعال باشگاه ✅ |
|
||
| **ذخیره فلش** | محاسبه در لحظه | ذخیره در دیتابیس ✅ |
|
||
|
||
---
|
||
|
||
## معماری سیستم
|
||
|
||
### 1. کیف پولهای سهگانه
|
||
|
||
هر کاربر دارای **سه کیف پول مجزا** است:
|
||
|
||
#### 1️⃣ کیف پول اصلی (`Balance`)
|
||
- **کاربری**: خرید از فروشگاه عمومی بازار
|
||
- **شارژ**: درگاه پرداخت یا خرید از دایا
|
||
- **قابل برداشت**: خیر
|
||
|
||
#### 2️⃣ کیف پول تخفیف (`DiscountBalance`)
|
||
- **کاربری**: خرید از فروشگاه باشگاه مشتریان (با تخفیف ویژه)
|
||
- **شارژ**: هنگام فعالسازی عضویت باشگاه
|
||
- **محدودیت**: فقط تا سقف درصد تخفیف محصول قابل استفاده
|
||
- **قابل برداشت**: خیر
|
||
|
||
#### 3️⃣ کیف پول طلایی/شبکه (`NetworkBalance`)
|
||
- **کاربری**:
|
||
- برداشت نقدی
|
||
- خرید الماس از دایا
|
||
- **شارژ**: دریافت کمیسیون هفتگی از شبکه
|
||
- **قابل برداشت**: بله ✅
|
||
|
||
---
|
||
|
||
### 2. شبکه باینری (Binary Tree)
|
||
|
||
```
|
||
User A (Root)
|
||
/ \
|
||
User B (Left) User C (Right)
|
||
/ \ / \
|
||
User D (L) User E (R) User F (L) User G (R)
|
||
```
|
||
|
||
**قوانین:**
|
||
- هر کاربر حداکثر **2 زیرمجموعه مستقیم** دارد
|
||
- موقعیتها: `Left` (چپ) یا `Right` (راست)
|
||
- عمق شبکه: نامحدود (اما محاسبات فقط تا **15 لول**)
|
||
- ریشه شبکه (`NetworkParentId = NULL`): فقط یک نفر
|
||
|
||
**فیلدهای مرتبط در `User` Entity:**
|
||
```csharp
|
||
public long? NetworkParentId { get; set; } // پدر در شبکه
|
||
public NetworkLeg? LegPosition { get; set; } // چپ یا راست
|
||
```
|
||
|
||
---
|
||
|
||
## فرآیند فعالسازی باشگاه
|
||
|
||
### مرحله 1: شارژ کیف پول (پیشنیاز)
|
||
|
||
کاربر **56,000,000 ریال** پرداخت میکند:
|
||
|
||
```csharp
|
||
// از طریق درگاه یا دایا
|
||
User.Balance += 56_000_000;
|
||
User.DiscountBalance += 56_000_000;
|
||
```
|
||
|
||
**نکته**: دو کیف پول همزمان شارژ میشوند.
|
||
|
||
---
|
||
|
||
### مرحله 2: فعالسازی عضویت (`ActivateClubMembership`)
|
||
|
||
**ورودیها:**
|
||
```csharp
|
||
{
|
||
"userId": 123,
|
||
"networkParentId": 45, // پدر در شبکه
|
||
"legPosition": "Left" // چپ یا راست
|
||
}
|
||
```
|
||
|
||
**فرآیند اجرایی:**
|
||
|
||
#### 2.1. اعتبارسنجی
|
||
```csharp
|
||
✅ User.Balance >= ActivationFee (25,000,000)
|
||
✅ NetworkParent وجود دارد
|
||
✅ موقعیت انتخابی (Left/Right) خالی است
|
||
✅ کاربر قبلاً عضو نیست
|
||
```
|
||
|
||
#### 2.2. کسر از کیف پول
|
||
```csharp
|
||
User.Balance -= 25_000_000;
|
||
```
|
||
|
||
#### 2.3. ایجاد عضویت باشگاه
|
||
```csharp
|
||
ClubMembership clubMembership = new()
|
||
{
|
||
UserId = userId,
|
||
IsActive = true,
|
||
ActivatedAt = DateTime.Now,
|
||
InitialContribution = 25_000_000,
|
||
TotalEarned = 0
|
||
};
|
||
```
|
||
|
||
#### 2.4. قرارگیری در شبکه
|
||
```csharp
|
||
User.NetworkParentId = networkParentId;
|
||
User.LegPosition = legPosition; // Left or Right
|
||
```
|
||
|
||
#### 2.5. اضافه به Pool هفتگی ⭐ **جدید در v2.0**
|
||
```csharp
|
||
// دریافت شماره هفته جاری (فرمت: "2025-W48")
|
||
var currentWeekNumber = GetCurrentWeekNumber();
|
||
|
||
var weeklyPool = await _context.WeeklyCommissionPools
|
||
.FirstOrDefaultAsync(p => p.WeekNumber == currentWeekNumber);
|
||
|
||
if (weeklyPool == null)
|
||
{
|
||
// ایجاد Pool جدید برای این هفته
|
||
weeklyPool = new WeeklyCommissionPool
|
||
{
|
||
WeekNumber = currentWeekNumber,
|
||
TotalPoolAmount = 25_200_000, // GiftValue
|
||
TotalBalances = 0,
|
||
ValuePerBalance = 0,
|
||
IsCalculated = false
|
||
};
|
||
await _context.WeeklyCommissionPools.AddAsync(weeklyPool);
|
||
}
|
||
else
|
||
{
|
||
// اضافه به Pool موجود
|
||
weeklyPool.TotalPoolAmount += 25_200_000;
|
||
_context.WeeklyCommissionPools.Update(weeklyPool);
|
||
}
|
||
```
|
||
|
||
**نکته مهم**:
|
||
- کاربر `25,000,000` پرداخت میکند
|
||
- سیستم `25,200,000` به Pool اضافه میکند (Gift از شرکت)
|
||
|
||
#### 2.6. اختصاص امکانات باشگاه
|
||
```csharp
|
||
// ۴ فیچر پایه باشگاه
|
||
var defaultFeatures = await _context.ClubFeatures
|
||
.Where(f => f.IsActive && f.RequiredPoints == null)
|
||
.ToListAsync();
|
||
|
||
foreach (var feature in defaultFeatures)
|
||
{
|
||
UserClubFeature userFeature = new()
|
||
{
|
||
UserId = userId,
|
||
ClubFeatureId = feature.Id,
|
||
GrantedAt = DateTime.Now,
|
||
Notes = "فیچر پیشفرض عضویت باشگاه"
|
||
};
|
||
await _context.UserClubFeatures.AddAsync(userFeature);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## محاسبات Binary Plan
|
||
|
||
### فرمولهای اصلی
|
||
|
||
این فرمولها از فایل اکسل کسبوکار استخراج شدهاند:
|
||
|
||
#### 1️⃣ محاسبه مجموع هر پا
|
||
|
||
```
|
||
LeftTotal = LeftCarryover + LeftNewMembers
|
||
RightTotal = RightCarryover + RightNewMembers
|
||
```
|
||
|
||
**مثال:**
|
||
```
|
||
هفته قبل: چپ=200, راست=0
|
||
این هفته: چپ=400, راست=500
|
||
|
||
LeftTotal = 200 + 400 = 600
|
||
RightTotal = 0 + 500 = 500
|
||
```
|
||
|
||
---
|
||
|
||
#### 2️⃣ محاسبه تعادل اولیه (قبل از سقف)
|
||
|
||
```
|
||
TotalBalances (initial) = MIN(LeftTotal, RightTotal)
|
||
```
|
||
|
||
**مثال:**
|
||
```
|
||
TotalBalances = MIN(600, 500) = 500
|
||
```
|
||
|
||
**معنی**: کوچکترین پا تعیینکننده تعادل است.
|
||
|
||
---
|
||
|
||
#### 3️⃣ اعمال سقف (Cap)
|
||
|
||
```
|
||
MaxBalancesPerLeg = 300 (از Config)
|
||
|
||
CappedBalances = MIN(TotalBalances, MaxBalancesPerLeg)
|
||
```
|
||
|
||
**مثال:**
|
||
```
|
||
CappedBalances = MIN(500, 300) = 300
|
||
```
|
||
|
||
**معنی**: حداکثر امتیاز قابل دریافت در هر هفته **300** است.
|
||
|
||
---
|
||
|
||
#### 4️⃣ محاسبه فلش (Flush - از دست رفته)
|
||
|
||
```
|
||
FlushedPerSide = TotalBalances - CappedBalances
|
||
TotalFlushed = FlushedPerSide × 2
|
||
```
|
||
|
||
**مثال:**
|
||
```
|
||
FlushedPerSide = 500 - 300 = 200
|
||
TotalFlushed = 200 × 2 = 400
|
||
```
|
||
|
||
**معنی**:
|
||
- از **چپ**: 600 → 300 استفاده شد → **200 فلش**
|
||
- از **راست**: 500 → 300 استفاده شد → **200 فلش**
|
||
- جمع فلش: **400** (از بین رفت) ❌
|
||
|
||
---
|
||
|
||
#### 5️⃣ محاسبه باقیمانده برای هفته بعد
|
||
|
||
```
|
||
LeftRemainder = LeftTotal - TotalBalances
|
||
RightRemainder = RightTotal - TotalBalances
|
||
```
|
||
|
||
**مثال:**
|
||
```
|
||
LeftRemainder = 600 - 500 = 100 ✅ به هفته بعد منتقل میشود
|
||
RightRemainder = 500 - 500 = 0
|
||
```
|
||
|
||
**نکته**: یکی از دو باقیمانده همیشه **صفر** است.
|
||
|
||
---
|
||
|
||
### جدول خلاصه محاسبات (مثال واقعی از Excel)
|
||
|
||
| متغیر | نماد | مقدار | توضیح |
|
||
|-------|------|-------|-------|
|
||
| باقیمانده قبل چپ | `LL` | 200 | از هفته قبل |
|
||
| باقیمانده قبل راست | `LR` | 0 | از هفته قبل |
|
||
| جدید چپ | `NL` | 400 | این هفته |
|
||
| جدید راست | `NR` | 500 | این هفته |
|
||
| **مجموع چپ** | `SLT` | **600** | LL + NL |
|
||
| **مجموع راست** | `SRT` | **500** | LR + NR |
|
||
| کمترین | `MinT` | 500 | MIN(SLT, SRT) |
|
||
| سقف | `MX` | 300 | از Config |
|
||
| **امتیاز نهایی** | `TB` | **300** | MIN(MinT, MX) ✅ |
|
||
| فلش چپ | `FL` | 200 | SLT - MX - RNWL |
|
||
| فلش راست | `FR` | 200 | SRT - MX - RNWR |
|
||
| باقی چپ | `RNWL` | 100 | به هفته بعد |
|
||
| باقی راست | `RNWR` | 0 | - |
|
||
|
||
---
|
||
|
||
## فرآیند محاسبه کمیسیون هفتگی
|
||
|
||
### تغییر معماری: 3 مرحله → 2 مرحله
|
||
|
||
#### ❌ معماری قدیم (v1.0)
|
||
```
|
||
Step 1: CalculateWeeklyBalances
|
||
└─ محاسبه تعادلهای شخصی
|
||
|
||
Step 2: CalculateWeeklyCommissionPool
|
||
└─ محاسبه Pool از تعادلها ❌ اشتباه بود!
|
||
└─ محاسبه تعادل زیرمجموعه (تکراری)
|
||
|
||
Step 3: ProcessUserPayouts
|
||
└─ ایجاد پرداختها (تکراری)
|
||
```
|
||
|
||
#### ✅ معماری جدید (v2.0)
|
||
```
|
||
Step 1: CalculateWeeklyBalances
|
||
└─ فقط اعضای فعال باشگاه
|
||
└─ محاسبه تعادل شخصی (تا 15 لول)
|
||
└─ محاسبه تعادل زیرمجموعه (تا 15 لول)
|
||
└─ ذخیره فلش
|
||
|
||
Step 2: CalculateWeeklyCommissionPool
|
||
└─ Pool از قبل پُر شده (در ActivateClubMembership)
|
||
└─ محاسبه ارزش هر امتیاز
|
||
└─ ایجاد UserCommissionPayout
|
||
└─ ثبت تاریخچه
|
||
```
|
||
|
||
---
|
||
|
||
### Step 1: محاسبه تعادلهای هفتگی (`CalculateWeeklyBalances`)
|
||
|
||
**ورودی:**
|
||
```csharp
|
||
{
|
||
"weekNumber": "2025-W48",
|
||
"forceRecalculate": false
|
||
}
|
||
```
|
||
|
||
**فرآیند:**
|
||
|
||
#### 1.1. فیلتر کاربران شرکتکننده
|
||
|
||
```csharp
|
||
// فقط اعضای فعال باشگاه (بدون محدودیت زمانی)
|
||
var activeClubMemberUserIds = await _context.ClubMemberships
|
||
.Where(c => c.IsActive)
|
||
.Select(c => c.UserId)
|
||
.ToHashSetAsync();
|
||
|
||
// دریافت کاربران شبکه که عضو باشگاه هستند
|
||
// نکته: شامل ریشه شبکه (NetworkParentId=NULL) هم میشود
|
||
var usersInNetwork = await _context.Users
|
||
.Where(x => activeClubMemberUserIds.Contains(x.Id))
|
||
.Select(x => new { x.Id })
|
||
.ToListAsync();
|
||
```
|
||
|
||
**چرا فیلتر زمانی نداریم؟**
|
||
- همه کسانی که **الان** عضو باشگاه هستند باید کمیسیون بگیرند
|
||
- حتی اگر 10 هفته پیش فعال شده باشند
|
||
|
||
---
|
||
|
||
#### 1.2. دریافت باقیمانده هفته قبل
|
||
|
||
```csharp
|
||
var previousWeekNumber = GetPreviousWeekNumber(request.WeekNumber);
|
||
// مثال: "2025-W48" → "2025-W47"
|
||
|
||
var previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||
.Where(x => x.WeekNumber == previousWeekNumber)
|
||
.Select(x => new
|
||
{
|
||
x.UserId,
|
||
x.LeftLegRemainder,
|
||
x.RightLegRemainder
|
||
})
|
||
.ToDictionaryAsync(x => x.UserId);
|
||
```
|
||
|
||
---
|
||
|
||
#### 1.3. خواندن Config ها
|
||
|
||
```csharp
|
||
var configs = await _context.SystemConfigurations
|
||
.Where(x => x.IsActive && (
|
||
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
|
||
x.Key == "Commission.MaxNetworkLevel"))
|
||
.ToDictionaryAsync(x => x.Key, x => x.Value);
|
||
|
||
var maxBalancesPerLeg = int.Parse(configs["Commission.MaxWeeklyBalancesPerLeg"]); // 300
|
||
var maxNetworkLevel = int.Parse(configs["Commission.MaxNetworkLevel"]); // 15
|
||
```
|
||
|
||
---
|
||
|
||
#### 1.4. محاسبه برای هر کاربر
|
||
|
||
```csharp
|
||
foreach (var user in usersInNetwork)
|
||
{
|
||
// 1. دریافت باقیمانده هفته قبل
|
||
var leftCarryover = previousWeekCarryovers.ContainsKey(user.Id)
|
||
? previousWeekCarryovers[user.Id].LeftLegRemainder
|
||
: 0;
|
||
var rightCarryover = previousWeekCarryovers.ContainsKey(user.Id)
|
||
? previousWeekCarryovers[user.Id].RightLegRemainder
|
||
: 0;
|
||
|
||
// 2. شمارش اعضای جدید (تا 15 لول)
|
||
var leftNewMembers = await CountNewMembersInLeg(
|
||
user.Id, NetworkLeg.Left, weekNumber, maxNetworkLevel);
|
||
var rightNewMembers = await CountNewMembersInLeg(
|
||
user.Id, NetworkLeg.Right, weekNumber, maxNetworkLevel);
|
||
|
||
// 3. محاسبه مجموع
|
||
var leftTotal = leftNewMembers + leftCarryover;
|
||
var rightTotal = rightNewMembers + rightCarryover;
|
||
|
||
// 4. محاسبه تعادل اولیه
|
||
var totalBalances = Math.Min(leftTotal, rightTotal);
|
||
|
||
// 5. اعمال سقف 300
|
||
var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
|
||
|
||
// 6. محاسبه فلش
|
||
var flushedPerSide = totalBalances - cappedBalances;
|
||
var totalFlushed = flushedPerSide * 2;
|
||
|
||
// 7. محاسبه باقیمانده
|
||
var leftRemainder = leftTotal - totalBalances;
|
||
var rightRemainder = rightTotal - totalBalances;
|
||
|
||
// 8. ذخیره در دیتابیس
|
||
var balance = new NetworkWeeklyBalance
|
||
{
|
||
UserId = user.Id,
|
||
WeekNumber = weekNumber,
|
||
LeftLegNewMembers = leftNewMembers,
|
||
RightLegNewMembers = rightNewMembers,
|
||
LeftLegCarryover = leftCarryover,
|
||
RightLegCarryover = rightCarryover,
|
||
LeftLegTotal = leftTotal,
|
||
RightLegTotal = rightTotal,
|
||
TotalBalances = cappedBalances, // امتیاز نهایی (300)
|
||
LeftLegRemainder = leftRemainder,
|
||
RightLegRemainder = rightRemainder,
|
||
FlushedPerSide = flushedPerSide, // جدید در v2.0
|
||
TotalFlushed = totalFlushed, // جدید در v2.0
|
||
SubordinateBalances = 0, // محاسبه در مرحله 2
|
||
WeeklyPoolContribution = 0,
|
||
CalculatedAt = DateTime.Now,
|
||
IsExpired = false
|
||
};
|
||
|
||
balancesList.Add(balance);
|
||
}
|
||
|
||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList);
|
||
await _context.SaveChangesAsync();
|
||
```
|
||
|
||
---
|
||
|
||
#### 1.5. محاسبه تعادل زیرمجموعه (فاز 2)
|
||
|
||
```csharp
|
||
// حالا که همه تعادلها ذخیره شدند، میتوانیم زیرمجموعهها را محاسبه کنیم
|
||
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
|
||
|
||
foreach (var balance in balancesList)
|
||
{
|
||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||
balance.UserId,
|
||
balancesDictionary,
|
||
maxNetworkLevel, // تا 15 لول
|
||
cancellationToken
|
||
);
|
||
|
||
balance.SubordinateBalances = subordinateBalances;
|
||
}
|
||
|
||
_context.NetworkWeeklyBalances.UpdateRange(balancesList);
|
||
await _context.SaveChangesAsync();
|
||
```
|
||
|
||
**الگوریتم `CalculateSubordinateBalancesAsync`:**
|
||
```csharp
|
||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||
long userId,
|
||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// 1. پیدا کردن همه زیرمجموعهها (تا maxLevel)
|
||
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel);
|
||
|
||
// 2. جمع تعادلهای آنها
|
||
var totalSubordinateBalances = 0;
|
||
foreach (var subordinateId in subordinates)
|
||
{
|
||
if (allBalances.ContainsKey(subordinateId))
|
||
{
|
||
totalSubordinateBalances += allBalances[subordinateId].TotalBalances;
|
||
}
|
||
}
|
||
|
||
return totalSubordinateBalances;
|
||
}
|
||
```
|
||
|
||
**نکته مهم**:
|
||
- `SubordinateBalances` فقط برای **گزارشگیری** ذخیره میشود
|
||
- در محاسبه Pool استفاده **نمیشود** (چون وقتی همه `TotalBalances` را جمع بزنیم، خودش شامل زیرمجموعهها هم هست)
|
||
|
||
---
|
||
|
||
### Step 2: محاسبه Pool و پرداختها (`CalculateWeeklyCommissionPool`)
|
||
|
||
**ورودی:**
|
||
```csharp
|
||
{
|
||
"weekNumber": "2025-W48",
|
||
"forceRecalculate": false
|
||
}
|
||
```
|
||
|
||
**فرآیند:**
|
||
|
||
#### 2.1. بررسی وجود Pool
|
||
|
||
```csharp
|
||
var existingPool = await _context.WeeklyCommissionPools
|
||
.FirstOrDefaultAsync(x => x.WeekNumber == request.WeekNumber);
|
||
|
||
if (existingPool == null)
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"Pool هفته {request.WeekNumber} وجود ندارد. " +
|
||
"Pool باید در هنگام فعالسازی باشگاه مشتریان ایجاد شده باشد"
|
||
);
|
||
}
|
||
```
|
||
|
||
**نکته کلیدی**: Pool از قبل توسط `ActivateClubMembership` پُر شده است! ✅
|
||
|
||
---
|
||
|
||
#### 2.2. دریافت تعادلهای محاسبه شده
|
||
|
||
```csharp
|
||
var weeklyBalances = await _context.NetworkWeeklyBalances
|
||
.Where(x => x.WeekNumber == request.WeekNumber)
|
||
.ToListAsync();
|
||
|
||
if (!weeklyBalances.Any())
|
||
{
|
||
throw new InvalidOperationException(
|
||
$"تعادلهای هفته {request.WeekNumber} هنوز محاسبه نشده است. " +
|
||
"ابتدا CalculateWeeklyBalances را اجرا کنید"
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### 2.3. محاسبه ارزش هر امتیاز
|
||
|
||
```csharp
|
||
// مجموع کل Pool (از فعالسازیهای باشگاه)
|
||
var totalPoolAmount = existingPool.TotalPoolAmount;
|
||
|
||
// مجموع کل تعادلهای شبکه
|
||
// نکته: SubordinateBalances اضافه نمیکنیم چون وقتی همه TotalBalances را
|
||
// جمع بزنیم، خودش شامل تعادلهای زیرمجموعهها هم هست (تکراری نشود)
|
||
var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances);
|
||
|
||
// ارزش هر امتیاز
|
||
long valuePerBalance = 0;
|
||
if (totalBalancesInNetwork > 0)
|
||
{
|
||
valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
|
||
}
|
||
|
||
// بهروزرسانی Pool
|
||
existingPool.TotalBalances = totalBalancesInNetwork;
|
||
existingPool.ValuePerBalance = valuePerBalance;
|
||
existingPool.IsCalculated = true;
|
||
existingPool.CalculatedAt = DateTime.Now;
|
||
|
||
_context.WeeklyCommissionPools.Update(existingPool);
|
||
await _context.SaveChangesAsync();
|
||
```
|
||
|
||
**مثال عددی:**
|
||
```
|
||
TotalPoolAmount = 252,000,000 ریال (10 نفر × 25.2M)
|
||
TotalBalances = 1,500 امتیاز
|
||
ValuePerBalance = 252,000,000 ÷ 1,500 = 168,000 ریال
|
||
```
|
||
|
||
---
|
||
|
||
#### 2.4. حذف پرداختهای قبلی (در صورت ForceRecalculate)
|
||
|
||
```csharp
|
||
if (request.ForceRecalculate)
|
||
{
|
||
var oldPayouts = await _context.UserCommissionPayouts
|
||
.Where(p => p.WeekNumber == request.WeekNumber)
|
||
.ToListAsync();
|
||
|
||
if (oldPayouts.Any())
|
||
{
|
||
var oldPayoutIds = oldPayouts.Select(p => p.Id).ToList();
|
||
|
||
// ⭐ اول تاریخچهها حذف شوند (FK constraint)
|
||
var oldHistories = await _context.CommissionPayoutHistories
|
||
.Where(h => oldPayoutIds.Contains(h.UserCommissionPayoutId))
|
||
.ToListAsync();
|
||
|
||
if (oldHistories.Any())
|
||
{
|
||
_context.CommissionPayoutHistories.RemoveRange(oldHistories);
|
||
}
|
||
|
||
// بعد پرداختها
|
||
_context.UserCommissionPayouts.RemoveRange(oldPayouts);
|
||
await _context.SaveChangesAsync();
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
#### 2.5. ایجاد پرداختها
|
||
|
||
```csharp
|
||
var payouts = new List<UserCommissionPayout>();
|
||
|
||
foreach (var balance in weeklyBalances)
|
||
{
|
||
// فقط تعادل شخصی (نه زیرمجموعه)
|
||
var userBalance = balance.TotalBalances;
|
||
|
||
// اگر تعادل صفر است، رد شود
|
||
if (userBalance <= 0)
|
||
continue;
|
||
|
||
// محاسبه مبلغ کمیسیون
|
||
var totalAmount = (long)(userBalance * valuePerBalance);
|
||
|
||
var payout = new UserCommissionPayout
|
||
{
|
||
UserId = balance.UserId,
|
||
WeekNumber = request.WeekNumber,
|
||
WeeklyPoolId = existingPool.Id,
|
||
BalancesEarned = userBalance,
|
||
ValuePerBalance = valuePerBalance,
|
||
TotalAmount = totalAmount,
|
||
Status = CommissionPayoutStatus.Pending,
|
||
PaidAt = null,
|
||
WithdrawalMethod = null,
|
||
IbanNumber = null,
|
||
WithdrawnAt = null
|
||
};
|
||
|
||
payouts.Add(payout);
|
||
}
|
||
|
||
await _context.UserCommissionPayouts.AddRangeAsync(payouts);
|
||
await _context.SaveChangesAsync();
|
||
```
|
||
|
||
---
|
||
|
||
#### 2.6. ثبت تاریخچه
|
||
|
||
```csharp
|
||
var historyList = new List<CommissionPayoutHistory>();
|
||
|
||
foreach (var payout in payouts)
|
||
{
|
||
var history = new CommissionPayoutHistory
|
||
{
|
||
UserCommissionPayoutId = payout.Id,
|
||
UserId = payout.UserId,
|
||
WeekNumber = request.WeekNumber,
|
||
AmountBefore = 0,
|
||
AmountAfter = payout.TotalAmount,
|
||
OldStatus = default(CommissionPayoutStatus),
|
||
NewStatus = CommissionPayoutStatus.Pending,
|
||
Action = CommissionPayoutAction.Created,
|
||
PerformedBy = "System",
|
||
Reason = "پردازش خودکار کمیسیون هفتگی"
|
||
};
|
||
|
||
historyList.Add(history);
|
||
}
|
||
|
||
await _context.CommissionPayoutHistories.AddRangeAsync(historyList);
|
||
await _context.SaveChangesAsync();
|
||
```
|
||
|
||
---
|
||
|
||
### فرآیند کلی (TriggerWeeklyCalculation)
|
||
|
||
**Command:**
|
||
```csharp
|
||
{
|
||
"weekNumber": "2025-W48",
|
||
"forceRecalculate": false,
|
||
"skipBalances": false,
|
||
"skipPayouts": false
|
||
}
|
||
```
|
||
|
||
**Handler:**
|
||
```csharp
|
||
// Step 1: محاسبه تعادلهای هفتگی
|
||
if (!request.SkipBalances)
|
||
{
|
||
await _mediator.Send(new CalculateWeeklyBalancesCommand
|
||
{
|
||
WeekNumber = request.WeekNumber,
|
||
ForceRecalculate = request.ForceRecalculate
|
||
});
|
||
steps.Add("محاسبه امتیازات هفتگی");
|
||
}
|
||
|
||
// Step 2: محاسبه Pool و پردازش پرداختها
|
||
if (!request.SkipPayouts)
|
||
{
|
||
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand
|
||
{
|
||
WeekNumber = request.WeekNumber,
|
||
ForceRecalculate = request.ForceRecalculate
|
||
});
|
||
steps.Add("محاسبه استخر و پرداخت کاربران");
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## موجودیتهای دامین
|
||
|
||
### 1. `ClubMembership`
|
||
|
||
```csharp
|
||
public class ClubMembership : BaseAuditableEntity
|
||
{
|
||
public long UserId { get; set; }
|
||
public virtual User User { get; set; }
|
||
|
||
/// <summary>
|
||
/// آیا عضویت فعال است؟
|
||
/// </summary>
|
||
public bool IsActive { get; set; }
|
||
|
||
/// <summary>
|
||
/// تاریخ فعالسازی عضویت
|
||
/// </summary>
|
||
public DateTime? ActivatedAt { get; set; }
|
||
|
||
/// <summary>
|
||
/// مبلغ اولیه پرداختی برای فعالسازی (25,000,000 ریال)
|
||
/// </summary>
|
||
public long InitialContribution { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع درآمد کارمزد شبکه تاکنون
|
||
/// </summary>
|
||
public long TotalEarned { get; set; }
|
||
|
||
public virtual ICollection<UserClubFeature> UserClubFeatures { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. `NetworkWeeklyBalance`
|
||
|
||
```csharp
|
||
public class NetworkWeeklyBalance : BaseAuditableEntity
|
||
{
|
||
public long UserId { get; set; }
|
||
public virtual User User { get; set; }
|
||
|
||
/// <summary>
|
||
/// شماره هفته (فرمت: "2025-W48")
|
||
/// </summary>
|
||
public string WeekNumber { get; set; }
|
||
|
||
// === اطلاعات پای چپ ===
|
||
public int LeftLegNewMembers { get; set; } // اعضای جدید این هفته
|
||
public int LeftLegCarryover { get; set; } // باقیمانده هفته قبل
|
||
public int LeftLegTotal { get; set; } // جمع (جدید + باقیمانده)
|
||
public int LeftLegRemainder { get; set; } // باقیمانده برای هفته بعد
|
||
|
||
// === اطلاعات پای راست ===
|
||
public int RightLegNewMembers { get; set; }
|
||
public int RightLegCarryover { get; set; }
|
||
public int RightLegTotal { get; set; }
|
||
public int RightLegRemainder { get; set; }
|
||
|
||
// === تعادل نهایی ===
|
||
/// <summary>
|
||
/// امتیاز نهایی بعد از اعمال سقف 300 (CappedBalances)
|
||
/// </summary>
|
||
public int TotalBalances { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع تعادلهای زیرمجموعه (تا 15 لول)
|
||
/// فقط برای گزارشگیری - در Pool استفاده نمیشود
|
||
/// </summary>
|
||
public int SubordinateBalances { get; set; }
|
||
|
||
// === فلش (از دست رفته) ===
|
||
/// <summary>
|
||
/// مقدار فلش هر طرف (TotalBalances - CappedBalances)
|
||
/// </summary>
|
||
public int FlushedPerSide { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع فلش از دو طرف (FlushedPerSide × 2)
|
||
/// </summary>
|
||
public int TotalFlushed { get; set; }
|
||
|
||
// === متا دیتا ===
|
||
public long WeeklyPoolContribution { get; set; } // همیشه 0 در v2.0
|
||
public DateTime CalculatedAt { get; set; }
|
||
public bool IsExpired { get; set; }
|
||
|
||
// === Deprecated ===
|
||
[Obsolete("از LeftLegTotal استفاده کنید")]
|
||
public int LeftLegBalances { get; set; }
|
||
|
||
[Obsolete("از RightLegTotal استفاده کنید")]
|
||
public int RightLegBalances { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. `WeeklyCommissionPool`
|
||
|
||
```csharp
|
||
public class WeeklyCommissionPool : BaseAuditableEntity
|
||
{
|
||
/// <summary>
|
||
/// شماره هفته (فرمت: "2025-W48")
|
||
/// </summary>
|
||
public string WeekNumber { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع مبلغ Pool (از فعالسازیهای باشگاه)
|
||
/// </summary>
|
||
public long TotalPoolAmount { get; set; }
|
||
|
||
/// <summary>
|
||
/// مجموع تعادلهای کل شبکه
|
||
/// </summary>
|
||
public int TotalBalances { get; set; }
|
||
|
||
/// <summary>
|
||
/// ارزش ریالی هر امتیاز (TotalPoolAmount ÷ TotalBalances)
|
||
/// </summary>
|
||
public long ValuePerBalance { get; set; }
|
||
|
||
/// <summary>
|
||
/// آیا Pool محاسبه و توزیع شده است؟
|
||
/// </summary>
|
||
public bool IsCalculated { get; set; }
|
||
|
||
public DateTime? CalculatedAt { get; set; }
|
||
|
||
public virtual ICollection<UserCommissionPayout> Payouts { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. `UserCommissionPayout`
|
||
|
||
```csharp
|
||
public class UserCommissionPayout : BaseAuditableEntity
|
||
{
|
||
public long UserId { get; set; }
|
||
public virtual User User { get; set; }
|
||
|
||
public string WeekNumber { get; set; }
|
||
|
||
public long WeeklyPoolId { get; set; }
|
||
public virtual WeeklyCommissionPool WeeklyPool { get; set; }
|
||
|
||
/// <summary>
|
||
/// تعداد امتیازهای کسب شده (تعادل شخصی)
|
||
/// </summary>
|
||
public int BalancesEarned { get; set; }
|
||
|
||
/// <summary>
|
||
/// ارزش ریالی هر امتیاز
|
||
/// </summary>
|
||
public long ValuePerBalance { get; set; }
|
||
|
||
/// <summary>
|
||
/// مبلغ کل کمیسیون (BalancesEarned × ValuePerBalance)
|
||
/// </summary>
|
||
public long TotalAmount { get; set; }
|
||
|
||
/// <summary>
|
||
/// وضعیت: Pending, Approved, Paid, Rejected
|
||
/// </summary>
|
||
public CommissionPayoutStatus Status { get; set; }
|
||
|
||
public DateTime? PaidAt { get; set; }
|
||
public string? WithdrawalMethod { get; set; }
|
||
public string? IbanNumber { get; set; }
|
||
public DateTime? WithdrawnAt { get; set; }
|
||
|
||
public virtual ICollection<CommissionPayoutHistory> Histories { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. `CommissionPayoutHistory`
|
||
|
||
```csharp
|
||
public class CommissionPayoutHistory : BaseAuditableEntity
|
||
{
|
||
public long UserCommissionPayoutId { get; set; }
|
||
public virtual UserCommissionPayout UserCommissionPayout { get; set; }
|
||
|
||
public long UserId { get; set; }
|
||
public virtual User User { get; set; }
|
||
|
||
public string WeekNumber { get; set; }
|
||
|
||
public long AmountBefore { get; set; }
|
||
public long AmountAfter { get; set; }
|
||
|
||
public CommissionPayoutStatus OldStatus { get; set; }
|
||
public CommissionPayoutStatus NewStatus { get; set; }
|
||
|
||
public CommissionPayoutAction Action { get; set; }
|
||
|
||
public string PerformedBy { get; set; } // UserId یا "System"
|
||
public string? Reason { get; set; }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## جزئیات پیادهسازی
|
||
|
||
### محاسبه شماره هفته (Week Number)
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// محاسبه شماره هفته جاری (شنبه محور)
|
||
/// فرمت: "YYYY-Www" (مثال: "2025-W48")
|
||
/// </summary>
|
||
private string GetCurrentWeekNumber()
|
||
{
|
||
var now = DateTime.Now;
|
||
var culture = new CultureInfo("fa-IR");
|
||
var calendar = culture.Calendar;
|
||
|
||
var year = calendar.GetYear(now);
|
||
var jan1 = new DateTime(year, 1, 1);
|
||
|
||
// محاسبه اولین شنبه سال
|
||
var daysOffset = DayOfWeek.Saturday - jan1.DayOfWeek;
|
||
if (daysOffset < 0) daysOffset += 7;
|
||
var firstSaturday = jan1.AddDays(daysOffset);
|
||
|
||
// محاسبه تعداد روزهای گذشته از اولین شنبه
|
||
var daysSinceFirstSaturday = (now - firstSaturday).Days;
|
||
|
||
// محاسبه شماره هفته
|
||
var weekNumber = (daysSinceFirstSaturday / 7) + 1;
|
||
|
||
return $"{year}-W{weekNumber:D2}";
|
||
}
|
||
```
|
||
|
||
**نکته**: هفته از **شنبه** شروع میشود (تقویم ایرانی).
|
||
|
||
---
|
||
|
||
### شمارش بازگشتی اعضای جدید
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// شمارش اعضای جدیدی که در یک هفته مشخص به یک پا اضافه شدند
|
||
/// </summary>
|
||
private async Task<int> CountNewMembersInLeg(
|
||
long userId,
|
||
NetworkLeg leg,
|
||
string weekNumber,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
var (startDate, endDate) = GetWeekDateRange(weekNumber);
|
||
|
||
return await CountNewMembersRecursive(
|
||
userId, leg, startDate, endDate,
|
||
currentLevel: 0,
|
||
maxLevel: maxLevel,
|
||
cancellationToken
|
||
);
|
||
}
|
||
|
||
private async Task<int> CountNewMembersRecursive(
|
||
long userId,
|
||
NetworkLeg leg,
|
||
DateTime startDate,
|
||
DateTime endDate,
|
||
int currentLevel,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// محدودیت عمق: تا 15 لول
|
||
if (currentLevel >= maxLevel)
|
||
return 0;
|
||
|
||
// پیدا کردن فرزند مستقیم
|
||
var child = await _context.Users
|
||
.FirstOrDefaultAsync(
|
||
x => x.NetworkParentId == userId && x.LegPosition == leg,
|
||
cancellationToken
|
||
);
|
||
|
||
if (child == null)
|
||
return 0;
|
||
|
||
var count = 0;
|
||
|
||
// بررسی فعالسازی باشگاه در این هفته
|
||
var membership = await _context.ClubMemberships
|
||
.FirstOrDefaultAsync(
|
||
x => x.UserId == child.Id && x.IsActive,
|
||
cancellationToken
|
||
);
|
||
|
||
if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate)
|
||
{
|
||
count = 1;
|
||
}
|
||
|
||
// جمع کردن از زیرشاخههای چپ و راست
|
||
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;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### تبدیل WeekNumber به تاریخ
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// تبدیل شماره هفته به بازه تاریخی (شنبه تا جمعه)
|
||
/// </summary>
|
||
private (DateTime startDate, DateTime endDate) GetWeekDateRange(string weekNumber)
|
||
{
|
||
// Parse: "2025-W48"
|
||
var parts = weekNumber.Split('-');
|
||
var year = int.Parse(parts[0]);
|
||
var week = int.Parse(parts[1].Replace("W", ""));
|
||
|
||
// محاسبه اولین شنبه سال
|
||
var jan1 = new DateTime(year, 1, 1);
|
||
var daysOffset = DayOfWeek.Saturday - jan1.DayOfWeek;
|
||
if (daysOffset < 0) daysOffset += 7;
|
||
var firstSaturday = jan1.AddDays(daysOffset);
|
||
|
||
// محاسبه شنبه این هفته
|
||
var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||
|
||
// جمعه همان هفته (23:59:59)
|
||
var weekEnd = weekStart.AddDays(6)
|
||
.AddHours(23)
|
||
.AddMinutes(59)
|
||
.AddSeconds(59);
|
||
|
||
return (weekStart, weekEnd);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## مثالهای عملی
|
||
|
||
### مثال 1: فعالسازی ساده
|
||
|
||
**وضعیت اولیه:**
|
||
```
|
||
User A (Root)
|
||
└─ خالی
|
||
```
|
||
|
||
**فعالسازی User B:**
|
||
```csharp
|
||
Request:
|
||
{
|
||
"userId": 2, // User B
|
||
"networkParentId": 1, // User A
|
||
"legPosition": "Left"
|
||
}
|
||
|
||
Result:
|
||
✅ User B عضو باشگاه شد
|
||
✅ 25M از Balance کسر شد
|
||
✅ 25.2M به Pool هفته جاری اضافه شد
|
||
✅ User B زیر User A قرار گرفت (چپ)
|
||
```
|
||
|
||
**ساختار شبکه بعد:**
|
||
```
|
||
User A (Root)
|
||
├─ User B (Left) ✅
|
||
└─ خالی (Right)
|
||
```
|
||
|
||
---
|
||
|
||
### مثال 2: محاسبه تعادل
|
||
|
||
**ساختار شبکه:**
|
||
```
|
||
User A
|
||
├─ Left: User B, User C (2 نفر)
|
||
└─ Right: User D (1 نفر)
|
||
```
|
||
|
||
**محاسبات User A:**
|
||
```
|
||
LeftTotal = 2 (عضو جدید این هفته)
|
||
RightTotal = 1
|
||
|
||
TotalBalances (initial) = MIN(2, 1) = 1
|
||
CappedBalances = MIN(1, 300) = 1
|
||
FlushedPerSide = 1 - 1 = 0
|
||
TotalFlushed = 0 × 2 = 0
|
||
|
||
LeftRemainder = 2 - 1 = 1 ✅ به هفته بعد
|
||
RightRemainder = 1 - 1 = 0
|
||
|
||
Result:
|
||
امتیاز این هفته: 1
|
||
باقیمانده چپ: 1
|
||
```
|
||
|
||
---
|
||
|
||
### مثال 3: توزیع Pool
|
||
|
||
**فرض:**
|
||
- Pool هفته: `252,000,000` ریال (10 فعالسازی × 25.2M)
|
||
- مجموع تعادلهای شبکه: `1,500` امتیاز
|
||
|
||
**محاسبات:**
|
||
```
|
||
ValuePerBalance = 252,000,000 ÷ 1,500 = 168,000 ریال/امتیاز
|
||
```
|
||
|
||
**کاربران:**
|
||
|
||
| کاربر | امتیاز شخصی | کمیسیون |
|
||
|-------|-------------|---------|
|
||
| User A | 300 | 300 × 168,000 = **50,400,000** ریال |
|
||
| User B | 150 | 150 × 168,000 = **25,200,000** ریال |
|
||
| User C | 50 | 50 × 168,000 = **8,400,000** ریال |
|
||
| **جمع** | **1,500** | **252,000,000** ریال ✅ |
|
||
|
||
---
|
||
|
||
### مثال 4: فلش (Overflow)
|
||
|
||
**User X:**
|
||
```
|
||
هفته قبل:
|
||
چپ = 250 باقیمانده
|
||
راست = 0
|
||
|
||
این هفته:
|
||
چپ = 400 عضو جدید
|
||
راست = 500 عضو جدید
|
||
|
||
محاسبات:
|
||
LeftTotal = 250 + 400 = 650
|
||
RightTotal = 0 + 500 = 500
|
||
|
||
TotalBalances (initial) = MIN(650, 500) = 500
|
||
CappedBalances = MIN(500, 300) = 300 ⭐
|
||
|
||
FlushedPerSide = 500 - 300 = 200
|
||
TotalFlushed = 200 × 2 = 400 ❌ (از دست رفت)
|
||
|
||
LeftRemainder = 650 - 500 = 150 ✅
|
||
RightRemainder = 500 - 500 = 0
|
||
|
||
Result:
|
||
امتیاز این هفته: 300
|
||
فلش: 400 (از بین رفت)
|
||
باقیمانده چپ: 150 (به هفته بعد)
|
||
```
|
||
|
||
**نکته**: حتی با 650 چپ و 500 راست، فقط **300 امتیاز** میگیرد (سقف).
|
||
|
||
---
|
||
|
||
## Configuration های سیستم
|
||
|
||
### جدول تنظیمات
|
||
|
||
| Key | Value | توضیحات |
|
||
|-----|-------|---------|
|
||
| `Club.ActivationFee` | `25000000` | هزینه فعالسازی باشگاه (25M ریال) |
|
||
| `Club.GiftValue` | `25200000` | مبلغ اضافه به Pool (25.2M ریال) |
|
||
| `Club.InitialBalance` | `56000000` | شارژ اولیه کیف پول (56M ریال) |
|
||
| `Commission.MaxWeeklyBalancesPerLeg` | `300` | سقف امتیاز هر پا |
|
||
| `Commission.MaxNetworkLevel` | `15` | حداکثر عمق شبکه برای محاسبات |
|
||
|
||
---
|
||
|
||
## Migration های ایجاد شده
|
||
|
||
### 1. `AddFlushedFieldsToNetworkWeeklyBalance`
|
||
|
||
```csharp
|
||
migrationBuilder.AddColumn<int>(
|
||
name: "FlushedPerSide",
|
||
schema: "Commission",
|
||
table: "NetworkWeeklyBalances",
|
||
type: "int",
|
||
nullable: false,
|
||
defaultValue: 0);
|
||
|
||
migrationBuilder.AddColumn<int>(
|
||
name: "TotalFlushed",
|
||
schema: "Commission",
|
||
table: "NetworkWeeklyBalances",
|
||
type: "int",
|
||
nullable: false,
|
||
defaultValue: 0);
|
||
```
|
||
|
||
### 2. `AddSubordinateBalancesToNetworkWeeklyBalance`
|
||
|
||
```csharp
|
||
migrationBuilder.AddColumn<int>(
|
||
name: "SubordinateBalances",
|
||
schema: "Commission",
|
||
table: "NetworkWeeklyBalances",
|
||
type: "int",
|
||
nullable: false,
|
||
defaultValue: 0);
|
||
```
|
||
|
||
---
|
||
|
||
## نکات مهم و Best Practices
|
||
|
||
### ✅ Do's
|
||
|
||
1. **همیشه Transaction استفاده کنید** برای عملیات چند مرحلهای
|
||
2. **ForceRecalculate با احتیاط** استفاده شود (حذف داده)
|
||
3. **Week Number** را از سیستم محاسبه کنید (نه دستی)
|
||
4. **فیلتر اعضای باشگاه** را فراموش نکنید
|
||
5. **FK Constraint** را رعایت کنید (History قبل از Payout حذف شود)
|
||
|
||
### ❌ Don'ts
|
||
|
||
1. **Pool را دستی پُر نکنید** (باید از ActivateClubMembership بیاید)
|
||
2. **SubordinateBalances را در Pool استفاده نکنید** (تکراری است)
|
||
3. **شرط `NetworkParentId.HasValue` نگذارید** (ریشه شبکه حذف میشود)
|
||
4. **محاسبات را بدون Lock اجرا نکنید** (امکان Race Condition)
|
||
|
||
---
|
||
|
||
## خلاصه فرآیند نهایی
|
||
|
||
```
|
||
1. کاربر شارژ میکند (56M)
|
||
├─ Balance += 56M
|
||
└─ DiscountBalance += 56M
|
||
|
||
2. کاربر «عضو باشگاه» میشود
|
||
├─ Balance -= 25M
|
||
├─ Pool += 25.2M ⭐
|
||
├─ قرار گرفتن در شبکه
|
||
└─ دریافت 4 امکان پایه
|
||
|
||
3. هر هفته: CalculateWeeklyBalances
|
||
├─ فقط اعضای فعال باشگاه
|
||
├─ محاسبه تعادل (تا 15 لول)
|
||
├─ محاسبه زیرمجموعه (تا 15 لول)
|
||
└─ ذخیره فلش
|
||
|
||
4. هر هفته: CalculateWeeklyCommissionPool
|
||
├─ Pool از قبل پُر شده
|
||
├─ ارزش هر امتیاز = Pool ÷ مجموع تعادلها
|
||
├─ ایجاد UserCommissionPayout
|
||
└─ ثبت تاریخچه
|
||
|
||
5. کاربر درخواست برداشت
|
||
└─ NetworkBalance → حساب بانکی
|
||
```
|
||
|
||
---
|
||
|
||
## تاریخچه تغییرات
|
||
|
||
| تاریخ | نسخه | تغییرات |
|
||
|-------|------|---------|
|
||
| 2025-12-04 | 1.0 | نسخه اولیه سیستم |
|
||
| 2025-12-10 | 1.5 | اصلاح Pool (از فعالسازی) |
|
||
| 2025-12-12 | 2.0 | سادهسازی به 2 مرحله + فیلتر باشگاه |
|
||
|
||
---
|
||
|
||
**پایان مستندات** 🎯
|