Implement Persian Date Conversion and Enhance User Network Information Service

- 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.
This commit is contained in:
masoodafar-web
2025-12-20 06:15:59 +03:30
parent 13a3489765
commit 002e99f6bf
32 changed files with 10263 additions and 106 deletions
@@ -0,0 +1,380 @@
# 📊 مثال‌های عملی محاسبه تعادل - 5 لول عمقی
**تاریخ**: 2025-12-09
**وضعیت**: مثال‌های کامل و تایید شده
**هدف**: نمایش محاسبات واقعی برای درخت باینری تا 5 لول
---
## 🌳 ساختار درخت نمونه
```
User1 (Level 0)
/ \
User2 (L1-L) User3 (L1-R)
/ \ / \
User4(L2-LL) User5(L2-LR) User6(L2-RL) User7(L2-RR)
/ \ / \ / \ / \
U8(L3) U9(L3) U10(L3) U11(L3) U12(L3) U13(L3) U14(L3) U15(L3)
/ \ / \ / \ / \ / \ / \ / \ / \
U16-U31 (Level 4 - 16 users)
/\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\ /\
U32-U63 (Level 5 - 32 users)
```
---
## 📋 داده‌های ورودی
### فرضیات:
- **هفته فعلی**: 2025-W50
- **سقف امتیاز**: 300
- **تعداد کل کاربران**: 63 نفر (6 لول: 1+2+4+8+16+32)
- **وضعیت**: همه کاربران فعال هستند (عضو باشگاه)
---
## 🎯 محاسبات Level 5 (پایین‌ترین سطح)
### User 32-63 (32 کاربر Leaf):
```
هیچ زیرمجموعه‌ای ندارند
چپ = 0، راست = 0
تعادل = MIN(0, 0) = 0
امتیاز = 0
باقیمانده چپ = 0
باقیمانده راست = 0
فلش = 0
```
**خلاصه Level 5**: تمام 32 کاربر → 0 امتیاز
---
## 🎯 محاسبات Level 4 (User 16-31)
### User 16:
**زیرمجموعه**:
- چپ: User 32 (1 نفر)
- راست: User 33 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل اولیه = MIN(1, 1) = 1
باقیمانده چپ = 1 - 1 = 0
باقیمانده راست = 1 - 1 = 0
امتیاز نهایی = MIN(1, 300) = 1 ✅
فلش = 0
```
### User 17:
**زیرمجموعه**:
- چپ: User 34 (1 نفر)
- راست: User 35 (1 نفر)
**محاسبات**: مشابه User 16
```
امتیاز = 1 ✅
```
### User 18-31 (14 کاربر دیگه):
همه مشابه User 16 → هر کدام 1 امتیاز
**خلاصه Level 4**: تمام 16 کاربر → هر کدام 1 امتیاز = **16 امتیاز**
---
## 🎯 محاسبات Level 3 (User 8-15)
### User 8:
**زیرمجموعه**:
- چپ: User 16 (1 نفر)
- راست: User 17 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 9:
**زیرمجموعه**:
- چپ: User 18 (1 نفر)
- راست: User 19 (1 نفر)
**محاسبات**: مشابه User 8
```
امتیاز = 1 ✅
```
### User 10-15 (6 کاربر دیگه):
همه مشابه → هر کدام 1 امتیاز
**خلاصه Level 3**: تمام 8 کاربر → هر کدام 1 امتیاز = **8 امتیاز**
---
## 🎯 محاسبات Level 2 (User 4-7)
### User 4:
**زیرمجموعه**:
- چپ: User 8 (1 نفر)
- راست: User 9 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 5:
**زیرمجموعه**:
- چپ: User 10 (1 نفر)
- راست: User 11 (1 نفر)
**محاسبات**: مشابه User 4
```
امتیاز = 1 ✅
```
### User 6, 7:
همه مشابه → هر کدام 1 امتیاز
**خلاصه Level 2**: تمام 4 کاربر → هر کدام 1 امتیاز = **4 امتیاز**
---
## 🎯 محاسبات Level 1 (User 2-3)
### User 2:
**زیرمجموعه**:
- چپ: User 4 (1 نفر)
- راست: User 5 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
### User 3:
**زیرمجموعه**:
- چپ: User 6 (1 نفر)
- راست: User 7 (1 نفر)
**محاسبات**: مشابه User 2
```
امتیاز = 1 ✅
```
**خلاصه Level 1**: تمام 2 کاربر → هر کدام 1 امتیاز = **2 امتیاز**
---
## 🎯 محاسبات Level 0 (User 1 - Root)
### User 1:
**زیرمجموعه**:
- چپ: User 2 (1 نفر)
- راست: User 3 (1 نفر)
**محاسبات**:
```
چپ = 1، راست = 1
تعادل = MIN(1, 1) = 1
امتیاز = 1 ✅
```
**خلاصه Level 0**: User 1 → **1 امتیاز**
---
## 📊 جمع کل سیستم
| Level | تعداد کاربران | امتیاز هر کاربر | جمع امتیازهای Level |
|-------|---------------|-----------------|---------------------|
| 5 | 32 | 0 | 0 |
| 4 | 16 | 1 | 16 |
| 3 | 8 | 1 | 8 |
| 2 | 4 | 1 | 4 |
| 1 | 2 | 1 | 2 |
| 0 | 1 | 1 | 1 |
| **جمع** | **63** | - | **31 امتیاز** |
---
## 💰 محاسبه صندوق
### داده‌های ورودی:
```
تعداد کاربران فعال شده این هفته: 63 نفر
هزینه فعال‌سازی هر نفر: 25,000,000 ریال
درصد سهم استخر: 20%
جمع ورودی استخر = 63 × 25,000,000 × 20%
= 63 × 5,000,000
= 315,000,000 ریال
```
### محاسبه ارزش هر امتیاز:
```
مجموع امتیازهای سیستم = 31
جمع استخر = 315,000,000 ریال
ارزش هر امتیاز = 315,000,000 ÷ 31
= 10,161,290 ریال (تقریباً)
```
### توزیع کمیسیون:
```
User 1: 1 × 10,161,290 = 10,161,290 ریال
User 2: 1 × 10,161,290 = 10,161,290 ریال
User 3: 1 × 10,161,290 = 10,161,290 ریال
User 4-7: 4 × 10,161,290 = 40,645,160 ریال
User 8-15: 8 × 10,161,290 = 81,290,320 ریال
User 16-31: 16 × 10,161,290 = 162,580,640 ریال
User 32-63: 0 ریال (امتیازی ندارند)
جمع کل پرداختی = 315,000,000 ریال ✅
```
---
## 🔥 مثال پیچیده‌تر: سناریو نامتعادل
### تغییر ساختار:
```
User 1:
چپ: 500 نفر (عمق زیاد)
راست: 600 نفر (عمق بیشتر)
```
### محاسبات User 1:
```
مرحله 1️⃣: تعادل اولیه
چپ = 500، راست = 600
تعادل = MIN(500, 600) = 500
مرحله 2️⃣: باقیمانده
باقی چپ = 500 - 500 = 0
باقی راست = 600 - 500 = 100 → هفته بعد
مرحله 3️⃣: اعمال سقف
امتیاز = MIN(500, 300) = 300 ✅
مرحله 4️⃣: فلش
فلش از چپ = 500 - 300 = 200
فلش از راست = 500 - 300 = 200
جمع فلش = 400 (از بین می‌رود)
```
### نتیجه:
```
✅ امتیاز User 1: 300
✅ باقیمانده راست: 100 (می‌رود هفته بعد)
✅ باقیمانده چپ: 0
✅ فلش شده: 400 (از بین رفته)
```
---
## 🔄 مثال با Carryover (هفته بعد)
### فرض: User 1 در هفته 2025-W51:
```
باقیمانده هفته قبل:
چپ: 0
راست: 100
جدیدهای این هفته:
چپ: 250
راست: 150
```
### محاسبات:
```
مرحله 1️⃣: جمع با هفته قبل
چپ کل = 0 + 250 = 250
راست کل = 100 + 150 = 250
مرحله 2️⃣: تعادل
تعادل = MIN(250, 250) = 250
مرحله 3️⃣: باقیمانده
باقی چپ = 250 - 250 = 0
باقی راست = 250 - 250 = 0
مرحله 4️⃣: امتیاز
امتیاز = MIN(250, 300) = 250 ✅
مرحله 5️⃣: فلش
فلش = 0 (چون 250 < 300)
```
---
## 📈 مثال سقف: User با شبکه بزرگ
### User A:
```
چپ: 800 نفر
راست: 900 نفر
```
### محاسبات:
```
تعادل = MIN(800, 900) = 800
باقی چپ = 800 - 800 = 0
باقی راست = 900 - 800 = 100
امتیاز = MIN(800, 300) = 300 ✅
فلش:
از چپ: 800 - 300 = 500
از راست: 800 - 300 = 500
جمع: 1000 (از بین می‌رود)
```
**نتیجه**: حتی با 800 تعادل، فقط **300 امتیاز** می‌گیرد!
---
## 🎯 جمع‌بندی قوانین
### ✅ قوانین کلیدی:
1. **تعادل** = MIN(چپ، راست)
2. **باقیمانده** = طرفی که بیشتر است (قبل از سقف)
3. **امتیاز** = MIN(تعادل، 300)
4. **فلش** = (تعادل - 300) از هر دو طرف (اگر > 300)
5. **محاسبه مستقل** = هر کاربر جداگانه
6. **جمع صندوق** = مجموع امتیازهای همه
### ✅ نکات مهم:
- باقیمانده **جداگانه** ذخیره می‌شود (چپ و راست)
- فلش از **هر دو طرف** اتفاق می‌افتد
- سقف 300 روی **امتیاز نهایی** اعمال می‌شود
- هر کاربر مستقل از دیگران محاسبه می‌شود
---
## 📊 جدول مقایسه سناریوها
| سناریو | چپ | راست | تعادل | امتیاز | باقی چپ | باقی راست | فلش کل |
|--------|-----|-------|--------|--------|---------|-----------|---------|
| متعادل کوچک | 50 | 50 | 50 | 50 | 0 | 0 | 0 |
| متعادل متوسط | 200 | 200 | 200 | 200 | 0 | 0 | 0 |
| نامتعادل کوچک | 100 | 150 | 100 | 100 | 0 | 50 | 0 |
| نامتعادل متوسط | 250 | 350 | 250 | 250 | 0 | 100 | 0 |
| **سقف ساده** | **350** | **350** | **350** | **300** | **0** | **0** | **100** |
| **سقف نامتعادل** | **500** | **600** | **500** | **300** | **0** | **100** | **400** |
| سقف بزرگ | 800 | 900 | 800 | 300 | 0 | 100 | 1000 |
---
**پایان مثال‌های عملی**
این مستند تمام حالات ممکن محاسبه تعادل را با مثال‌های عددی واقعی نشان می‌دهد.
+86 -42
View File
@@ -1,16 +1,36 @@
# Balance Calculation with Carryover Logic - Complete Guide
**Date**: 2025-12-01
**Last Updated**: 2025-12-04 (⚠️ تغییر مهم: سقف 300 برای هر دست، نه کل)
**Status**: ✅ Implemented (نیاز به اصلاح سقف دارد)
**Last Updated**: 2025-12-09 (✅ اصلاح نهایی: محاسبات تعادل و فلش)
**Status**: ✅ Fully Implemented & Verified
**Migration**: `UpdateNetworkWeeklyBalanceWithCarryover`
---
## ⚠️ اصلاحیه مهم بیزینس (2025-12-04)
## ✅ آخرین به‌روزرسانی (2025-12-09)
### مشکل شناسایی شده:
در پیاده‌سازی فعلی، سقف تعادل هفتگی **300 کل** در نظر گرفته شده بود. اما طبق قانون صحیح بیزینس:
### تغییرات اعمال شده:
کد محاسبه تعادل با توضیحات دقیق بیزینس تطبیق داده شد:
1.**ترتیب محاسبات اصلاح شد**:
- اول تعادل اولیه محاسبه می‌شود
- بعد باقیمانده (برای هفته بعد)
- سپس سقف 300 اعمال می‌شود
- در نهایت فلش محاسبه می‌شود
2.**فلش از هر دو طرف**:
- اگر تعادل > 300 باشد
- از چپ: (تعادل - 300) فلش می‌شود
- از راست: (تعادل - 300) فلش می‌شود
- جمع فلش = (تعادل - 300) × 2
3.**باقیمانده جداگانه ذخیره می‌شود**:
- `LeftLegRemainder`: باقیمانده دست چپ
- `RightLegRemainder`: باقیمانده دست راست
---
## 📋 قوانین اصلی بیزینس
| توضیح | منطق فعلی (اشتباه) | منطق صحیح |
|-------|---------------------|-----------|
@@ -71,10 +91,10 @@ Key = "Commission.MaxWeeklyBalancesPerLeg", Value = "300"
// تمام مقادیر از جدول SystemConfigurations خوانده می‌شوند
Club.ActivationFee = 25,000,000 ریال (هزینه فعالسازی)
Commission.WeeklyPoolContributionPercent = 20% (سهم استخر)
Commission.MaxWeeklyBalancesPerLeg = 300 ( سقف تعادل هفتگی - هر دست)
Commission.MaxWeeklyBalancesPerLeg = 300 ( سقف امتیاز نهایی)
```
**توجه:** کلید قدیمی `MaxWeeklyBalancesPerUser` باید به `MaxWeeklyBalancesPerLeg` تغییر کند.
**نکته مهم**: سقف 300 روی **امتیاز نهایی** اعمال می‌شود، نه روی تعادل اولیه!
### **Pool Contribution Calculation:**
@@ -90,38 +110,52 @@ weeklyPoolContribution = totalNewMembers × activationFee × poolPercent
---
## 🚫 MaxWeeklyBalances Cap (محدودیت سقف - هر دست)
## 🚫 MaxWeeklyBalances Cap (محدودیت سقف 300)
### **Logic (صحیح):**
### **Logic صحیح (به‌روز شده 2025-12-09):**
```csharp
// ⚠️ سقف روی هر دست جداگانه اعمال می‌شود
cappedLeftTotal = MIN(leftTotal, maxBalancesPerLeg) // 300
cappedRightTotal = MIN(rightTotal, maxBalancesPerLeg) // 300
// ✅ مرحله 1: محاسبه تعادل اولیه (بدون سقف)
totalBalances = MIN(leftTotal, rightTotal)
// تعادل = کمترین مقدار بعد از اعمال سقف
totalBalances = MIN(cappedLeftTotal, cappedRightTotal)
// ✅ مرحله 2: محاسبه باقیمانده برای هفته بعد
leftRemainder = leftTotal - totalBalances
rightRemainder = rightTotal - totalBalances
// باقیمانده = مقدار قبل از سقف - سقف (نه از totalBalances)
leftRemainder = leftTotal - cappedLeftTotal
rightRemainder = rightTotal - cappedRightTotal
// ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
cappedBalances = MIN(totalBalances, 300)
// ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
flushedPerSide = totalBalances - cappedBalances
totalFlushed = flushedPerSide × 2
```
### **Example (جدید):**
### **Example (مثال کامل):**
```
Week 5:
leftTotal = 350, rightTotal = 400
maxBalancesPerLeg = 300
leftTotal = 500, rightTotal = 600
cappedLeftTotal = MIN(350, 300) = 300
cappedRightTotal = MIN(400, 300) = 300
مرحله 1️⃣: تعادل اولیه
totalBalances = MIN(500, 600) = 500
totalBalances = MIN(300, 300) = 300 ✅
مرحله 2️⃣: باقیمانده برای هفته بعد
leftRemainder = 500 - 500 = 0 ✅
rightRemainder = 600 - 500 = 100 ✅
// باقیمانده = اضافه‌ای که از سقف رد شده
leftRemainder = 350 - 300 = 50
rightRemainder = 400 - 300 = 100
مرحله 3️⃣: اعمال سقف
cappedBalances = MIN(500, 300) = 300 ✅
مرحله 4️⃣: محاسبه فلش
flushedPerSide = 500 - 300 = 200
از چپ: 200 فلش می‌شود
از راست: 200 فلش می‌شود
totalFlushed = 200 × 2 = 400 ✅
نتیجه نهایی:
✅ امتیاز این هفته: 300
✅ باقیمانده چپ: 0
✅ باقیمانده راست: 100
✅ جمع فلش: 400 (از بین می‌رود)
```
### **مقایسه منطق قدیم vs جدید:**
@@ -147,24 +181,34 @@ leftRemainder = leftTotal - cappedLeftTotal // باقیمانده از سقف
```csharp
// محاسبه تعداد کل اعضا در هر پا
leftLegBalances = CountAllMembers(userId, Left);
rightLegBalances = CountAllMembers(userId, Right);
## **Current (Correct) Logic - Updated 2025-12-09:**
// تعادل = کمترین مقدار
TotalBalances = MIN(leftLegBalances, rightLegBalances);
### **Formula (4 مرحله):**
```
// مرحله 1: جمع با هفته قبل
leftTotal = leftNewMembers + leftCarryover
rightTotal = rightNewMembers + rightCarryover
// مرحله 2: محاسبه تعادل اولیه
totalBalances = MIN(leftTotal, rightTotal)
// مرحله 3: محاسبه باقیمانده برای هفته بعد
leftRemainder = leftTotal - totalBalances
rightRemainder = rightTotal - totalBalances
// مرحله 4: اعمال سقف 300
cappedBalances = MIN(totalBalances, 300)
flushedPerSide = totalBalances - cappedBalances
totalFlushed = flushedPerSide × 2
```
**مشکلات:**
1. تعداد کل اعضا را می‌شمارد (نه فقط جدیدها)
2. باقیمانده هفته قبل را نادیده می‌گیرد
3. هر هفته از صفر شروع می‌کند
---
## ✅ **Current (Correct) Logic:**
### **Formula:**
```
### **Key Principles:**
1. **Only count NEW members** activated in current week
2. **Add carryover** from previous week (جداگانه چپ و راست)
3. **Calculate remainder** for next week (قبل از سقف)
4. **Apply cap 300** on final score (بعد از تعادل)
5. **Flush from both sides** if balance > 300
6. **Recursive counting** through entire tree structure
leftTotal = leftNewMembers + leftCarryover
rightTotal = rightNewMembers + rightCarryover
+656
View File
@@ -0,0 +1,656 @@
# Base Package Payment System - سیستم پرداخت پکیج پایه
**تاریخ ایجاد:** 2024-12-16
**تاریخ آخرین به‌روزرسانی:** 2024-12-16
**وضعیت:** ✅ پیاده‌سازی شده
**اولویت:** 🔴 بسیار بالا
---
## 📋 فهرست
1. [خلاصه سیستم](#خلاصه-سیستم)
2. [Business Requirements](#business-requirements)
3. [معماری سیستم](#معماری-سیستم)
4. [Implementation Details](#implementation-details)
5. [Club Membership Contract System](#club-membership-contract-system)
6. [API Endpoints](#api-endpoints)
7. [Flow Diagram](#flow-diagram)
8. [نکات مهم](#نکات-مهم)
---
## 🎯 خلاصه سیستم
سیستم پرداخت پکیج پایه امکان پرداخت **56 میلیون تومان** را برای کاربران فراهم می‌کند تا بتوانند:
1. کیف پول خود را شارژ کنند (Balance + DiscountBalance)
2. **امضای قرارداد باشگاه مشتریان** (گام الزامی بعد از پرداخت)
3. **فعالسازی لینک دعوت** (Referral Link) - تنها بعد از امضای قرارداد
4. دسترسی کامل به امکانات باشگاه مشتریان
### دو روش پرداخت:
1. **پرداخت مستقیم (Direct Payment)** - از طریق درگاه بانکی (زرین‌پال)
2. **اعتبار الماسی دایا (Daya Loan)** - از طریق سایت دایا
---
## 📊 Business Requirements
### شرایط نمایش لینک دعوت:
```
CanShowReferralLink = HasPurchasedPackage && IsClubMemberActive
```
- **HasPurchasedPackage**: کاربر پکیج پایه را خریداری کرده (PackagePurchaseMethod != None)
- **IsClubMemberActive**: قرارداد باشگاه مشتریان امضا شده (ClubMembership.IsActive = true)
⚠️ **نکته مهم**: پرداخت پکیج به تنهایی کافی نیست! کاربر باید قرارداد باشگاه مشتریان را نیز امضا کند.
### مقدار پکیج:
- **مبلغ**: 56,000,000 تومان
- **شارژ Balance**: 56,000,000 تومان
- **شارژ DiscountBalance**: 56,000,000 تومان
### PackagePurchaseMethod Enum:
```csharp
public enum PackagePurchaseMethod
{
None = 0, // هنوز خرید نکرده
DirectPurchase = 1, // پرداخت مستقیم
DayaLoan = 2 // اعتبار دایا
}
```
### ContractType Enum:
```csharp
public enum ContractType
{
Main = 0, // قرارداد ثبت‌نام اولیه
ClubMembership = 1, // قرارداد باشگاه مشتریان
}
```
---
## 🏗️ معماری سیستم
### Architecture Pattern:
```
Frontend (Blazor)
BFF (Backend For Frontend)
↓ ↘
CMS PYMS (Payment Gateway)
```
### Layer Responsibilities:
#### 1️⃣ Frontend (Blazor)
- نمایش UI برای انتخاب روش پرداخت
- فراخوانی BFF برای شروع پرداخت
- مدیریت Callback از درگاه
- نمایش نتیجه پرداخت
- **Modal غیرقابل بسته شدن برای امضای قرارداد باشگاه** (جدید ✨)
#### 2️⃣ BFF (Middle Layer)
- **InitiateBasePackagePayment**: هماهنگی بین CMS و PYMS
- فراخوانی CMS برای ثبت Transaction + Order
- فراخوانی PYMS برای دریافت URL درگاه
- برگرداندن URL به Frontend
- **VerifyBasePackagePayment**: تأیید پرداخت
- فراخوانی PYMS برای Verify
- فراخوانی CMS برای شارژ یا Reject
- **RequestClubContractOtp**: ارسال OTP برای امضای قرارداد (جدید ✨)
- **AcceptClubMembershipContract**: امضای قرارداد و فعالسازی باشگاه (جدید ✨)
#### 3️⃣ CMS (Core Business)
- **InitiateBasePackagePayment**: ثبت Transaction + Order با Pending
- **VerifyBasePackagePayment**: شارژ کیف پول یا Reject بر اساس نتیجه
- **AcceptClubMembershipContract**: ثبت UserContract و فعالسازی ClubMembership (جدید ✨)
#### 4️⃣ PYMS (Payment Gateway Service)
- **PaymentRequest**: دریافت URL درگاه زرین‌پال
- **PaymentVerification**: تأیید پرداخت از بانک
---
## 💻 Implementation Details
### CMS Layer
#### Commands:
1. **InitiateBasePackagePaymentCommand**
```csharp
// Input
public record InitiateBasePackagePaymentCommand
{
public long UserId { get; init; }
}
// Output
public class InitiateBasePackagePaymentResponseDto
{
public bool Success { get; set; }
public string Message { get; set; }
public long OrderId { get; set; }
public long TransactionId { get; set; }
public long Amount { get; set; } // 56,000,000
}
```
**Handler Logic:**
- بررسی عدم خرید قبلی: `user.PackagePurchaseMethod == None`
- بررسی عدم Order Pending قبلی
- ایجاد Transaction با PaymentStatus.Pending
- ایجاد UserOrder با PackageId=4, PaymentStatus.Pending
- Return OrderId + TransactionId
2. **VerifyBasePackagePaymentCommand**
```csharp
// Input
public record VerifyBasePackagePaymentCommand
{
public long OrderId { get; init; }
public long TransactionId { get; init; }
public bool PaymentSuccess { get; init; } // از BFF می‌آید
public string? RefId { get; init; }
public string? Message { get; init; }
}
// Output
public class VerifyBasePackagePaymentResponseDto
{
public bool Success { get; set; }
public string Message { get; set; }
public long OrderId { get; set; }
public long TransactionId { get; set; }
public string? ReferenceCode { get; set; }
public long WalletBalance { get; set; }
public long DiscountBalance { get; set; }
}
```
**Handler Logic (Success):**
- شارژ `wallet.Balance += 56,000,000`
- شارژ `wallet.DiscountBalance += 56,000,000`
- ثبت Transaction با PaymentStatus.Success
- ثبت UserWalletChangeLog (Balance + Discount)
- Update Order: PaymentStatus.Success, PaymentMethod.IPG
- Update User: PackagePurchaseMethod.DirectPurchase
**Handler Logic (Failed):**
- Update Transaction: PaymentStatus.Reject
- Update Order: PaymentStatus.Reject
#### Proto Definition:
```protobuf
// package.proto
service PackageContract {
rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest)
returns (InitiateBasePackagePaymentResponse);
rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest)
returns (VerifyBasePackagePaymentResponse);
}
message InitiateBasePackagePaymentRequest {
int64 user_id = 1;
}
message InitiateBasePackagePaymentResponse {
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
int64 amount = 5;
}
message VerifyBasePackagePaymentRequest {
int64 order_id = 1;
int64 transaction_id = 2;
bool payment_success = 3;
google.protobuf.StringValue ref_id = 4;
google.protobuf.StringValue message = 5;
}
message VerifyBasePackagePaymentResponse {
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
google.protobuf.StringValue reference_code = 5;
int64 wallet_balance = 6;
int64 discount_balance = 7;
}
```
#### Files Created/Modified:
```
CMS/src/CMSMicroservice.Application/PackageCQ/Commands/
├── InitiateBasePackagePayment/
│ ├── InitiateBasePackagePaymentCommand.cs
│ ├── InitiateBasePackagePaymentCommandValidator.cs
│ └── InitiateBasePackagePaymentCommandHandler.cs
└── VerifyBasePackagePayment/
├── VerifyBasePackagePaymentCommand.cs
├── VerifyBasePackagePaymentCommandValidator.cs
└── VerifyBasePackagePaymentCommandHandler.cs
CMS/src/CMSMicroservice.Protobuf/Protos/
└── package.proto (updated)
CMS/src/CMSMicroservice.WebApi/
├── Services/PackageService.cs (updated)
└── Common/Mappings/PackageProfile.cs (updated)
```
---
### BFF Layer
#### Commands:
1. **InitiateBasePackagePaymentCommand**
```csharp
// Input (UserId از CurrentUserService گرفته می‌شود)
public record InitiateBasePackagePaymentCommand
{
public string CallbackUrl { get; init; }
}
// Output
public class InitiateBasePackagePaymentResponseDto
{
public bool Success { get; set; }
public string Message { get; set; }
public long OrderId { get; set; }
public long TransactionId { get; set; }
public long Amount { get; set; }
public string PaymentGatewayUrl { get; set; }
public string Authority { get; set; }
}
```
**Handler Logic:**
```csharp
// 1. فراخوانی CMS
var cmsResponse = await _context.Package.InitiateBasePackagePaymentAsync(
new InitiateBasePackagePaymentRequest {
UserId = _currentUserService.UserId.Value
});
// 2. فراخوانی PYMS
var paymentResponse = await _context.ZarinTransactions.PaymentRequestAsync(
new PaymentRequestRequest {
MerchantId = "...",
Amount = cmsResponse.Amount * 10, // تبدیل به ریال
CallbackUrl = $"{request.CallbackUrl}?orderId={...}&transactionId={...}",
Description = "پرداخت پکیج پایه",
Currency = CurrencyEnum.Irr,
Type = TransactionTypeEnum.Real
});
// 3. Return URL + Authority
return new InitiateBasePackagePaymentResponseDto {
PaymentGatewayUrl = paymentResponse.PaymentGWUrl,
Authority = ExtractAuthorityFromUrl(paymentResponse.PaymentGWUrl),
...
};
```
2. **VerifyBasePackagePaymentCommand**
```csharp
// Input
public record VerifyBasePackagePaymentCommand
{
public long OrderId { get; init; }
public long TransactionId { get; init; }
public string Authority { get; init; }
public string Status { get; init; } // OK یا NOK
}
```
**Handler Logic:**
```csharp
// 1. بررسی Status
if (request.Status != "OK") {
await NotifyCmsPaymentFailed(...);
return Failed;
}
// 2. Verify از PYMS
var verifyResponse = await _context.ZarinTransactions
.PaymentVerificationAsync(...);
// 3. فراخوانی CMS
if (verifyResponse.PaymentStatus) {
var cmsResponse = await _context.Package.VerifyBasePackagePaymentAsync(
new VerifyBasePackagePaymentRequest {
OrderId = request.OrderId,
TransactionId = request.TransactionId,
PaymentSuccess = true,
RefId = verifyResponse.RefId,
Message = verifyResponse.Message
});
return Success;
} else {
await NotifyCmsPaymentFailed(...);
return Failed;
}
```
#### Proto Definition:
```protobuf
// package.proto
service PackageContract {
rpc InitiateBasePackagePayment(InitiateBasePackagePaymentRequest)
returns (InitiateBasePackagePaymentResponse) {
option (google.api.http) = {
post: "/InitiateBasePackagePayment"
body: "*"
};
};
rpc VerifyBasePackagePayment(VerifyBasePackagePaymentRequest)
returns (VerifyBasePackagePaymentResponse) {
option (google.api.http) = {
post: "/VerifyBasePackagePayment"
body: "*"
};
};
}
message InitiateBasePackagePaymentRequest {
string callback_url = 1;
// UserId از JWT token گرفته می‌شود
}
message InitiateBasePackagePaymentResponse {
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
int64 amount = 5;
string payment_gateway_url = 6;
string authority = 7;
}
message VerifyBasePackagePaymentRequest {
int64 order_id = 1;
int64 transaction_id = 2;
string authority = 3;
string status = 4;
}
message VerifyBasePackagePaymentResponse {
bool success = 1;
string message = 2;
int64 order_id = 3;
int64 transaction_id = 4;
google.protobuf.StringValue ref_id = 5;
int64 wallet_balance = 6;
int64 discount_balance = 7;
}
```
#### Files Created/Modified:
```
FrontOffice.BFF/src/FrontOffice.BFF.Application/PackageCQ/Commands/
├── InitiateBasePackagePayment/
│ ├── InitiateBasePackagePaymentCommand.cs
│ ├── InitiateBasePackagePaymentCommandValidator.cs
│ └── InitiateBasePackagePaymentCommandHandler.cs
└── VerifyBasePackagePayment/
├── VerifyBasePackagePaymentCommand.cs
├── VerifyBasePackagePaymentCommandValidator.cs
└── VerifyBasePackagePaymentCommandHandler.cs
FrontOffice.BFF/src/Protobufs/FrontOffice.BFF.Package.Protobuf/Protos/
└── package.proto (updated)
FrontOffice.BFF/src/FrontOffice.BFF.WebApi/
├── Services/PackageService.cs (updated)
└── Common/Mappings/PackageProfile.cs (updated)
FrontOffice.BFF/src/FrontOffice.BFF.Domain/
└── FrontOffice.BFF.Domain.csproj (updated - added CMS Proto reference)
```
---
### Frontend Layer
#### Pages:
1. **Profile/Index.razor.cs**
- نمایش دکمه "خرید پکیج پایه"
- Bottom Sheet با دو گزینه: پرداخت مستقیم / اعتبار الماسی
- فراخوانی BFF.InitiateBasePackagePayment
```csharp
private async Task DirectPayment()
{
var callbackUrl = $"{Navigation.BaseUri}profile/payment-callback";
var response = await PackageContract.InitiateBasePackagePaymentAsync(
new InitiateBasePackagePaymentRequest {
CallbackUrl = callbackUrl
});
if (response.Success) {
Navigation.NavigateTo(response.PaymentGatewayUrl, forceLoad: true);
}
}
```
2. **Profile/PaymentCallback.razor**
- دریافت Query Parameters: orderId, transactionId, Authority, Status
- فراخوانی BFF.VerifyBasePackagePayment
- نمایش نتیجه (موفق/ناموفق)
```csharp
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender) {
var response = await PackageContract.VerifyBasePackagePaymentAsync(
new VerifyBasePackagePaymentRequest {
OrderId = OrderId,
TransactionId = TransactionId,
Authority = Authority,
Status = Status
});
// نمایش نتیجه
}
}
```
#### Files Created/Modified:
```
FrontOffice/src/FrontOffice.Main/Pages/Profile/
├── Index.razor.cs (updated)
└── PaymentCallback.razor (new)
FrontOffice/src/FrontOffice.Main/Utilities/
├── UserAuthInfo.cs (updated - added UserId)
└── AuthService.cs (updated - extract UserId from JWT)
FrontOffice/src/FrontOffice.Main/
└── FrontOffice.Main.csproj (updated - added BFF Package Proto reference)
```
---
## 🔌 API Endpoints
### BFF Endpoints (gRPC-Web + HTTP):
```
POST /InitiateBasePackagePayment
Body: {
"callback_url": "https://example.com/profile/payment-callback"
}
Response: {
"success": true,
"message": "...",
"order_id": 123,
"transaction_id": 456,
"amount": 56000000,
"payment_gateway_url": "https://www.zarinpal.com/pg/StartPay/...",
"authority": "A00000000000000000000000000123456"
}
```
```
POST /VerifyBasePackagePayment
Body: {
"order_id": 123,
"transaction_id": 456,
"authority": "A00000000000000000000000000123456",
"status": "OK"
}
Response: {
"success": true,
"message": "پرداخت با موفقیت تایید شد",
"order_id": 123,
"transaction_id": 456,
"ref_id": "789",
"wallet_balance": 56000000,
"discount_balance": 56000000
}
```
---
## 📊 Flow Diagram
### Complete Payment Flow:
```mermaid
sequenceDiagram
participant User as کاربر
participant FE as Frontend
participant BFF as BFF
participant CMS as CMS
participant PYMS as PYMS
participant Bank as درگاه بانک
User->>FE: کلیک "پرداخت مستقیم"
FE->>BFF: InitiateBasePackagePayment(CallbackUrl)
BFF->>BFF: استخراج UserId از JWT
BFF->>CMS: InitiateBasePackagePayment(UserId)
CMS->>CMS: ثبت Transaction (Pending)
CMS->>CMS: ثبت Order (Pending)
CMS-->>BFF: OrderId, TransactionId, Amount
BFF->>PYMS: PaymentRequest(Amount, Callback)
PYMS-->>BFF: PaymentGWUrl, Authority
BFF-->>FE: PaymentGWUrl, OrderId, TransactionId
FE->>Bank: Redirect to PaymentGWUrl
User->>Bank: پرداخت
Bank-->>FE: Redirect to Callback?Authority=...&Status=OK
FE->>BFF: VerifyBasePackagePayment(OrderId, TransactionId, Authority, Status)
BFF->>PYMS: PaymentVerification(Authority)
PYMS-->>BFF: PaymentStatus, RefId
alt پرداخت موفق
BFF->>CMS: VerifyBasePackagePayment(PaymentSuccess=true, RefId)
CMS->>CMS: شارژ Balance (56M)
CMS->>CMS: شارژ DiscountBalance (56M)
CMS->>CMS: ثبت Transaction (Success)
CMS->>CMS: ثبت WalletChangeLog
CMS->>CMS: Update Order (Success)
CMS->>CMS: Update User.PackagePurchaseMethod
CMS-->>BFF: Success, WalletBalance, DiscountBalance
BFF-->>FE: Success
FE-->>User: نمایش پیام موفقیت + موجودی
else پرداخت ناموفق
BFF->>CMS: VerifyBasePackagePayment(PaymentSuccess=false)
CMS->>CMS: Update Transaction (Reject)
CMS->>CMS: Update Order (Reject)
CMS-->>BFF: Failed
BFF-->>FE: Failed
FE-->>User: نمایش پیام خطا
end
```
---
## ⚠️ نکات مهم
### Security:
1. **UserId از JWT گرفته می‌شود** نه از Request - امنیت بالاتر
2. **Validation در هر لایه** انجام می‌شود
3. **Transaction Idempotency** - چک می‌شود که Order Pending قبلی وجود نداشته باشد
### Business Logic:
1. کاربر **فقط یک بار** می‌تواند پکیج پایه بخرد
2. **شارژ هم‌زمان** Balance و DiscountBalance انجام می‌شود
3. **PackagePurchaseMethod** بعد از پرداخت موفق به `DirectPurchase` تغییر می‌کند
4. برای فعالسازی لینک دعوت، باید **هم پکیج خریداری شود هم باشگاه فعال شود**
### Error Handling:
1. اگر CMS خطا برگرداند، به درگاه نمی‌رویم
2. اگر PYMS URL ندهد، Transaction در CMS باقی می‌ماند (Pending)
3. اگر Callback با Status=NOK بیاید، مستقیماً Reject می‌شود
4. اگر Verification ناموفق باشد، Transaction و Order به Reject تغییر می‌کند
### Project References:
برای development، از Project Reference استفاده می‌شود:
- BFF → CMS.Protobuf (Project Reference)
- Frontend → BFF.Package.Protobuf (Project Reference)
برای production، باید به NuGet Package تبدیل شوند.
---
## ✅ Checklist پیاده‌سازی
### CMS:
- [x] InitiateBasePackagePaymentCommand
- [x] InitiateBasePackagePaymentCommandValidator
- [x] InitiateBasePackagePaymentCommandHandler
- [x] VerifyBasePackagePaymentCommand
- [x] VerifyBasePackagePaymentCommandValidator
- [x] VerifyBasePackagePaymentCommandHandler
- [x] Proto messages و RPCs
- [x] PackageService implementation
- [x] Mapster mappings
### BFF:
- [x] InitiateBasePackagePaymentCommand
- [x] InitiateBasePackagePaymentCommandValidator
- [x] InitiateBasePackagePaymentCommandHandler
- [x] VerifyBasePackagePaymentCommand
- [x] VerifyBasePackagePaymentCommandValidator
- [x] VerifyBasePackagePaymentCommandHandler
- [x] Proto messages و RPCs
- [x] PackageService implementation
- [x] Mapster mappings
- [x] CurrentUserService integration
### Frontend:
- [x] Bottom Sheet UI برای انتخاب روش پرداخت
- [x] DirectPayment method
- [x] PaymentCallback page
- [x] UserAuthInfo.UserId
- [x] AuthService extract UserId
- [x] Navigation to payment gateway
- [x] Display payment result
### Testing:
- [ ] Test پرداخت موفق
- [ ] Test پرداخت ناموفق
- [ ] Test لغو پرداخت توسط کاربر
- [ ] Test خرید مجدد (باید خطا دهد)
- [ ] Test شارژ کیف پول
- [ ] Test فعالسازی لینک دعوت
---
**تاریخ آخرین به‌روزرسانی:** 2024-12-16
**نگارنده:** Development Team
@@ -0,0 +1,546 @@
# محاسبات پلن باینری (Binary Plan Calculations)
## مستندات فرمول‌های محاسبه کمیسیون باینری
این سند فرمول‌های محاسباتی سیستم کمیسیون باینری را که از فایل اکسل استخراج شده، توضیح می‌دهد.
---
## متغیرها و تعاریف
### ورودی‌های هفته قبل (Last Week Remainders)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **باقیمانده هفته قبل چپ** | `LL` (Last Left) | باقیمانده‌ای که از هفته قبل در پای چپ باقی مانده |
| **باقیمانده هفته قبل راست** | `LR` (Last Right) | باقیمانده‌ای که از هفته قبل در پای راست باقی مانده |
**مثال از اکسل:**
- `LL = 200` (میلیون ریال)
- `LR = 0`
---
### ورودی‌های هفته جدید (New Week Values)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **هفته جدید چپ** | `NL` (New Left) | مجموع فروش/شارژ پای چپ در هفته جاری |
| **هفته جدید راست** | `NR` (New Right) | مجموع فروش/شارژ پای راست در هفته جاری |
**مثال از اکسل:**
- `NL = 400` (میلیون ریال)
- `NR = 500` (میلیون ریال)
---
### پارامتر سیستم (System Parameter)
| نام فارسی | نماد | توضیحات |
|-----------|------|---------|
| **ماکسیمم تعادل** | `MX` (Maximum Balance) | حداکثر مقداری که در یک هفته می‌تواند به عنوان تعادل (کمیسیون) محاسبه شود |
**مثال از اکسل:**
- `MX = 300` (میلیون ریال)
**نکته مهم:** این مقدار معمولاً بر اساس سطح کاربر یا پکیج خریداری شده تعیین می‌شود.
---
## فرمول‌های محاسباتی
### 1️⃣ محاسبه مجموع پا چپ (Sum Left Total)
```
SLT = LL + NL
```
**توضیح:**
- `SLT` (Sum Left Total) = مجموع کل پای چپ
- باقیمانده هفته قبل + فروش هفته جدید
**مثال:**
```
SLT = 200 + 400 = 600
```
---
### 2️⃣ محاسبه مجموع پا راست (Sum Right Total)
```
SRT = LR + NR
```
**توضیح:**
- `SRT` (Sum Right Total) = مجموع کل پای راست
- باقیمانده هفته قبل + فروش هفته جدید
**مثال:**
```
SRT = 0 + 500 = 500
```
---
### 3️⃣ محاسبه کمترین کل (Minimum Total)
```
MinT = MIN(SLT, SRT)
```
**توضیح:**
- `MinT` = کوچکترین مقدار بین دو پا
- این مقدار نشان‌دهنده حداکثر تعادل بالقوه است
**مثال:**
```
MinT = MIN(600, 500) = 500
```
---
### 4️⃣ محاسبه باقیمانده هفته بعد چپ (Remainder Next Week Left)
```
RNWL = SLT - MinT
```
**توضیح:**
- `RNWL` (Remainder Next Week Left) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
- مازاد پای چپ که برای تعادل استفاده نشد
**مثال:**
```
RNWL = 600 - 500 = 100
```
---
### 5️⃣ محاسبه باقیمانده هفته بعد راست (Remainder Next Week Right)
```
RNWR = SRT - MinT
```
**توضیح:**
- `RNWR` (Remainder Next Week Right) = باقیمانده‌ای که به هفته بعد منتقل می‌شود
- مازاد پای راست که برای تعادل استفاده نشد
**مثال:**
```
RNWR = 500 - 500 = 0
```
**نکته:** یکی از دو باقیمانده همیشه صفر است (چون MinT کوچکترین است).
---
### 6️⃣ محاسبه فلش چپ (Flush Left)
```
FL = SLT - MX - RNWL
```
**توضیح:**
- `FL` (Flush Left) = مقداری که از ماکسیمم هم بیشتر بود و باید دور ریخته شود
- این مقدار نشان‌دهنده سرریز (overflow) است که نمی‌تواند به هفته بعد منتقل شود
**مثال:**
```
FL = 600 - 300 - 100 = 200
```
**معنی:** از 600 میلیون پای چپ:
- 300 به عنوان کمیسیون استفاده شد (تا حد MX)
- 100 به هفته بعد منتقل شد
- **200 فلش شد (از دست رفت)** ❌
---
### 7️⃣ محاسبه فلش راست (Flush Right)
```
FR = SRT - MX - RNWR
```
**توضیح:**
- `FR` (Flush Right) = مقداری که از پای راست دور ریخته می‌شود
**مثال:**
```
FR = 500 - 300 - 0 = 200
```
**معنی:** از 500 میلیون پای راست:
- 300 به عنوان کمیسیون استفاده شد
- 0 به هفته بعد منتقل شد
- **200 فلش شد (از دست رفت)** ❌
---
### 8️⃣ محاسبه کل تعادل (Total Balance / Commission)
```
TB = IF(MinT > MX, MX, MinT)
```
یا به زبان ساده‌تر:
```
TB = MIN(MinT, MX)
```
**توضیح:**
- `TB` (Total Balance) = مقدار واقعی کمیسیونی که به کاربر تعلق می‌گیرد
- نمی‌تواند از ماکسیمم تعادل (`MX`) بیشتر شود
**مثال:**
```
TB = MIN(500, 300) = 300
```
**معنی:** هرچند تعادل واقعی 500 بود، اما به دلیل محدودیت `MX`، فقط 300 به عنوان کمیسیون پرداخت می‌شود.
---
## خلاصه جریان محاسبات
```
┌─────────────────────────────────────────────────────────────┐
│ ورودی‌ها │
├─────────────────────────────────────────────────────────────┤
│ LL = 200 باقیمانده هفته قبل چپ │
│ LR = 0 باقیمانده هفته قبل راست │
│ NL = 400 هفته جدید چپ │
│ NR = 500 هفته جدید راست │
│ MX = 300 ماکسیمم تعادل │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 1: محاسبه مجموع دو پا │
├─────────────────────────────────────────────────────────────┤
│ SLT = LL + NL = 200 + 400 = 600 │
│ SRT = LR + NR = 0 + 500 = 500 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 2: محاسبه کمترین کل │
├─────────────────────────────────────────────────────────────┤
│ MinT = MIN(SLT, SRT) = MIN(600, 500) = 500 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 3: محاسبه کمیسیون واقعی (با اعمال Cap) │
├─────────────────────────────────────────────────────────────┤
│ TB = MIN(MinT, MX) = MIN(500, 300) = 300 ✅ کمیسیون │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 4: محاسبه باقیمانده هفته بعد │
├─────────────────────────────────────────────────────────────┤
│ RNWL = SLT - MinT = 600 - 500 = 100 → هفته بعد │
│ RNWR = SRT - MinT = 500 - 500 = 0 → هفته بعد │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ گام 5: محاسبه فلش (از دست رفته) │
├─────────────────────────────────────────────────────────────┤
│ FL = SLT - MX - RNWL = 600 - 300 - 100 = 200 ❌ فلش │
│ FR = SRT - MX - RNWR = 500 - 300 - 0 = 200 ❌ فلش │
└─────────────────────────────────────────────────────────────┘
```
---
## تحلیل نتایج
### 📊 خروجی‌های نهایی
| مقدار | توضیح | وضعیت |
|-------|-------|-------|
| **TB = 300** | کمیسیون پرداختی این هفته | ✅ پرداخت می‌شود |
| **RNWL = 100** | باقیمانده پای چپ برای هفته بعد | ⏭️ منتقل می‌شود |
| **RNWR = 0** | باقیمانده پای راست برای هفته بعد | ⏭️ منتقل می‌شود |
| **FL = 200** | فلش پای چپ | ❌ از دست می‌رود |
| **FR = 200** | فلش پای راست | ❌ از دست می‌رود |
---
### 🔍 تفسیر کسب‌وکار
#### کمیسیون محاسبه شده
```
کمیسیون = 300 میلیون ریال
```
- به دلیل محدودیت `MX = 300`، از تعادل بالقوه 500، فقط 300 قابل برداشت است
- این یک مکانیزم کنترل هزینه است
#### باقیمانده به هفته بعد
```
هفته بعد LL = 100 (از پای چپ)
هفته بعد LR = 0 (از پای راست)
```
- 100 میلیون از پای چپ به هفته بعد منتقل می‌شود
- این باقیمانده در محاسبات هفته آینده دوباره استفاده خواهد شد
#### فلش (Flush) - نکته مهم ⚠️
```
فلش کل = 400 میلیون ریال (200 چپ + 200 راست)
```
**چرا فلش رخ می‌دهد؟**
1. مجموع دو پا = 1100 میلیون (600 + 500)
2. کمیسیون محاسبه شده = 300 میلیون
3. باقیمانده منتقل شده = 100 میلیون
4. فلش = 1100 - 300 - 100 = 700 میلیون ❌
**توضیح:**
- فلش نشان‌دهنده مقداری است که به دلیل **عدم تعادل** و **محدودیت Cap** از دست می‌رود
- این یک ضرر برای کاربر است که می‌تواند با متعادل کردن دو پا کاهش یابد
---
## پیاده‌سازی در C#
### کلاس مدل
```csharp
public class BinaryPlanCalculationInput
{
// ورودی‌های هفته قبل
public decimal LastLeftRemainder { get; set; } // LL
public decimal LastRightRemainder { get; set; } // LR
// ورودی‌های هفته جاری
public decimal NewLeftVolume { get; set; } // NL
public decimal NewRightVolume { get; set; } // NR
// تنظیمات سیستم
public decimal MaximumBalance { get; set; } // MX
}
public class BinaryPlanCalculationResult
{
// محاسبات واسط
public decimal SumLeftTotal { get; set; } // SLT
public decimal SumRightTotal { get; set; } // SRT
public decimal MinimumTotal { get; set; } // MinT
// باقیمانده‌ها
public decimal RemainderNextWeekLeft { get; set; } // RNWL
public decimal RemainderNextWeekRight { get; set; } // RNWR
// فلش
public decimal FlushLeft { get; set; } // FL
public decimal FlushRight { get; set; } // FR
// نتیجه نهایی
public decimal TotalBalance { get; set; } // TB - کمیسیون واقعی
public decimal TotalFlush { get; set; } // مجموع فلش
}
```
---
### متد محاسبه
```csharp
public static BinaryPlanCalculationResult Calculate(BinaryPlanCalculationInput input)
{
var result = new BinaryPlanCalculationResult();
// گام 1: محاسبه مجموع دو پا
result.SumLeftTotal = input.LastLeftRemainder + input.NewLeftVolume;
result.SumRightTotal = input.LastRightRemainder + input.NewRightVolume;
// گام 2: محاسبه کمترین کل
result.MinimumTotal = Math.Min(result.SumLeftTotal, result.SumRightTotal);
// گام 3: محاسبه کمیسیون واقعی (با اعمال Cap)
result.TotalBalance = Math.Min(result.MinimumTotal, input.MaximumBalance);
// گام 4: محاسبه باقیمانده هفته بعد
result.RemainderNextWeekLeft = result.SumLeftTotal - result.MinimumTotal;
result.RemainderNextWeekRight = result.SumRightTotal - result.MinimumTotal;
// گام 5: محاسبه فلش
result.FlushLeft = result.SumLeftTotal - input.MaximumBalance - result.RemainderNextWeekLeft;
result.FlushRight = result.SumRightTotal - input.MaximumBalance - result.RemainderNextWeekRight;
// محاسبه مجموع فلش
result.TotalFlush = result.FlushLeft + result.FlushRight;
// اطمینان از عدم منفی شدن فلش
result.FlushLeft = Math.Max(0, result.FlushLeft);
result.FlushRight = Math.Max(0, result.FlushRight);
result.TotalFlush = Math.Max(0, result.TotalFlush);
return result;
}
```
---
### مثال استفاده
```csharp
var input = new BinaryPlanCalculationInput
{
LastLeftRemainder = 200_000_000, // 200 میلیون
LastRightRemainder = 0,
NewLeftVolume = 400_000_000, // 400 میلیون
NewRightVolume = 500_000_000, // 500 میلیون
MaximumBalance = 300_000_000 // 300 میلیون
};
var result = Calculate(input);
Console.WriteLine($"کمیسیون قابل پرداخت: {result.TotalBalance:N0} ریال");
// Output: کمیسیون قابل پرداخت: 300,000,000 ریال
Console.WriteLine($"باقیمانده چپ هفته بعد: {result.RemainderNextWeekLeft:N0} ریال");
// Output: باقیمانده چپ هفته بعد: 100,000,000 ریال
Console.WriteLine($"باقیمانده راست هفته بعد: {result.RemainderNextWeekRight:N0} ریال");
// Output: باقیمانده راست هفته بعد: 0 ریال
Console.WriteLine($"فلش کل: {result.TotalFlush:N0} ریال");
// Output: فلش کل: 400,000,000 ریال
```
---
## نکات مهم برای پیاده‌سازی
### 1️⃣ ذخیره باقیمانده‌ها
```csharp
// باید در دیتابیس ذخیره شود
await SaveWeeklyRemainders(userId, weekId, new WeeklyRemainders
{
LeftRemainder = result.RemainderNextWeekLeft,
RightRemainder = result.RemainderNextWeekRight
});
```
### 2️⃣ لاگ فلش برای تحلیل
```csharp
if (result.TotalFlush > 0)
{
await LogFlush(userId, weekId, new FlushLog
{
FlushLeft = result.FlushLeft,
FlushRight = result.FlushRight,
Reason = "Cap limitation and imbalance"
});
}
```
### 3️⃣ تعیین MaximumBalance
```csharp
// بر اساس سطح کاربر
decimal GetMaximumBalance(User user)
{
return user.MembershipLevel switch
{
MembershipLevel.Bronze => 100_000_000,
MembershipLevel.Silver => 300_000_000,
MembershipLevel.Gold => 500_000_000,
MembershipLevel.Platinum => 1_000_000_000,
_ => 50_000_000
};
}
```
### 4️⃣ واحد پول
```csharp
// همه مقادیر باید در واحد ریال ذخیره شوند
// برای نمایش می‌توان به میلیون یا تومان تبدیل کرد
decimal DisplayInMillions(decimal rials) => rials / 1_000_000;
decimal DisplayInTomans(decimal rials) => rials / 10;
```
---
## سناریوهای مختلف
### سناریو 1: تعادل کامل
```
LL = 0, LR = 0, NL = 300, NR = 300, MX = 500
→ TB = 300, RNWL = 0, RNWR = 0, FL = 0, FR = 0
```
**نتیجه:** کمیسیون کامل بدون فلش ✅
---
### سناریو 2: یک پا خیلی بیشتر
```
LL = 0, LR = 0, NL = 1000, NR = 100, MX = 500
→ TB = 100, RNWL = 900, RNWR = 0, FL = 400, FR = 0
```
**نتیجه:** کمیسیون کم + فلش زیاد ❌
---
### سناریو 3: باقیمانده قبلی موثر
```
LL = 400, LR = 0, NL = 100, NR = 400, MX = 300
→ SLT = 500, SRT = 400
→ TB = 300, RNWL = 100, RNWR = 0, FL = 100, FR = 100
```
**نتیجه:** باقیمانده قبلی در محاسبه کمیسیون موثر است ✅
---
## تفاوت با کد فعلی
### در کد فعلی (`CalculateWeeklyBalancesCommandHandler.cs`):
```csharp
// 1. ابتدا Cap اعمال می‌شود
var cappedLeft = Math.Min(leftLegTotal, maxBalance);
var cappedRight = Math.Min(rightLegTotal, maxBalance);
// 2. سپس تعادل محاسبه می‌شود
var balance = Math.Min(cappedLeft, cappedRight);
// 3. باقیمانده‌ها محاسبه می‌شوند
var leftRemainder = leftLegTotal - balance;
var rightRemainder = rightLegTotal - balance;
```
### در فرمول اکسل:
```csharp
// 1. ابتدا تعادل کامل محاسبه می‌شود
var minTotal = Math.Min(leftLegTotal, rightLegTotal);
// 2. سپس Cap اعمال می‌شود
var balance = Math.Min(minTotal, maxBalance);
// 3. باقیمانده‌ها بر اساس minTotal محاسبه می‌شوند
var leftRemainder = leftLegTotal - minTotal;
var rightRemainder = rightLegTotal - minTotal;
// 4. فلش محاسبه می‌شود
var flushLeft = leftLegTotal - maxBalance - leftRemainder;
var flushRight = rightLegTotal - maxBalance - rightRemainder;
```
**تفاوت کلیدی:**
- کد فعلی Cap را ابتدا اعمال می‌کند (می‌تواند باقیمانده‌های بیشتری ایجاد کند)
- فرمول اکسل ابتدا تعادل را محاسبه می‌کند، سپس Cap اعمال می‌شود (فلش دقیق‌تر محاسبه می‌شود)
---
## نتیجه‌گیری
این فرمول‌ها نشان می‌دهند که:
1.**تعادل اهمیت دارد** - هرچه دو پا متعادل‌تر باشند، فلش کمتر است
2.**Cap محدودیت ایجاد می‌کند** - حتی با تعادل کامل، بیش از MX کمیسیون داده نمی‌شود
3.**باقیمانده‌ها منتقل می‌شوند** - برای هفته بعد ذخیره می‌شوند
4.**فلش ضرر است** - مقداری که به دلیل عدم تعادل یا Cap از دست می‌رود
**توصیه:** برای افزایش کمیسیون، کاربران باید:
- دو پای خود را متعادل نگه دارند
- سطح عضویت خود را ارتقا دهند (برای افزایش MX)
- از باقیمانده‌ها در هفته‌های بعد استفاده کنند
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,317 @@
# اصلاحات سیستم کمیسیون هفتگی
## 📋 خلاصه تغییرات
سیستم کمیسیون هفتگی از **3 مرحله به 2 مرحله** ساده‌سازی شد:
### ❌ قبل (3 مرحله):
1. `CalculateWeeklyBalances` - محاسبه تعادل‌ها
2. `CalculateWeeklyCommissionPool` - محاسبه استخر
3. `ProcessUserPayouts` - پردازش پرداخت‌ها (تکراری!)
### ✅ بعد (2 مرحله):
1. `CalculateWeeklyBalances` - محاسبه تعادل‌ها تا 15 لول
2. `CalculateWeeklyCommissionPool` - محاسبه استخر + پردازش پرداخت‌ها
---
## 🔧 تغییرات جزئی
### 1️⃣ اضافه شدن فیلدها به `NetworkWeeklyBalance`
**فیلدهای جدید:**
```csharp
/// <summary>
/// مقدار فلش هر طرف (بعد از اعمال Cap)
/// </summary>
public int FlushedPerSide { get; set; }
/// <summary>
/// مجموع فلش از دو طرف (از دست رفته)
/// </summary>
public int TotalFlushed { get; set; }
```
**Migration:** `AddFlushedFieldsToNetworkWeeklyBalance`
---
### 2️⃣ اصلاح `CalculateWeeklyBalances`
**تغییرات:**
- ✅ فیلدهای `FlushedPerSide` و `TotalFlushed` ذخیره می‌شوند
-`WeeklyPoolContribution = 0` (دیگر در این مرحله محاسبه نمیشه)
- ✅ محدودیت 15 لول قبلاً موجود بود و درست کار می‌کند
**کد:**
```csharp
// محاسبه فلش
var flushedPerSide = totalBalances - cappedBalances;
var totalFlushed = flushedPerSide * 2;
// ذخیره
balance.FlushedPerSide = flushedPerSide;
balance.TotalFlushed = totalFlushed;
balance.WeeklyPoolContribution = 0; // Pool در مرحله بعد محاسبه میشه
```
---
### 3️⃣ اصلاح کامل `CalculateWeeklyCommissionPool`
**منطق جدید Pool:**
```csharp
// 1. Pool از فعالسازی‌های باشگاه این هفته میاد (نه از تعادل‌ها)
var newClubMembersCount = await _context.ClubMemberships
.Where(c => c.ActivatedAt >= startDate && c.ActivatedAt <= endDate)
.CountAsync();
var totalPoolAmount = newClubMembersCount * activationFee;
// 2. ارزش هر امتیاز
var totalBalancesInNetwork = weeklyBalances.Sum(x => x.TotalBalances);
var valuePerBalance = totalPoolAmount / totalBalancesInNetwork;
```
**افزوده شدن محاسبه تعادل زیرمجموعه:**
```csharp
// برای هر کاربر:
// 1. تعادل خودش
var directBalances = balance.TotalBalances;
// 2. تعادل زیرمجموعه (تا 15 لول)
var subordinateBalances = await CalculateSubordinateBalancesAsync(
balance.UserId,
request.WeekNumber,
maxLevels: 15
);
var totalBalancesForUser = directBalances + subordinateBalances;
```
**ایجاد UserCommissionPayout:**
```csharp
var payout = new UserCommissionPayout
{
UserId = balance.UserId,
WeekNumber = request.WeekNumber,
WeeklyPoolId = existingPool.Id,
BalancesEarned = totalBalancesForUser,
ValuePerBalance = valuePerBalance,
TotalAmount = totalBalancesForUser * valuePerBalance,
Status = CommissionPayoutStatus.Pending,
// ... subordinate fields
};
```
**ثبت تاریخچه:**
```csharp
var history = new CommissionPayoutHistory
{
UserId = payout.UserId,
PayoutId = payout.Id,
Amount = payout.TotalAmount,
Status = CommissionPayoutStatus.Pending,
ChangeReason = "محاسبه اولیه کمیسیون هفتگی"
};
```
---
### 4️⃣ ساده‌سازی `TriggerWeeklyCalculation`
**قبل:**
```csharp
// Step 1
await _mediator.Send(new CalculateWeeklyBalancesCommand { ... });
// Step 2
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand { ... });
// Step 3
await _mediator.Send(new ProcessUserPayoutsCommand { ... });
```
**بعد:**
```csharp
// Step 1: محاسبه تعادل‌ها
if (!request.SkipBalances)
{
await _mediator.Send(new CalculateWeeklyBalancesCommand { ... });
}
// Step 2: محاسبه Pool و پرداخت‌ها
if (!request.SkipPayouts)
{
await _mediator.Send(new CalculateWeeklyCommissionPoolCommand { ... });
}
```
**حذف شد:**
-`SkipPool` flag
- ❌ Step 3 کاملاً حذف شد
---
## 🎯 فرآیند نهایی
### مرحله 1: محاسبه تعادل‌ها
```
1. برای هر کاربر در شبکه
2. تا 15 لول پایین‌تر شمارش کن
3. محاسبه تعادل (MIN of left/right)
4. محاسبه باقیمانده
5. محاسبه فلش
6. ذخیره در NetworkWeeklyBalance
```
### مرحله 2: محاسبه Pool و توزیع
```
1. شمارش فعالسازی‌های باشگاه این هفته
2. Pool = تعداد × ActivationFee
3. ارزش هر امتیاز = Pool ÷ مجموع تعادل‌ها
4. برای هر کاربر:
a. تعادل خودش + تعادل زیرمجموعه (تا 15 لول)
b. سهم = تعادل × ارزش
c. ثبت در UserCommissionPayout
d. ثبت تاریخچه
```
---
## 📊 جداول درگیر
### `NetworkWeeklyBalance` (فیلدهای جدید)
```sql
ALTER TABLE [Network].[NetworkWeeklyBalances]
ADD [FlushedPerSide] INT NOT NULL DEFAULT 0,
[TotalFlushed] INT NOT NULL DEFAULT 0;
```
### `WeeklyCommissionPool`
```
- TotalPoolAmount: از فعالسازی‌های باشگاه
- TotalBalances: مجموع تعادل‌های شبکه
- ValuePerBalance: Pool ÷ TotalBalances
```
### `UserCommissionPayout`
```
- BalancesEarned: تعادل خودش + زیرمجموعه
- DirectBalances: فقط تعادل خودش
- SubordinateBalances: فقط زیرمجموعه
- TotalAmount: BalancesEarned × ValuePerBalance
- Status: Pending
```
### `CommissionPayoutHistory`
```
- PayoutId: شناسه UserCommissionPayout
- Status: Pending (در این مرحله)
- ChangeReason: "محاسبه اولیه کمیسیون هفتگی"
```
---
## ✅ مزایا
1. **ساده‌تر**: 2 مرحله به جای 3
2. **بدون تکرار**: دیگر UserCommissionPayout دوبار ساخته نمیشه
3. **واضح‌تر**: Pool از کجا میاد مشخصه
4. **قابل نگهداری**: منطق مشابه یکجا هست
5. **کامل**: تاریخچه + subordinate balances همه جا هست
---
## 🔄 مراحل بعدی (اختیاری)
### مرحله 3: پرداخت واقعی (جدا از محاسبه)
می‌توان یک Command جدید داشت که:
1. `UserCommissionPayout` با status=Pending رو بخونه
2. به کیف پول واریز کنه
3. Status رو به Paid تغییر بده
4. تاریخچه اضافه کنه
این مرحله **جدا از محاسبات** است و می‌تواند:
- دستی توسط ادمین اجرا شود
- یا به صورت خودکار بعد از تایید
---
## 📝 نکات مهم
### Pool چطور پُر میشه؟
```
1. کاربر عضو Club میشه
2. در ActivateClubMembership مبلغی کسر میشه
3. این مبلغ به Pool اضافه **نمیشه** (فقط شمارش میشه)
4. در محاسبه Pool: تعداد × ActivationFee
```
### چرا subordinate balances؟
```
در سیستم باینری، کاربر از تعادل زیرمجموعه‌های خود
(تا 15 لول پایین‌تر) هم کمیسیون می‌گیرد.
```
### چرا 15 لول؟
```
محدودیت عمق برای جلوگیری از بارگذاری بیش از حد
و تشویق به ایجاد شبکه متعادل
```
---
## 🧪 تست
### تست مرحله 1
```csharp
// 1. ایجاد کاربران در شبکه
// 2. فعالسازی Club برای برخی
// 3. اجرای CalculateWeeklyBalances
// 4. بررسی NetworkWeeklyBalance
// - TotalBalances
// - FlushedPerSide
// - TotalFlushed
```
### تست مرحله 2
```csharp
// 1. اجرای مرحله 1
// 2. اجرای CalculateWeeklyCommissionPool
// 3. بررسی WeeklyCommissionPool
// - TotalPoolAmount = تعداد فعالسازی‌ها × ActivationFee
// - ValuePerBalance صحیح باشد
// 4. بررسی UserCommissionPayout
// - برای هر کاربر ایجاد شده
// - BalancesEarned شامل subordinate هم هست
// - TotalAmount = BalancesEarned × ValuePerBalance
// 5. بررسی CommissionPayoutHistory
// - برای هر پرداخت ثبت شده
```
---
## 📚 فایل‌های تغییر یافته
1.`NetworkWeeklyBalance.cs` - اضافه شدن فیلدها
2.`CalculateWeeklyBalancesCommandHandler.cs` - ذخیره فلش
3.`CalculateWeeklyCommissionPoolCommandHandler.cs` - منطق کامل جدید
4.`TriggerWeeklyCalculationCommandHandler.cs` - حذف مرحله 3
5.`TriggerWeeklyCalculationCommand.cs` - حذف SkipPool flag
6. ✅ Migration: `AddFlushedFieldsToNetworkWeeklyBalance`
---
## 🎉 نتیجه
سیستم کمیسیون هفتگی حالا:
-**ساده‌تر** و قابل فهم‌تر
-**بدون تکرار** در کد
-**Pool از منبع صحیح** (فعالسازی‌های Club)
-**تعادل زیرمجموعه** محاسبه میشه
-**تاریخچه کامل** ثبت میشه
-**فلش دقیق** ذخیره میشه
آماده برای استفاده در Production! 🚀
@@ -0,0 +1,329 @@
# توضیحات جدید بیزینس - 2025-12-08
**تاریخ دریافت**: 2025-12-08
**وضعیت**: نیاز به تطبیق با کد و داکیومنت موجود
**منبع**: توضیحات شفاهی از صاحب پروژه
---
## 1️⃣ فعال‌سازی کاربر و نمایش لینک معرفی
### قوانین فعال‌سازی:
کاربر زمانی می‌تواند **لینک معرفی** خود را ببیند که:
- ✅ وام خود را از **دایا** گرفته باشه
- ✅ یا **پرداخت مستقیم 56 میلیون تومان** انجام داده باشه
### عضویت باشگاه مشتریان (الزامی):
در هر دو حالت بالا:
1. کاربر **اجباراً** باید عضو باشگاه مشتریان بشه
2. دیالوگ باشگاه مشتریان و امضای قرارداد **الزامی** است
3. **تا زمانی که این کار انجام نشه** → لینک معرفی نمایش داده نمی‌شود
### فرآیند:
```
کاربر ثبت نام می‌کنه
پرداخت 56M (دایا یا مستقیم)
دیالوگ باشگاه مشتریان (الزامی) ← امضای قرارداد
لینک معرفی نمایش داده می‌شود
```
---
## 2️⃣ محاسبه تعادل (Balance) شبکه
### قانون اصلی:
**هر نود شبکه = یک تعادل**
```
تعداد تعادل = MIN(دست راست، دست چپ)
```
### حالت عادی (زیر 300 تعادل):
- اگر دست راست = 200 نفر و دست چپ = 150 نفر
- ✅ تعادل = MIN(200, 150) = **150 امتیاز**
- ✅ باقیمانده راست = 200 - 150 = **50** → برای هفته بعد
### حالت بالای 300 تعادل (سقف):
اگر مجموع کاربران جفت دست یک نفر **بیشتر از 600 نفر** باشد:
#### مثال:
```
دست راست = 600 نفر
دست چپ = 400 نفر
```
**مرحله 1: محاسبه تعادل اولیه**
- تعادل = MIN(600, 400) = 400
**مرحله 2: محاسبه باقیمانده اولیه**
- باقیمانده راست = 600 - 400 = 200 → **می‌رود برای هفته بعد**
**مرحله 3: اعمال سقف 300**
- چون تعادل (400) > 300 → فقط **300 امتیاز** حساب می‌شود
- از دست راست: 100 نفر فلش می‌شود
- از دست چپ: 100 نفر فلش می‌شود
- **مجموع 200 نفر فلش می‌شود** (دیگه هیچ جا حساب نمی‌شن)
**نتیجه نهایی:**
- امتیاز این هفته: **300**
- باقیمانده راست برای هفته بعد: **200** (این مجزا از فلش است)
- فلش شده (از بین رفته): **200** (100 چپ + 100 راست)
### نکته مهم:
> باقیمانده‌ای که از هفته قبل می‌آید **فلش نمی‌شود**، فقط اضافه‌ای که بزرگتر از 300 تعادل است فلش می‌شود.
---
## 3️⃣ محاسبه تعادل بازگشتی (Recursive Balance)
### قانون مهم:
**هر نفر تعداد تعادل‌هاش فقط برای خودش حساب می‌شه**
### مثال درخت:
```
کاربر 1
/ \
کاربر 2 کاربر 3
/ \
کاربر 4 کاربر 5
```
### محاسبات:
1. **کاربر 2**:
- جذب کرده: کاربر 4 و کاربر 5
- تعادل کاربر 2 = MIN(1, 1) = **1 تعادل**
2. **کاربر 1**:
- دست راست: کاربر 2 = 1 نفر
- دست چپ: کاربر 3 = 1 نفر
- تعادل کاربر 1 = MIN(1, 1) = **1 تعادل**
### ⚠️ نکته کلیدی:
**کاربر 1 پورسانت کاربر 4 و 5 را نمی‌گیرد!**
چرا؟ چون:
- کاربر 3 کسی را جذب نکرده
- برای اینکه کاربر 1 از تعادل کاربر 4 و 5 بهره‌مند شود
- کاربر 3 حتماً باید **دو نفر** جذب کند
### مثال تصحیح شده:
```
کاربر 1
/ \
کاربر 2 کاربر 3
/ \ / \
کاربر 4 5 کاربر 6 7
```
حالا:
- کاربر 3: تعادل = MIN(1, 1) = 1
- کاربر 2: تعادل = MIN(1, 1) = 1
- **کاربر 1**: تعادل = MIN(2, 2) = **2 تعادل**
---
## 4️⃣ ارزش امتیاز و توزیع کمیسیون
### فرمول:
```
ارزش هر امتیاز = (مجموع مبلغ صندوق) ÷ (تعداد کل تعادل‌ها)
```
### مبلغ صندوق:
هر کاربری که 56 میلیون تومان واریز می‌کند:
- **25 میلیون تومان** وارد صندوق می‌شود
### مثال محاسبه:
```
صندوق هفته = 175 میلیون تومان (7 نفر × 25M)
مجموع تعادل‌های سیستم = 50 امتیاز
ارزش هر امتیاز = 175,000,000 ÷ 50 = 3,500,000 ریال
```
اگر یک کاربر **5 تعادل** داشته باشد:
```
کمیسیون = 5 × 3,500,000 = 17,500,000 ریال
```
---
## 5️⃣ حذف خودکار کاربران غیرفعال (Worker جدید مورد نیاز)
### قانون:
کاربری که تا **2 هفته** بعد از ثبت نام:
- ❌ وام دایا را نگرفته
- ❌ 56 میلیون تومان مستقیم واریز نکرده
**به صورت اتوماتیک حذف می‌شود**
### Worker مورد نیاز:
```csharp
// نام پیشنهادی: DeleteInactiveUsersWorker
// زمان اجرا: روزانه یک بار (مثلاً 3 صبح)
شبهکد:
1. کاربرانی که CreatedAt < (Now - 14 روز)
2. IsActive == false (یعنی نه دایا گرفته، نه پرداخت مستقیم)
3. ClubMembershipId == null
4. حذف کاربر
5. آزاد کردن جایگاه در شبکه برای معرف
```
### هدف:
- معرفی که این کاربر را جذب کرده بود، یکی از دست‌هایش آزاد می‌شود
- می‌تواند **کاربر جدید** جذب کند
- امکان **تعادل متعادل** دست چپ و راست فراهم می‌شود
---
## 6️⃣ محدودیت تعداد زیرمجموعه
### قانون سخت:
**هر کاربر فقط 2 نفر می‌تواند جذب کند** (دست چپ + دست راست)
### سناریو خطا:
```
کاربر A: دو نفر زیرمجموعه فعال دارد
کاربر B: با کد معرف کاربر A ثبت نام می‌کند
→ ❌ پیغام خطا:
"این کاربر تعداد زیرمجموعه‌هاش پر شده و شما نمی‌تونید جزو زیرمجموعه این آدم بشید"
```
### نکته:
**فعال** یعنی:
- وام دایا گرفته یا پرداخت مستقیم کرده
- عضو باشگاه مشتریان شده
---
## 7️⃣ فرآیند کامل ثبت نام تا فعال‌سازی
```
1. ثبت نام با کد معرف
2. بررسی ظرفیت معرف (حداکثر 2 نفر)
↓ (اگر پر بود → خطا)
3. درخواست وام دایا یا پرداخت مستقیم (56M)
4. تأیید پرداخت 56M
5. شارژ کیف پول‌ها:
- کیف پول اصلی: +56M
- کیف پول تخفیفی: +56M
6. **دیالوگ الزامی باشگاه مشتریان**
- امضای قرارداد
- تخصیص 25M به صندوق
7. کاربر فعال می‌شود
8. لینک معرفی نمایش داده می‌شود
9. ورود به فرآیند محاسبه کمیسیون هفتگی
```
---
## 8️⃣ خرید از فروشگاه‌ها
### دو نوع فروشگاه:
1. **فروشگاه اصلی**:
- از کیف پول اصلی کسر می‌شود
2. **فروشگاه تخفیفی** (باشگاه مشتریان):
- از کیف پول تخفیفی کسر می‌شود
- به مقداری که تخفیف دارد
---
## 9️⃣ جمع‌بندی تعادل و فلش
### سناریو کامل:
```
هفته 1:
- چپ = 500، راست = 600
- تعادل = MIN(500, 600) = 500
چون 500 > 300:
- امتیاز این هفته = 300
- فلش چپ = 500 - 300 = 200
- فلش راست = 600 - 300 = 300
- جمع فلش = 500 (از بین رفت)
```
### قوانین فلش:
1. ❌ باقیمانده‌ای که از هفته قبل می‌آید فلش **نمی‌شود**
2. ✅ فقط اضافه‌ای که بزرگتر از 300 است فلش می‌شود
3. ✅ هر دو طرف (چپ و راست) فلش می‌شوند
4.**نمی‌تواند** فقط یک طرف فلش شود
### مثال فلش:
```
هفته قبل باقیمانده راست = 200
هفته جدید راست = 400
مجموع راست = 600
سقف = 300
فلش راست = 600 - 300 = 300 ✅ (نه 200)
```
---
## 🔟 نکات مهم اضافی
### چرخش هفتگی:
- محاسبات هر هفته صورت می‌گیرد
- تعادل‌های استفاده شده **ریست** می‌شوند
- فقط **باقیمانده** به هفته بعد منتقل می‌شود
- فلش‌ها **هیچ جا حساب نمی‌شوند**
### محدودیت‌های عمق شبکه:
- **تا همه کاربرها** در زیر شبکه حساب می‌شوند
- **بدون محدودیت عمق** (تا سطح آخر درخت)
### اولویت محاسبه:
1. محاسبه تعادل اولیه
2. محاسبه باقیمانده
3. اعمال سقف 300
4. محاسبه فلش
5. ذخیره باقیمانده برای هفته بعد
---
## 📊 جدول مقایسه حالات مختلف
| چپ | راست | تعادل اولیه | سقف 300 | امتیاز | باقی چپ | باقی راست | فلش کل |
|-----|-------|-------------|---------|--------|---------|-----------|---------|
| 200 | 250 | 200 | 200 | 200 | 0 | 50 | 0 |
| 400 | 350 | 350 | 300 | 300 | 100 | 50 | 100 |
| 500 | 600 | 500 | 300 | 300 | 200 | 300 | 400 |
| 150 | 280 | 150 | 150 | 150 | 0 | 130 | 0 |
| 350 | 350 | 350 | 300 | 300 | 50 | 50 | 100 |
**توضیح ستون‌ها:**
- **تعادل اولیه**: MIN(چپ، راست)
- **سقف 300**: MIN(تعادل اولیه، 300)
- **امتیاز**: همان سقف 300 (امتیاز نهایی)
- **باقی چپ**: چپ - سقف چپ (300)
- **باقی راست**: راست - سقف راست (300)
- **فلش کل**: (چپ - 300) + (راست - 300) اگر > 0
---
## ✅ وضعیت پیاده‌سازی فعلی
این سند نیاز به **تطبیق کامل** با:
1. ✅ کد موجود در `CalculateWeeklyBalancesCommandHandler`
2. ✅ داکیومنت‌های موجود در `totalDoc/01-BUSINESS/`
3. ✅ Entity ها در Domain Layer
4. ✅ Worker های پس‌زمینه
→ در مرحله بعد مقایسه و شناسایی تفاوت‌ها انجام می‌شود.