# سیستم باشگاه مشتریان و کمیسیون شبکه - مستندات کامل و نهایی **تاریخ آخرین بروزرسانی**: ۲۱ آذر ۱۴۰۴ (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 CalculateSubordinateBalancesAsync( long userId, Dictionary 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(); 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(); 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; } /// /// آیا عضویت فعال است؟ /// public bool IsActive { get; set; } /// /// تاریخ فعال‌سازی عضویت /// public DateTime? ActivatedAt { get; set; } /// /// مبلغ اولیه پرداختی برای فعال‌سازی (25,000,000 ریال) /// public long InitialContribution { get; set; } /// /// مجموع درآمد کارمزد شبکه تاکنون /// public long TotalEarned { get; set; } public virtual ICollection UserClubFeatures { get; set; } } ``` --- ### 2. `NetworkWeeklyBalance` ```csharp public class NetworkWeeklyBalance : BaseAuditableEntity { public long UserId { get; set; } public virtual User User { get; set; } /// /// شماره هفته (فرمت: "2025-W48") /// 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; } // === تعادل نهایی === /// /// امتیاز نهایی بعد از اعمال سقف 300 (CappedBalances) /// public int TotalBalances { get; set; } /// /// مجموع تعادل‌های زیرمجموعه (تا 15 لول) /// فقط برای گزارش‌گیری - در Pool استفاده نمی‌شود /// public int SubordinateBalances { get; set; } // === فلش (از دست رفته) === /// /// مقدار فلش هر طرف (TotalBalances - CappedBalances) /// public int FlushedPerSide { get; set; } /// /// مجموع فلش از دو طرف (FlushedPerSide × 2) /// 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 { /// /// شماره هفته (فرمت: "2025-W48") /// public string WeekNumber { get; set; } /// /// مجموع مبلغ Pool (از فعالسازی‌های باشگاه) /// public long TotalPoolAmount { get; set; } /// /// مجموع تعادل‌های کل شبکه /// public int TotalBalances { get; set; } /// /// ارزش ریالی هر امتیاز (TotalPoolAmount ÷ TotalBalances) /// public long ValuePerBalance { get; set; } /// /// آیا Pool محاسبه و توزیع شده است؟ /// public bool IsCalculated { get; set; } public DateTime? CalculatedAt { get; set; } public virtual ICollection 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; } /// /// تعداد امتیازهای کسب شده (تعادل شخصی) /// public int BalancesEarned { get; set; } /// /// ارزش ریالی هر امتیاز /// public long ValuePerBalance { get; set; } /// /// مبلغ کل کمیسیون (BalancesEarned × ValuePerBalance) /// public long TotalAmount { get; set; } /// /// وضعیت: Pending, Approved, Paid, Rejected /// 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 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 /// /// محاسبه شماره هفته جاری (شنبه محور) /// فرمت: "YYYY-Www" (مثال: "2025-W48") /// 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 /// /// شمارش اعضای جدیدی که در یک هفته مشخص به یک پا اضافه شدند /// private async Task 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 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 /// /// تبدیل شماره هفته به بازه تاریخی (شنبه تا جمعه) /// 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( name: "FlushedPerSide", schema: "Commission", table: "NetworkWeeklyBalances", type: "int", nullable: false, defaultValue: 0); migrationBuilder.AddColumn( name: "TotalFlushed", schema: "Commission", table: "NetworkWeeklyBalances", type: "int", nullable: false, defaultValue: 0); ``` ### 2. `AddSubordinateBalancesToNetworkWeeklyBalance` ```csharp migrationBuilder.AddColumn( 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 مرحله + فیلتر باشگاه | --- **پایان مستندات** 🎯