# سیستم باشگاه مشتریان و کمیسیون شبکه - مستندات کامل و نهایی

**تاریخ آخرین بروزرسانی**: ۲۱ آذر ۱۴۰۴ (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 مرحله + فیلتر باشگاه |

---

**پایان مستندات** 🎯
