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:
@@ -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 |
|
||||
|
||||
---
|
||||
|
||||
**پایان مثالهای عملی**
|
||||
|
||||
این مستند تمام حالات ممکن محاسبه تعادل را با مثالهای عددی واقعی نشان میدهد.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 های پسزمینه
|
||||
|
||||
→ در مرحله بعد مقایسه و شناسایی تفاوتها انجام میشود.
|
||||
Reference in New Issue
Block a user