39590d2cbe
Updated 8 docs: - CHANGELOG: Phase 10a-d (DataMigration tool, EF staging migrations, PackagePurchaseDialog, 4 UI fixes) - PAYMENT-FINANCE: Rial→Toman conversion chain documented, PackagePurchaseDialog status - TECH-02: Added PackagePurchaseDialog to folder structure + status table - TECH-04: DataMigration tool features (smart retry, FK handling, fallback tables), EF staging - PACKAGE-TASKS: Phase 10 graph, NuGet v0.0.189, T4.1+T4.2 marked ✅ - BIZ-PACKAGE: v6→v7, commits updated, T4.1+T4.2 marked ✅ - INDEX: Updated last-update + R3 description - ROADMAP: Progress bars updated, DONE section + NOW section refreshed
1785 lines
95 KiB
Markdown
1785 lines
95 KiB
Markdown
# 📦 سیستم مبتنی بر پکیج (Package-Based System)
|
||
|
||
> **وضعیت:** پیادهسازی کد تکمیل — **فاز ۰-۱۰ ✅** | تست نهایی باقیمانده
|
||
> **تاریخ بروزرسانی:** ۸ اسفند ۱۴۰۴
|
||
> **نسخه:** v7 (DataMigration + EF Staging + PackagePurchaseDialog + UI Fixes)
|
||
> **تاثیرگذاری:** زیاد — **۱۰۰+** تغییر در ۶ لایه (۵۱ اصلی + ۴۴ سایدافکت + ۵ استقرار)
|
||
> **کامیتها:** `8b9c317` (Phase 0) → `ae92ab8` (Phase 1) → `a9cd2fd` (Phase 1.5) → `8e5c7c5` (Phase 2) → `ccb938e` (Phase 3) → `0002a5a` (Phase 4) → `a1024a3` (Q24+Q26) → `fdbb91d` (Q27) → `10d2ca2` (Rename+Interceptor+Migration) → `a3681a8` (PackagePurchaseDialog) → `3c1a8ff` (UI Fixes)
|
||
|
||
---
|
||
|
||
## ۱. خلاصه فیچر
|
||
|
||
**وضعیت فعلی:** سیستم فقط یک پکیج پایه (۵۶ میلیون تومان) دارد و همه چیز حول آن hardcode شده.
|
||
|
||
**وضعیت هدف:** سیستم چندین پکیج با قیمتها و ویژگیهای متفاوت پشتیبانی میکند. هر پکیج مقادیر مالی، فیچرها و Commission Pool مستقل خود را دارد. کاربر میتواند **N بار** پکیج بخرد (بعد از تکمیل چرخه Magic Wallet).
|
||
|
||
---
|
||
|
||
## ۲. تصمیمات تاییدشده
|
||
|
||
| # | سوال | تصمیم |
|
||
|---|------|-------|
|
||
| Q1 | باگ DiscountBalance در VerifyGoldenPackagePurchase | ✅ **باگه — باید فیکس بشه** |
|
||
| Q2 | سه مسیر پرداخت موازی | ✅ **ادغام به سرویس Generic** — نه به نام پکیج خاص |
|
||
| Q3 | پکیجهای اولیه | ✅ **نقرهای (۵.۶M) + پایه (۵۶M)** — سیستم داینامیک |
|
||
| Q4 | ActivationFee و GiftValue | ✅ **یک فیلد (ActivationFee)** — GiftValue حذف (تکراری بود) |
|
||
| Q5 | DiscountMultiplier | ✅ **×2 برای همه** — ولی داینامیک در entity |
|
||
| Q6 | Migration کاربران فعلی | ✅ **Pipeline/Script** — کاربران فعلی → PackageId = پکیج پایه |
|
||
| Q7 | خرید چند پکیج | ✅ **N بار** — بعد تکمیل Magic Wallet و صفر شدن Balance |
|
||
| Q8 | Commission Pool | ✅ **جدا برای هر پکیج** — تمام جداول Commission با PackageId |
|
||
| Q9 | MagicWallet Multiplier | ✅ **داینامیک** بهازای هر پکیج (فعلاً همه ×2.5) |
|
||
| Q10 | کاربران دایا | ✅ **پکیج پایه** گرفتن — "طلایی" اشتباه نامگذاری بوده |
|
||
| Q11 | فیچرها | ✅ **داینامیک** — ادمین مدیریت میکند |
|
||
| Q12 | MaxWeeklyBalancesPerLeg (300) | ✅ **per-package** — پکیج نقرهای ۳۰، پکیج پایه ۳۰۰ |
|
||
| Q13 | MaxNetworkLevel (15) | ✅ **per-package** — ادمین تنظیم کند |
|
||
| Q14 | MagicWalletMaxDeposit (1B) | ✅ **per-package** — سقف شارژ بر اساس پکیج |
|
||
| Q15 | MagicWalletMaxCredit (2.5B) | ✅ **per-package** — سقف اعتبار بر اساس پکیج |
|
||
| Q16 | NetworkWeeklyBalance + PackageId | ✅ **هر رکورد تعادل = per-package** — carryover هم جداگانه |
|
||
| Q17 | گزارش پورسانت FO | ✅ **breakdown per-package** — مشتری ببیند از هر پکیج چقدر |
|
||
| Q18 | گزارش پورسانت BO | ✅ **فیلتر بر اساس پکیج** — ادمین بر اساس پکیج فیلتر کند |
|
||
| Q19 | قرارداد باشگاه — چند بار؟ | ✅ **فقط یک بار در طول عمر** — قرارداد فقط اولین خرید امضا میشود. خرید مجدد بدون قرارداد |
|
||
| Q20 | فیچرها در خرید مجدد — چطور؟ | ✅ **DIFF/تفاضل** — مقایسه فیچرهای فعلی با پکیج جدید. فقط اختلاف اعمال میشود |
|
||
| Q21 | ردیابی فعالسازی ClubMembership | ✅ **FirstActivation + LastActivation** — ۴ فیلد: `FirstActivationDate` + `FirstPackageId` + `LastActivationDate` + `LastPackageId` |
|
||
| Q22 | تشخیص فعالشدگان هفته | ✅ **از `LastActivationDate`** — هر کسی که `LastActivationDate` در بازه هفته باشد |
|
||
| Q23 | Carryover تعادل هفتگی | ✅ **per-downline-package** — تعادل بر اساس پکیج **زیرمجموعهها** (نه پکیج خود کاربر). هر کاربر N رکورد تعادل دارد. تغییر پکیج خود کاربر تاثیری بر carryover ندارد |
|
||
| Q24 | آستانه موجودی برای ورود Magic و خرید مجدد | ✅ **کمتر از ۱۰۰,۰۰۰ تومان** — چون قیمت محصولات متفاوته، `Balance == 0` عملاً غیرممکنه. آستانه ثابت ۱,۰۰۰,۰۰۰ ریال |
|
||
| Q25 | DayaLoans محدودیت پکیج | ✅ **فقط پکیج پایه** — `SupportsDayaPurchase` فقط روی پکیج پایه `true` هست. تغییر نمیکنه |
|
||
| Q26 | مدیریت Stored Procedures | ✅ **SP Worker (IHostedService)** — در startup، فایلهای `.sql` از embedded resource خوانده و با checksum مقایسه و اعمال میشوند |
|
||
| Q27 | History Tables یکسانسازی | ✅ **نامگذاری مشابه master** + ثبت خودکار تغییرات در EF interceptor/domain events |
|
||
| Q28 | UI Guidance (آموزش/هشدار) | ✅ **مودال + متن inline** — در سراسر FO/BO توضیحات آموزشی و هشداری برای سیستم پکیجبیس |
|
||
| Q29 | شرط EXIT Magic | ✅ **آخرین پکیج فعال** — از `ClubMembershipCycle.PackageId` (چرخه فعلی) → `Package.MagicWalletMaxDeposit` |
|
||
| Q30 | Carryover توضیح | ✅ **باقیمانده تعادل هفتگی** — بر اساس پکیج **زیرمجموعهها**. تغییر پکیج خود کاربر carryover را ریست **نمیکند**. هر Pool مجزا |
|
||
|
||
---
|
||
|
||
## ۳. تحلیل عمیق وضعیت فعلی (AS-IS)
|
||
|
||
### ۳.۱ باگهای کشفشده
|
||
|
||
| # | باگ | شدت | فایل |
|
||
|---|-----|------|------|
|
||
| **B1** | `VerifyGoldenPackagePurchase` → **DiscountBalance شارژ نمیشود** | 🔴 بحرانی | VerifyGoldenPackagePurchaseCommandHandler.cs |
|
||
| **B2** | `VerifyGoldenPackagePurchase` → **UserPackagePurchase ساخته نمیشود** | 🔴 بحرانی | VerifyGoldenPackagePurchaseCommandHandler.cs |
|
||
| **B3** | `VerifyPackagePurchase` → **UserPackagePurchase ساخته نمیشود** | 🔴 بحرانی | VerifyPackagePurchaseCommandHandler.cs |
|
||
| **B4** | `VerifyBasePackagePayment` → **UserPackagePurchase ساخته نمیشود** | 🔴 بحرانی | VerifyBasePackagePaymentCommandHandler.cs |
|
||
| **B5** | `PackageService.CustomerPurchasePackage` → **guard برای خرید تکراری ندارد** | 🟡 متوسط | PackageService.cs |
|
||
| **B6** | EXIT Magic Mode → **PackagePurchaseMethod ریست نمیشود** (خرید مجدد مسدود) | 🔴 بحرانی | UserOrderService.cs |
|
||
|
||
### ۳.۲ ناسازگاری مسیرهای پرداخت
|
||
|
||
| مسیر | Balance | Discount | UserPackagePurchase | WalletChangeLog |
|
||
|------|---------|----------|---------------------|-----------------|
|
||
| **BFF/PYMS** (InitiateBase→VerifyBase) | ✅ | ✅ | ❌ | ✅ |
|
||
| **ZarinPal Golden** (PurchaseGolden→VerifyGolden) | ✅ | ❌ | ❌ | ✅ (فقط Balance) |
|
||
| **ZarinPal Generic** (Purchase→VerifyPurchase) | ✅ | ✅ | ❌ | ✅ |
|
||
| **Daya Loan** (CheckAndProcess) | ✅ | ✅ | ✅ | ✅ |
|
||
| **Manual** (CreateManualPayment) | ✅ | ✅ | ❌ | ✅ |
|
||
|
||
> **فقط Daya Loan** همه مراحل را کامل انجام میدهد. بقیه مسیرها ناقص هستند.
|
||
|
||
### ۳.۳ مقادیر Hardcoded (۱۵ مورد)
|
||
|
||
| # | مکان | مقدار | باید بشه |
|
||
|---|------|-------|----------|
|
||
| H1 | `InitiateBasePackagePaymentCommandHandler` | `BasePackageId = 4` | خوانش از پکیج فعال |
|
||
| H2 | `CheckAndProcessDayaLoansCommandHandler` | `p.Id == 4` | خوانش از پکیج پایه |
|
||
| H3 | `SystemConstants.BasePackageAmount` | `56_000_000` | `Package.Price` |
|
||
| H4 | `SystemConstants.DayaLoanAmount` | `56_000_000` | `Package.Price` |
|
||
| H5 | `SystemConstants.ClubActivationFee` | `25_200_000` | `Package.ActivationFee` |
|
||
| H6 | `SystemConstants.ClubMembershipGiftValue` | `25_200_000` | حذف (= ActivationFee) |
|
||
| H7 | `VerifyPackagePurchaseCommandHandler` | `Amount * 2` (×3 جا) | `Package.DiscountMultiplier` |
|
||
| H8 | `VerifyBasePackagePaymentCommandHandler` | `BasePackageAmount * 2` (×2) | `Package.DiscountMultiplier` |
|
||
| H9 | `CreateManualPaymentCommandHandler` | `BasePackageAmount * 2` | `Package.DiscountMultiplier` |
|
||
| H10 | `CheckAndProcessDayaLoansCommandHandler` | `DayaLoanAmount * 2` | `Package.DiscountMultiplier` |
|
||
| H11 | `ActivateClubMembershipCommandHandler` | `PackageAmount = BasePackageAmount` | `Package.Price` |
|
||
| H12 | `PurchaseGoldenPackageCommandHandler` | `Title.Contains("طلایی")` | حذف — Generic |
|
||
| H13 | `ActivateClubMembershipCommandHandler` | `GetAllFeatureIds()` | `Package.PackageFeatures` |
|
||
| H14 | `ActivationSection.razor` | `56_000_000 × months` | از پکیج خوانده شود |
|
||
| H15 | `ClubMembershipContractDialog.razor` | متن قرارداد ۵۶M | داینامیک از پکیج |
|
||
|
||
### ۳.۴ Guardهای مسدودکننده خرید مجدد
|
||
|
||
| # | فایل | Guard | وضعیت | تغییر |
|
||
|---|------|-------|-------|-------|
|
||
| G1 | `PurchaseGoldenPackageCommandHandler` | `PackagePurchaseMethod != None` → throw | مسدود | ✅ اجازه بعد تکمیل چرخه |
|
||
| G2 | `InitiateBasePackagePaymentCommandHandler` | `PackagePurchaseMethod != None` → fail | مسدود | ✅ اجازه بعد تکمیل چرخه |
|
||
| G3 | `PurchasePackageCommandHandler` | `PackagePurchaseMethod != None` → throw | مسدود | ✅ اجازه بعد تکمیل چرخه |
|
||
| G4 | `CheckAndProcessDayaLoansCommandHandler` | `hasPreviousCycle` → skip | عمدی ✅ | ❌ حفظ (دایا فقط دور اول) |
|
||
| G5 | `AcceptClubMembershipContractCommandHandler` | `IsActive == true` → fail | مسدود | ✅ حفظ — قرارداد فقط یک بار (Q19). خرید مجدد → Skip قرارداد |
|
||
| G6 | `ActivateClubMembershipCommandHandler` | `IsActive == true` → return true | short-circuit | ✅ باید چرخه جدید بسازه + فیچر DIFF (Q20) |
|
||
| G7 | JWT Claim `HasPurchasedPackage` | permanent true | UI مسدود | ✅ اضافه `CanRepurchase` |
|
||
|
||
### ۳.۵ Root Cause — خرید مجدد کار نمیکند
|
||
|
||
```
|
||
EXIT Magic Mode (UserOrderService.cs):
|
||
✅ wallet.WalletMode = Normal
|
||
✅ wallet.MagicCompletedAt = now
|
||
✅ cycle.MagicCompletedAt = now
|
||
|
||
❌ MISSING: user.PackagePurchaseMethod = None ← Guards G1-G3 مسدود میمانند
|
||
❌ MISSING: cycle.IsCurrentCycle = false ← آماده چرخه جدید
|
||
❌ MISSING: JWT CanRepurchase = true ← UI دکمه خرید نشان نمیدهد
|
||
|
||
ℹ️ membership.IsActive حفظ میشود (قرارداد یکبار — Q19)
|
||
ℹ️ فیچرها حفظ میشوند — در خرید مجدد DIFF اعمال میشود (Q20)
|
||
```
|
||
|
||
**راهحل:** در EXIT Magic Mode، وضعیت کاربر ریست شود تا بتواند پکیج جدید بخرد.
|
||
|
||
---
|
||
|
||
## ۴. طراحی نهایی (TO-BE)
|
||
|
||
### ۴.۱ تغییرات Entity — Package
|
||
|
||
```csharp
|
||
public class Package : BaseAuditableEntity
|
||
{
|
||
// === فیلدهای فعلی (حفظ) ===
|
||
public string Title { get; set; }
|
||
public string Description { get; set; }
|
||
public string ImagePath { get; set; }
|
||
public long Price { get; set; } // قیمت پکیج (ریال)
|
||
|
||
// === فیلدهای جدید ===
|
||
public int SortOrder { get; set; } // ترتیب نمایش
|
||
public bool IsActive { get; set; } = true; // فعال/غیرفعال
|
||
public bool IsBasePackage { get; set; } // پکیج پایه؟ (فقط یکی true)
|
||
public bool SupportsDayaPurchase { get; set; } // پشتیبانی از خرید دایا
|
||
public bool SupportsDirectPurchase { get; set; } = true; // پشتیبانی از پرداخت مستقیم
|
||
|
||
// === محاسبات مالی ===
|
||
public long ActivationFee { get; set; } // سهم Commission Pool
|
||
public decimal DiscountMultiplier { get; set; } = 2.0m; // ضریب شارژ DiscountBalance
|
||
public decimal MagicWalletMultiplier { get; set; } = 2.5m; // ضریب کیفپول جادویی
|
||
|
||
// === تنظیمات پورسانت (v3 — انتقال از SystemConstants) ===
|
||
public int MaxBalancesPerLeg { get; set; } = 300; // سقف تعادل هر پا (نقرهای=۳۰)
|
||
public int MaxNetworkLevel { get; set; } = 15; // عمق شبکه برای محاسبه
|
||
|
||
// === تنظیمات کیفپول جادویی (v3) ===
|
||
public long MagicWalletMaxDeposit { get; set; } = 1_000_000_000; // سقف شارژ
|
||
public long MagicWalletMaxCredit { get; set; } = 2_500_000_000; // سقف اعتبار
|
||
|
||
// === Navigation ===
|
||
public virtual ICollection<PackageFeature> PackageFeatures { get; set; }
|
||
public virtual ICollection<UserPackagePurchase> Purchases { get; set; }
|
||
public virtual ICollection<UserOrder> UserOrders { get; set; }
|
||
}
|
||
```
|
||
|
||
**نسبت به v1:**
|
||
- ❌ `GiftValue` حذف (= ActivationFee — تکراری)
|
||
- ✅ `MagicWalletMultiplier` اضافه (داینامیک)
|
||
- ❌ `PackageType` enum نیاز نیست (`IsBasePackage` کافیست)
|
||
|
||
### ۴.۲ Entity جدید — PackageFeature
|
||
|
||
```csharp
|
||
public class PackageFeature : BaseAuditableEntity
|
||
{
|
||
public long PackageId { get; set; }
|
||
public virtual Package Package { get; set; }
|
||
|
||
public long ClubFeatureId { get; set; }
|
||
public virtual ClubFeature ClubFeature { get; set; }
|
||
|
||
public bool IsIncluded { get; set; } = true;
|
||
}
|
||
```
|
||
|
||
### ۴.۳ تغییرات Entity — سایر
|
||
|
||
| Entity | فیلد جدید | توضیح |
|
||
|--------|-----------|-------|
|
||
| `ClubMembership` | `DateTime FirstActivationDate` | اولین فعالسازی — فقط یک بار ست میشود (v5 — Q21) |
|
||
| `ClubMembership` | `long FirstPackageId` + FK | اولین پکیج خریداریشده (v5 — Q21) |
|
||
| `ClubMembership` | `DateTime LastActivationDate` | آخرین/جاری فعالسازی — هر خرید بروزرسانی میشود (v5 — Q21) |
|
||
| `ClubMembership` | `long LastPackageId` + FK | آخرین/جاری پکیج (v5 — Q21) |
|
||
| `ClubMembership` | حذف `ActivatedAt` | ← جایگزین با First/LastActivationDate |
|
||
| `ClubMembershipCycle` | `long PackageId` + FK | پکیج این چرخه |
|
||
| `WeeklyCommissionPool` | `long PackageId` + FK | Pool جداگانه هر پکیج |
|
||
| `UserCommissionPayout` | `long PackageId` + FK | از کدام Pool |
|
||
| **`NetworkWeeklyBalance`** | **`long PackageId` + FK** ← v3 | **هر رکورد تعادل = per-package** |
|
||
|
||
> **تغییر v5:** `ClubMembership.ActivatedAt` حذف شد و به دو فیلد `FirstActivationDate` و `LastActivationDate` تبدیل شد.
|
||
> `FirstActivationDate` فقط در اولین فعالسازی ست میشود و هیچوقت تغییر نمیکند.
|
||
> `LastActivationDate` در هر خرید مجدد بروزرسانی میشود و مبنای تشخیص "فعالشدگان این هفته" است (Q22).
|
||
|
||
**ClubMembership entity (v5):**
|
||
|
||
```csharp
|
||
public class ClubMembership : BaseAuditableEntity
|
||
{
|
||
public long UserId { get; set; }
|
||
public virtual User User { get; set; }
|
||
|
||
public bool IsActive { get; set; }
|
||
|
||
// === v5: First/Last Activation Tracking (Q21) ===
|
||
/// <summary>
|
||
/// اولین فعالسازی — فقط یک بار ست میشود، هیچوقت overwrite نمیشود
|
||
/// </summary>
|
||
public DateTime FirstActivationDate { get; set; }
|
||
|
||
/// <summary>
|
||
/// اولین پکیج خریداریشده — فقط یک بار ست میشود
|
||
/// </summary>
|
||
public long FirstPackageId { get; set; }
|
||
public virtual Package FirstPackage { get; set; }
|
||
|
||
/// <summary>
|
||
/// آخرین/جاری فعالسازی — هر خرید مجدد بروزرسانی میشود
|
||
/// مبنای تشخیص "فعالشدگان این هفته" (Q22)
|
||
/// </summary>
|
||
public DateTime LastActivationDate { get; set; }
|
||
|
||
/// <summary>
|
||
/// آخرین/جاری پکیج — هر خرید مجدد بروزرسانی میشود
|
||
/// مبنای carryover per-package (Q23)
|
||
/// </summary>
|
||
public long LastPackageId { get; set; }
|
||
public virtual Package LastPackage { get; set; }
|
||
|
||
// === فیلدهای فعلی (حفظ) ===
|
||
public long InitialContribution { get; set; }
|
||
public long GiftValue { get; set; }
|
||
public long TotalEarned { get; set; }
|
||
public PackagePurchaseMethod PurchaseMethod { get; set; }
|
||
|
||
// === Navigation ===
|
||
public virtual ICollection<UserClubFeature>? UserClubFeatures { get; set; }
|
||
public virtual ICollection<ClubMembershipHistory>? ClubMembershipHistories { get; set; }
|
||
public virtual ICollection<ClubMembershipCycle>? Cycles { get; set; }
|
||
}
|
||
```
|
||
|
||
**Constraintهای جدید:**
|
||
- `WeeklyCommissionPool` → Unique(`WeekDefinitionId`, `PackageId`)
|
||
- `NetworkWeeklyBalance` → Unique(`UserId`, `WeekDefinitionId`, `PackageId`) ← v3
|
||
- `UserCommissionPayout` → Unique(`UserId`, `WeekDefinitionId`, `PackageId`) ← v3
|
||
|
||
> ⚠️ **تاثیر حجم داده:** رکوردهای `NetworkWeeklyBalance` ضربدر تعداد پکیجهای فعال میشوند. مثلاً ۱۰۰۰ کاربر × ۲ پکیج = ۲۰۰۰ رکورد تعادل هفتگی (بجای ۱۰۰۰).
|
||
|
||
### ۴.۴ حذف/تغییر SystemConstants
|
||
|
||
| ثابت | تغییر | جایگزین |
|
||
|------|-------|---------|
|
||
| `ClubMembershipGiftValue` | ❌ حذف | تکراری بود |
|
||
| `ClubActivationFee` | ❌ حذف | `Package.ActivationFee` |
|
||
| `BasePackageAmount` | ❌ حذف | `Package.Price` |
|
||
| `DayaLoanAmount` | ❌ حذف | `Package.Price` (base) |
|
||
| `MagicWalletMultiplier` | ❌ حذف | `Package.MagicWalletMultiplier` |
|
||
| `CommissionMaxWeeklyBalancesPerLeg` | ❌ **حذف** ← تغییر از v2 | `Package.MaxBalancesPerLeg` (نقرهای=۳۰، پایه=۳۰۰) |
|
||
| `CommissionMaxNetworkLevel` | ❌ **حذف** ← تغییر از v2 | `Package.MaxNetworkLevel` (قابل تنظیم ادمین) |
|
||
| `MagicWalletMaxDeposit` | ❌ **حذف** ← جدید v3 | `Package.MagicWalletMaxDeposit` (سقف شارژ) |
|
||
| `MagicWalletMaxCredit` | ❌ **حذف** ← جدید v3 | `Package.MagicWalletMaxCredit` (سقف اعتبار) |
|
||
| `ShopVAT` | ✅ حفظ | عمومی |
|
||
| `CommissionCalculationStrategy` | ✅ حفظ | عمومی (ORM or SP) |
|
||
| `AllowDeletingParentsWithChildren` | ✅ حفظ | عمومی |
|
||
| `MaxDirectChildrenPerLeg` | ✅ حفظ | عمومی |
|
||
| `IsCommissionWithdrawalEnabled` | ✅ حفظ | عمومی |
|
||
| `CommissionMinWithdrawalAmount` | ✅ حفظ | عمومی |
|
||
| `IsMaintenanceMode` | ✅ حفظ | عمومی |
|
||
| `IsAuditLogEnabled` | ✅ حفظ | عمومی |
|
||
| `IsVATEnabled` | ✅ حفظ | عمومی |
|
||
|
||
### ۴.۵ فرمول مالی
|
||
|
||
```
|
||
ActivationFee = Price × 0.45
|
||
|
||
پکیج نقرهای (۵,۶۰۰,۰۰۰ ریال):
|
||
├── Balance += ۵,۶۰۰,۰۰۰ (Price)
|
||
├── DiscountBalance += ۱۱,۲۰۰,۰۰۰ (Price × DiscountMultiplier)
|
||
└── CommissionPool += ۲,۵۲۰,۰۰۰ (ActivationFee)
|
||
|
||
پکیج پایه (۵۶,۰۰۰,۰۰۰ ریال):
|
||
├── Balance += ۵۶,۰۰۰,۰۰۰ (Price)
|
||
├── DiscountBalance += ۱۱۲,۰۰۰,۰۰۰ (Price × DiscountMultiplier)
|
||
└── CommissionPool += ۲۵,۲۰۰,۰۰۰ (ActivationFee)
|
||
```
|
||
|
||
---
|
||
|
||
## ۵. فلوی خرید مجدد (Re-Purchase)
|
||
|
||
### ۵.۱ چرخه حیات کامل
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> NoPurchase: کاربر ثبتنام کرده
|
||
|
||
NoPurchase --> PackagePurchased: خرید پکیج\n(هر پکیجی)
|
||
|
||
PackagePurchased --> ClubActivated: فعالسازی باشگاه\n(OTP + قرارداد — فقط بار اول)
|
||
|
||
ClubActivated --> Shopping: خرج Balance\nدر فروشگاه
|
||
|
||
Shopping --> MagicMode: Balance == 0
|
||
|
||
MagicMode --> MagicCharging: شارژ + خرج\n(تا سقف per-package)
|
||
|
||
MagicCharging --> MagicMode: ادامه
|
||
|
||
MagicMode --> CycleComplete: Balance == 0\nAND Deposit ≥ MaxDeposit
|
||
|
||
CycleComplete --> RePurchase: ریست PackagePurchaseMethod\n(membership فعال باقی — Q19)
|
||
|
||
RePurchase --> ClubActivated: خرید مجدد\n(بدون قرارداد + فیچر DIFF)
|
||
```
|
||
|
||
### ۵.۲ ریست وضعیت بعد تکمیل چرخه (EXIT Magic Mode)
|
||
|
||
```csharp
|
||
// UserOrderService.cs — EXIT Magic Mode — تغییرات لازم:
|
||
wallet.WalletMode = WalletMode.Normal;
|
||
wallet.MagicCompletedAt = DateTime.UtcNow;
|
||
cycle.MagicCompletedAt = DateTime.UtcNow;
|
||
|
||
// ✅ اضافه شود:
|
||
user.PackagePurchaseMethod = PackagePurchaseMethod.None; // اجازه خرید مجدد (G1-G3)
|
||
cycle.IsCurrentCycle = false; // آماده چرخه جدید
|
||
|
||
// ❌ membership.IsActive حفظ میشود! (قرارداد یکبار — Q19)
|
||
// ❌ فیچرها حفظ میشوند! (DIFF در خرید بعدی اعمال میشود — Q20)
|
||
```
|
||
|
||
> **تفاوت v5 با v4:** `membership.IsActive = false` حذف شد. قرارداد فقط یک بار امضا میشود و عضویت فعال باقی میماند. در خرید مجدد، فقط فیچر DIFF + شارژ wallet + Commission انجام میشود.
|
||
|
||
### ۵.۳ نکات مهم
|
||
|
||
1. **دایا فقط دور اول** — بعد از دور اول، فقط IPG مجاز
|
||
2. **هر خرید = Commission contribution** — ActivationFee به Pool آن پکیج
|
||
3. **PackagePurchaseMethod ریست** بعد تکمیل چرخه
|
||
4. **فیچرها بر اساس پکیج جدید — DIFF/تفاضل اعمال میشود (Q20)**
|
||
|
||
### ۵.۴ قرارداد باشگاه — فقط یک بار (v5 — Q19)
|
||
|
||
> ⚠️ **تغییر بنیادی v5:** قرارداد باشگاه مشتریان **فقط یک بار** در طول عمر کاربر امضا میشود.
|
||
|
||
```
|
||
سناریو:
|
||
خرید اول (پکیج پایه):
|
||
✅ OTP + امضای قرارداد + فعالسازی باشگاه
|
||
✅ فیچرهای پکیج پایه فعال
|
||
✅ قرارداد ثبت شد → UserContract record
|
||
|
||
خرید دوم (پکیج پایه مجدد):
|
||
✅ پرداخت + شارژ wallet + Commission
|
||
❌ قرارداد مجدد نمیشود (Q19)
|
||
✅ فیچرها بدون تغییر (همان پکیج)
|
||
|
||
خرید سوم (پکیج نقرهای):
|
||
✅ پرداخت + شارژ wallet + Commission
|
||
❌ قرارداد مجدد نمیشود (Q19)
|
||
✅ فیچر DIFF: حذف فیچرهای پایهای که نقرهای ندارد + اضافه فیچرهای جدید نقرهای
|
||
```
|
||
|
||
**فلوی خرید مجدد (بدون قرارداد):**
|
||
|
||
```mermaid
|
||
flowchart TD
|
||
A["EXIT Magic Mode\n(چرخه قبلی تکمیل شد)"] --> B["ریست PackagePurchaseMethod=None\n+ JWT: CanRepurchase=true"]
|
||
B --> C["کاربر پکیج جدید انتخاب میکند"]
|
||
C --> D["پرداخت\n(IPG / دایا دور اول)"]
|
||
D --> E["Verify Payment"]
|
||
E --> F["شارژ Balance + DiscountBalance"]
|
||
F --> G{"قرارداد دارد؟\n(UserContract exists?)"}
|
||
G -->|بله — Skip| H["فیچر DIFF (Q20)"]
|
||
G -->|خیر — اولین خرید| I["OTP + امضای قرارداد"]
|
||
I --> H
|
||
H --> J["بروزرسانی ClubMembership\nLastActivationDate + LastPackageId"]
|
||
J --> K["ساخت ClubMembershipCycle جدید"]
|
||
K --> L["ActivationFee → Pool پکیج"]
|
||
```
|
||
|
||
### ۵.۵ الگوریتم DIFF فیچرها (v5 — Q20)
|
||
|
||
> ⚠️ **فیچرها بر اساس تفاضل (DIFF) اعمال میشوند — نه تخصیص کامل مجدد.**
|
||
|
||
```
|
||
ورودی:
|
||
currentFeatures = UserClubFeatures WHERE UserId = X AND IsActive = true
|
||
newFeatures = PackageFeatures WHERE PackageId = newPackage.Id AND IsIncluded = true
|
||
|
||
الگوریتم:
|
||
toAdd = newFeatures - currentFeatures (فیچرهای جدید که قبلاً نداشت)
|
||
toRemove = currentFeatures - newFeatures (فیچرهای قبلی که پکیج جدید ندارد)
|
||
toKeep = currentFeatures ∩ newFeatures (مشترک — بدون تغییر)
|
||
|
||
اقدامات:
|
||
foreach feature in toAdd:
|
||
INSERT UserClubFeature (UserId, ClubFeatureId, IsActive=true)
|
||
Notes = "اضافه شده بابت خرید پکیج [نام پکیج]"
|
||
|
||
foreach feature in toRemove:
|
||
UPDATE UserClubFeature SET IsActive = false
|
||
Notes = "حذف شده بابت تغییر از پکیج [قبلی] به [جدید]"
|
||
|
||
foreach feature in toKeep:
|
||
بدون تغییر — فیچر فعال باقی میماند
|
||
```
|
||
|
||
**مثال عملی:**
|
||
|
||
```
|
||
پکیج پایه فیچرها: [چاتیکا, بیمه, سفر, آموزش]
|
||
پکیج نقرهای فیچرها: [چاتیکا, بیمه]
|
||
|
||
کاربر "علی" (فعلاً پکیج پایه) → خرید پکیج نقرهای:
|
||
currentFeatures = [چاتیکا, بیمه, سفر, آموزش]
|
||
newFeatures = [چاتیکا, بیمه]
|
||
|
||
toKeep = [چاتیکا, بیمه] → بدون تغییر
|
||
toRemove = [سفر, آموزش] → IsActive = false
|
||
toAdd = [] → هیچ فیچر جدیدی نیست
|
||
|
||
کاربر "سارا" (فعلاً پکیج نقرهای) → خرید پکیج پایه:
|
||
currentFeatures = [چاتیکا, بیمه]
|
||
newFeatures = [چاتیکا, بیمه, سفر, آموزش]
|
||
|
||
toKeep = [چاتیکا, بیمه] → بدون تغییر
|
||
toRemove = [] → هیچی حذف نمیشه
|
||
toAdd = [سفر, آموزش] → UserClubFeature جدید ساخته میشه
|
||
```
|
||
|
||
**کد پیشنهادی:**
|
||
|
||
```csharp
|
||
// FeatureDiffService.cs — لاجیک DIFF فیچرها
|
||
public async Task ApplyFeatureDiffAsync(
|
||
long userId, long clubMembershipId,
|
||
long oldPackageId, long newPackageId,
|
||
CancellationToken ct)
|
||
{
|
||
// 1. فیچرهای فعال فعلی کاربر
|
||
var currentFeatureIds = await _context.UserClubFeatures
|
||
.Where(ucf => ucf.UserId == userId && ucf.IsActive)
|
||
.Select(ucf => ucf.ClubFeatureId)
|
||
.ToListAsync(ct);
|
||
|
||
// 2. فیچرهای پکیج جدید
|
||
var newFeatureIds = await _context.PackageFeatures
|
||
.Where(pf => pf.PackageId == newPackageId && pf.IsIncluded)
|
||
.Select(pf => pf.ClubFeatureId)
|
||
.ToListAsync(ct);
|
||
|
||
// 3. DIFF
|
||
var toAdd = newFeatureIds.Except(currentFeatureIds).ToList();
|
||
var toRemove = currentFeatureIds.Except(newFeatureIds).ToList();
|
||
|
||
// 4. حذف فیچرهای قدیمی
|
||
if (toRemove.Any())
|
||
{
|
||
var featuresToDeactivate = await _context.UserClubFeatures
|
||
.Where(ucf => ucf.UserId == userId
|
||
&& ucf.IsActive
|
||
&& toRemove.Contains(ucf.ClubFeatureId))
|
||
.ToListAsync(ct);
|
||
|
||
foreach (var f in featuresToDeactivate)
|
||
{
|
||
f.IsActive = false;
|
||
f.Notes = $"حذف شده بابت تغییر پکیج (PackageId: {oldPackageId} → {newPackageId})";
|
||
}
|
||
}
|
||
|
||
// 5. اضافه فیچرهای جدید
|
||
if (toAdd.Any())
|
||
{
|
||
var newFeatures = toAdd.Select(featureId => new UserClubFeature
|
||
{
|
||
UserId = userId,
|
||
ClubMembershipId = clubMembershipId,
|
||
ClubFeatureId = featureId,
|
||
GrantedAt = DateTime.Now,
|
||
IsActive = true,
|
||
Notes = $"اضافه شده بابت خرید پکیج (PackageId: {newPackageId})"
|
||
}).ToList();
|
||
|
||
_context.UserClubFeatures.AddRange(newFeatures);
|
||
}
|
||
|
||
await _context.SaveChangesAsync(ct);
|
||
|
||
_logger.LogInformation(
|
||
"Feature DIFF applied for UserId={UserId}: Added={Added}, Removed={Removed}, Kept={Kept}",
|
||
userId, toAdd.Count, toRemove.Count,
|
||
currentFeatureIds.Count - toRemove.Count);
|
||
}
|
||
```
|
||
|
||
> **نکته مهم:** در اولین خرید (`currentFeatures` خالی)، تمام فیچرهای پکیج اضافه میشوند.
|
||
> در خرید مجدد **همان پکیج**، DIFF خالی است و هیچ تغییری در فیچرها نمیشود.
|
||
|
||
### ۵.۶ تشخیص فعالشدگان هفته (v5 — Q22)
|
||
|
||
```sql
|
||
-- کاربرانی که در هفته جاری فعال/خرید مجدد کردهاند:
|
||
SELECT cm."UserId", cm."LastPackageId", cm."LastActivationDate",
|
||
p."Title" AS PackageTitle
|
||
FROM "CMS"."ClubMemberships" cm
|
||
JOIN "CMS"."Packages" p ON p."Id" = cm."LastPackageId"
|
||
WHERE cm."IsActive" = true
|
||
AND cm."LastActivationDate" >= @WeekStartDate
|
||
AND cm."LastActivationDate" < @WeekEndDate;
|
||
|
||
-- اولین فعالسازی (عمر کاربر):
|
||
SELECT cm."FirstActivationDate", cm."FirstPackageId"
|
||
FROM "CMS"."ClubMemberships" cm
|
||
WHERE cm."UserId" = @UserId;
|
||
|
||
-- تعداد خرید (از تاریخچه):
|
||
SELECT COUNT(*) AS TotalPurchases
|
||
FROM "CMS"."ClubMembershipCycles" c
|
||
WHERE c."UserId" = @UserId;
|
||
```
|
||
|
||
---
|
||
|
||
## ۶. Commission Pool — تغییرات
|
||
|
||
### ۶.۱ ساختار جدید
|
||
|
||
```
|
||
هفته ۱:
|
||
Pool_نقرهای (PackageId=X): TotalAmount = Σ ActivationFee نقرهای
|
||
Pool_پایه (PackageId=Y): TotalAmount = Σ ActivationFee پایه
|
||
|
||
هر Pool مستقل:
|
||
ValuePerBalance = TotalPoolAmount ÷ TotalBalances
|
||
(فقط کاربران همان پکیج)
|
||
```
|
||
|
||
### ۶.۲ ساختار شبکه یکیست
|
||
|
||
```
|
||
[Ali - پایه]
|
||
/ \
|
||
[Sara - نقرهای] [Reza - پایه]
|
||
|
||
Commission (بالاسری Ali):
|
||
Ali → از Pool_پایه (چون Reza پکیج پایه داره)
|
||
Ali → از Pool_نقرهای (چون Sara پکیج نقرهای داره)
|
||
→ مجموع هر دو Pool → کیف پول شبکه Ali
|
||
→ به تفکیک: X از پایه، Y از نقرهای
|
||
|
||
⚠️ پکیج خود Ali مهم نیست! مهم پکیج زیرمجموعههاست
|
||
```
|
||
|
||
### ۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v5)
|
||
|
||
```
|
||
چرخه ۱ — کاربر "علی" پکیج پایه میخرد (۵۶M) — اولین خرید:
|
||
────────────────────────────────────────────────────────────────
|
||
✅ Balance += 56M
|
||
✅ DiscountBalance += 112M (×2)
|
||
✅ ActivationFee → Pool_پایه هفته جاری
|
||
✅ OTP + امضای قرارداد (فقط این بار — Q19)
|
||
✅ فیچرهای پکیج پایه فعال (DIFF = همه اضافه — لیست خالی بود)
|
||
✅ ClubMembership:
|
||
FirstActivationDate = now, FirstPackageId = پایه
|
||
LastActivationDate = now, LastPackageId = پایه
|
||
✅ ClubMembershipCycle #1 ساخته میشود
|
||
✅ NetworkWeeklyBalance (PackageId=پایه, WeekId=هفته جاری)
|
||
✅ بالاسریها: تعادلها بر اساس MaxBalancesPerLeg=300 + MaxNetworkLevel=15
|
||
✅ پورسانت بالاسریها از Pool_پایه
|
||
|
||
... خرید از فروشگاه → Balance صفر شد → Magic Mode فعال ...
|
||
... خرید جادویی → MagicBalance صفر + Deposit ≥ MaxDeposit → EXIT Magic ...
|
||
|
||
چرخه ۲ — کاربر "علی" دوباره پکیج پایه میخرد:
|
||
──────────────────────────────────────────────
|
||
✅ ریست وضعیت: PackagePurchaseMethod=None (membership فعال باقی — Q19)
|
||
✅ Balance += 56M (مجدد شارژ)
|
||
✅ DiscountBalance += 112M (مجدد شارژ)
|
||
✅ ActivationFee → Pool_پایه هفته جاری
|
||
❌ بدون قرارداد مجدد (Q19 — قبلاً امضا شده)
|
||
✅ فیچر DIFF = خالی (همان پکیج → بدون تغییر فیچر — Q20)
|
||
✅ ClubMembership:
|
||
FirstActivationDate = حفظ, FirstPackageId = حفظ
|
||
LastActivationDate = now (بروزرسانی), LastPackageId = پایه
|
||
✅ ClubMembershipCycle #2 ساخته میشود
|
||
✅ بالاسری علی: carryover همه Poolها (بر اساس پکیج زیرمجموعهها) حفظ میشود
|
||
✅ پورسانت بالاسری: از هر Pool که زیرمجموعهای دارد
|
||
|
||
... همان چرخه Magic Wallet تکرار ...
|
||
|
||
چرخه ۵ — کاربر "علی" پکیج نقرهای میخرد (۵.۶M):
|
||
────────────────────────────────────────────────────
|
||
✅ Balance += 5.6M
|
||
✅ DiscountBalance += 11.2M (×2)
|
||
✅ ActivationFee → Pool_نقرهای هفته جاری (۲,۵۲۰,۰۰۰)
|
||
❌ بدون قرارداد مجدد (Q19)
|
||
✅ فیچر DIFF (Q20):
|
||
فعلی: [چاتیکا, بیمه, سفر, آموزش] (از پکیج پایه)
|
||
جدید: [چاتیکا, بیمه] (از پکیج نقرهای)
|
||
→ حذف: [سفر, آموزش] → IsActive=false
|
||
→ اضافه: [] → هیچی
|
||
→ حفظ: [چاتیکا, بیمه]
|
||
✅ ClubMembership:
|
||
FirstActivationDate = حفظ, FirstPackageId = حفظ (پایه)
|
||
LastActivationDate = now (بروزرسانی), LastPackageId = نقرهای
|
||
✅ ClubMembershipCycle #5 ساخته میشود (PackageId=نقرهای)
|
||
✅ بالاسریهای علی: carryover همه Poolها حفظ — تغییر پکیج علی تاثیری ندارد!
|
||
(carryover بر اساس پکیج زیرمجموعههاست نه خود علی)
|
||
✅ پورسانت بالاسری از هر Pool جداگانه:
|
||
Pool_پایه: بر اساس زیرمجموعههایی که پکیج پایه دارند
|
||
Pool_نقرهای: بر اساس زیرمجموعههایی که پکیج نقرهای دارند
|
||
✅ ActivationFee علی → Pool_نقرهای (۲,۵۲۰,۰۰۰)
|
||
```
|
||
|
||
### ۶.۴ تعادلها (NetworkWeeklyBalance) — per-downline-package (v6)
|
||
|
||
> ⚠️ **تغییر اساسی v6:** تعادل هر کاربر بر اساس پکیج **زیرمجموعهها** گروهبندی میشود — **نه** پکیج خود کاربر.
|
||
|
||
> ✅ **قانون carryover v6 (Q23 اصلاحشده):** تغییر پکیج خود کاربر **هیچ تاثیری** بر carryover ندارد. carryover مال زیرمجموعههاست.
|
||
|
||
```
|
||
قبل (تکپکیج):
|
||
NetworkWeeklyBalance: [UserId, WeekId] → یک رکورد
|
||
|
||
بعد (چندپکیج):
|
||
NetworkWeeklyBalance: [UserId, WeekId, PackageId] → N رکورد
|
||
(N = تعداد پکیجهای مختلف زیرمجموعهها)
|
||
```
|
||
|
||
**الگوریتم محاسبه تعادل per-downline-package:**
|
||
|
||
```
|
||
برای هر بالاسری (کاربر):
|
||
برای هر پکیج فعال (X):
|
||
1. واکشی زیرمجموعههایی که پکیج X دارند:
|
||
زیرمجموعهها WHERE ClubMembership.LastPackageId = X
|
||
2. شمارش: چند نفر تیم چپ + چند نفر تیم راست
|
||
3. carryover از هفته قبل:
|
||
خواندن رکورد [UserId, PrevWeekId, PackageId=X]
|
||
✅ همیشه خوانده میشود — مستقل از پکیج خود کاربر!
|
||
4. LeftLegTotal = NewLeft + CarryoverLeft
|
||
5. RightLegTotal = NewRight + CarryoverRight
|
||
6. TotalBalances = MIN(Left, Right) → cap at Package.MaxBalancesPerLeg
|
||
7. Remainder → carryover هفته بعد (برای PackageId = X)
|
||
8. پورسانت: TotalBalances × ValuePerBalance_X → کیف پول شبکه
|
||
|
||
مجموع پورسانت از همه Poolها → واریز به کیف پول شبکه کاربر
|
||
(به تفکیک مشخص: هر مبلغ از کدام Pool)
|
||
```
|
||
|
||
**مثال جامع — بالاسری "علی":**
|
||
|
||
```
|
||
علی (خودش پکیج پایه داره)
|
||
├── تیم چپ:
|
||
│ ├── سارا (پکیج پایه)
|
||
│ ├── رضا (پکیج پایه)
|
||
│ └── مریم (پکیج نقرهای)
|
||
└── تیم راست:
|
||
├── حسین (پکیج پایه)
|
||
└── زهرا (پکیج نقرهای)
|
||
```
|
||
|
||
```
|
||
هفته ۱۰ — محاسبه تعادل علی:
|
||
═══════════════════════════════════════════════════════
|
||
|
||
═══ Pool پکیج پایه (زیرمجموعههایی که پایه دارن) ═══
|
||
چپ: سارا + رضا = 2
|
||
راست: حسین = 1
|
||
carryover هفته ۹: {چپ: 3, راست: 0}
|
||
جمع: چپ = 2+3 = 5, راست = 1+0 = 1
|
||
تعادل = MIN(5, 1) = 1 (cap 300 → OK)
|
||
carryover → هفته ۱۱: {چپ: 4, راست: 0}
|
||
پورسانت: 1 × ValuePerBalance_پایه = A ریال
|
||
|
||
═══ Pool پکیج نقرهای (زیرمجموعههایی که نقرهای دارن) ═══
|
||
چپ: مریم = 1
|
||
راست: زهرا = 1
|
||
carryover هفته ۹: {چپ: 0, راست: 0}
|
||
جمع: چپ = 1+0 = 1, راست = 1+0 = 1
|
||
تعادل = MIN(1, 1) = 1 (cap 30 → OK)
|
||
carryover → هفته ۱۱: {چپ: 0, راست: 0}
|
||
پورسانت: 1 × ValuePerBalance_نقرهای = B ریال
|
||
|
||
═══ مجموع پورسانت علی هفته ۱۰: ═══
|
||
کیف پول شبکه += (A + B)
|
||
ردیابی: A از Pool پایه، B از Pool نقرهای
|
||
```
|
||
|
||
**هفته ۱۱ — علی پکیج خودش رو عوض میکنه (پایه → نقرهای):**
|
||
|
||
```
|
||
═══ Pool پکیج پایه ═══
|
||
carryover از هفته ۱۰: {چپ: 4, راست: 0} ← هنوز هست!
|
||
✅ تغییر پکیج خود علی تاثیری نداره!
|
||
اعضای جدید: چپ = 0, راست = 1
|
||
جمع: چپ = 0+4 = 4, راست = 1+0 = 1
|
||
تعادل = MIN(4, 1) = 1
|
||
✅ علی هنوز از Pool پایه سود میبره (چون زیرمجموعههایی با پکیج پایه داره)
|
||
|
||
═══ Pool پکیج نقرهای ═══
|
||
carryover از هفته ۱۰: {چپ: 0, راست: 0}
|
||
... محاسبه عادی ...
|
||
```
|
||
|
||
**نکته کلیدی:** پکیج **خود کاربر** فقط تعیین میکنه ActivationFeeاش به کدوم Pool بره.
|
||
**تعادل و carryover** بر اساس پکیج **زیرمجموعهها** محاسبه میشه.
|
||
|
||
**Cap (سقف):**
|
||
```
|
||
اگه تعادل بیشتر از MaxBalancesPerLeg بشه → بریده میشه (flush):
|
||
تعادل = 500، سقف = 300 → CappedBalance = 300، Flushed = 200
|
||
⚠️ Flushed از بین میره — carry نمیشه!
|
||
```
|
||
|
||
### ۶.۵ تغییرات SP (v3 — بروزرسانی)
|
||
|
||
| SP | تغییرات v2 | تغییرات اضافی v3 |
|
||
|----|------------|------------------|
|
||
| `sp_CalculateWeeklyBalances` | پارامتر `@PackageId` | `@MaxBalancesPerLeg` و `@MaxNetworkLevel` هم **پارامتر** شوند (نه hardcoded) |
|
||
| `sp_CalculateWeeklyCommissionPool` | پارامتر `@PackageId` | Pool فقط از تعادلهای همان PackageId |
|
||
|
||
**sp_CalculateWeeklyBalances — تغییرات ساختاری:**
|
||
|
||
```sql
|
||
-- قبل (v2 — فقط PackageId فیلتر):
|
||
CREATE PROCEDURE sp_CalculateWeeklyBalances
|
||
@WeekDefinitionId BIGINT,
|
||
@PackageId BIGINT,
|
||
@MaxBalancesPerLeg INT = 300, -- ← hardcoded!
|
||
@MaxNetworkLevel INT = 15 -- ← hardcoded!
|
||
|
||
-- بعد (v3 — همه داینامیک):
|
||
CREATE PROCEDURE sp_CalculateWeeklyBalances
|
||
@WeekDefinitionId BIGINT,
|
||
@PackageId BIGINT,
|
||
@MaxBalancesPerLeg INT, -- ← از Package entity خوانده میشود
|
||
@MaxNetworkLevel INT, -- ← از Package entity خوانده میشود
|
||
@ForceRecalculate BIT = 0,
|
||
@RowCount INT OUTPUT
|
||
```
|
||
|
||
**StoredProcedureCommissionCalculationStrategy — تغییرات:**
|
||
|
||
```csharp
|
||
// قبل: فقط WeekDefinitionId پاس میداد
|
||
await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances",
|
||
new { WeekDefinitionId = weekId, ForceRecalculate = true });
|
||
|
||
// بعد (v3): پکیج + تنظیمات داینامیک
|
||
await connection.ExecuteAsync("CMS.sp_CalculateWeeklyBalances",
|
||
new {
|
||
WeekDefinitionId = weekId,
|
||
PackageId = package.Id,
|
||
MaxBalancesPerLeg = package.MaxBalancesPerLeg,
|
||
MaxNetworkLevel = package.MaxNetworkLevel,
|
||
ForceRecalculate = true
|
||
});
|
||
```
|
||
|
||
### ۶.۶ تغییرات Service (v3)
|
||
|
||
```csharp
|
||
// WeeklyCommissionCalculationService — Loop روی پکیجها:
|
||
var activePackages = await _context.Packages
|
||
.Where(p => p.IsActive && !p.IsDeleted)
|
||
.ToListAsync();
|
||
|
||
foreach (var package in activePackages)
|
||
{
|
||
_logger.LogInformation(
|
||
"Calculating commission for package {Id}: {Title} " +
|
||
"(MaxBalances={Max}, MaxLevel={Level})",
|
||
package.Id, package.Title,
|
||
package.MaxBalancesPerLeg, package.MaxNetworkLevel);
|
||
|
||
// محاسبه تعادلها — هر پکیج با تنظیمات خودش
|
||
await strategy.CalculateWeeklyBalancesAsync(
|
||
weekId, package.Id,
|
||
package.MaxBalancesPerLeg, package.MaxNetworkLevel);
|
||
|
||
// محاسبه Pool — هر پکیج جداگانه
|
||
await strategy.CalculateWeeklyPoolAsync(weekId, package.Id);
|
||
}
|
||
```
|
||
|
||
### ۶.۷ گزارشدهی پورسانت per-package (v3 — جدید)
|
||
|
||
> ⚠️ **فعلاً `package_id` در هیچکدام از پیامهای commission.proto وجود ندارد!**
|
||
|
||
#### تغییرات Proto (commission.proto):
|
||
|
||
```diff
|
||
message UserWeeklyBalanceModel {
|
||
// ... فیلدهای فعلی ...
|
||
+ int64 package_id = 17;
|
||
+ string package_title = 18;
|
||
}
|
||
|
||
message UserCommissionPayoutModel {
|
||
// ... فیلدهای فعلی ...
|
||
+ int64 package_id = 16;
|
||
+ string package_title = 17;
|
||
}
|
||
|
||
message CustomerCommissionPayoutModel {
|
||
// ... فیلدهای فعلی ...
|
||
+ int64 package_id = 10;
|
||
+ string package_title = 11;
|
||
}
|
||
|
||
message CustomerWeeklyBalanceModel {
|
||
// ... فیلدهای فعلی ...
|
||
+ int64 package_id = 15;
|
||
+ string package_title = 16;
|
||
}
|
||
|
||
+ // مدل خلاصه پورسانت per-package برای مشتری
|
||
+ message CustomerCommissionPackageSummary {
|
||
+ int64 package_id = 1;
|
||
+ string package_title = 2;
|
||
+ int32 downline_count = 3; // تعداد زیرمجموعه با این پکیج
|
||
+ int32 left_leg_members = 4;
|
||
+ int32 right_leg_members = 5;
|
||
+ int32 total_balances = 6;
|
||
+ int64 commission_earned = 7; // پورسانت کسبشده از این پکیج
|
||
+ string commission_formatted = 8;
|
||
+ }
|
||
|
||
// Request فیلتر بر اساس پکیج
|
||
message GetAllWeeklyBalancesByFilterRequest {
|
||
// ... فیلدهای فعلی ...
|
||
+ Int64Value package_id = 7; // فیلتر اختیاری
|
||
}
|
||
|
||
message GetUserCommissionPayoutsRequest {
|
||
// ... فیلدهای فعلی ...
|
||
+ Int64Value package_id = 7; // فیلتر اختیاری
|
||
}
|
||
```
|
||
|
||
#### FrontOffice — گزارش پورسانت per-package (صفحه جدید/بهبود):
|
||
|
||
```
|
||
┌─── پاداشهای من — هفته ۱۰ ────────────────────────────────────────────┐
|
||
│ │
|
||
│ 📊 خلاصه بر اساس پکیج: │
|
||
│ │
|
||
│ ┌── پکیج پایه ─────────────────┐ ┌── پکیج نقرهای ───────────────┐ │
|
||
│ │ زیرمجموعه: ۱۲ نفر │ │ زیرمجموعه: ۵ نفر │ │
|
||
│ │ تیم اول: ۷ │ تیم دوم: ۵ │ │ تیم اول: ۳ │ تیم دوم: ۲ │ │
|
||
│ │ تعادل: ۵ │ │ تعادل: ۲ │ │
|
||
│ │ 💰 پاداش: ۱,۲۵۰,۰۰۰ تومان │ │ 💰 پاداش: ۱۸۰,۰۰۰ تومان │ │
|
||
│ └───────────────────────────────┘ └──────────────────────────────┘ │
|
||
│ │
|
||
│ 📦 مجموع پاداش هفته: ۱,۴۳۰,۰۰۰ تومان │
|
||
│ ├── از پکیج پایه: ۱,۲۵۰,۰۰۰ │
|
||
│ └── از پکیج نقرهای: ۱۸۰,۰۰۰ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
#### BackOffice — گزارش ادمین با فیلتر پکیج:
|
||
|
||
```
|
||
┌─── گزارش تعادلها — هفته ۱۰ ───────────────────────────────────────┐
|
||
│ │
|
||
│ فیلتر: [▼ پکیج: همه ▼] [کاربر: ___] [هفته: ▼ هفته ۱۰ ▼] [جستجو]│
|
||
│ ├── همه │
|
||
│ ├── پکیج پایه │
|
||
│ └── پکیج نقرهای │
|
||
│ │
|
||
│ # │ کاربر │ پکیج │ تیم اول │ تیم دوم │ تعادل │ سهم استخر │
|
||
│ ──┼──────────┼─────────┼─────────┼─────────┼───────┼────────────────│
|
||
│ 1 │ علی │ پایه │ ۴۵ │ ۵۲ │ ۴۵ │ ۲,۲۵۰,۰۰۰ │
|
||
│ 2 │ سارا │ نقرهای │ ۱۲ │ ۸ │ ۸ │ ۱۴۴,۰۰۰ │
|
||
│ 3 │ رضا │ پایه │ ۳۰ │ ۲۵ │ ۲۵ │ ۱,۲۵۰,۰۰۰ │
|
||
│ 4 │ رضا │ نقرهای │ ۲ │ ۰ │ ۰ │ ۰ │
|
||
│ │
|
||
│ خلاصه: پکیج پایه: ۷۰ تعادل | پکیج نقرهای: ۸ تعادل │
|
||
└──────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## ۷. Event-Driven Side Effects
|
||
|
||
### ۷.۱ ساخت پکیج جدید
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
A["ساخت پکیج جدید<br/>(از BackOffice)"] --> B["PackageCreatedEvent"]
|
||
B --> C["ایجاد Pool خالی<br/>برای هفته جاری"]
|
||
B --> D["لاگ ادمین"]
|
||
```
|
||
|
||
### ۷.۲ جدول رویدادها
|
||
|
||
| رویداد | Side Effect |
|
||
|--------|------------|
|
||
| `PackageCreated` | ساخت WeeklyCommissionPool خالی هفته جاری |
|
||
| `PackageDeactivated` | هشدار ادمین — Pool موجود تکمیل شود |
|
||
| `PackagePurchased` | ActivationFee → Pool پکیج + شارژ wallets |
|
||
| `MagicCycleCompleted` | ریست PackagePurchaseMethod + حفظ membership فعال (Q19) |
|
||
|
||
---
|
||
|
||
## ۷٫۵ تحلیل جامع Side Effectها (v4 — کشف جدید)
|
||
|
||
> ⚠️ **این بخش سایدافکتهایی را مستند میکند که در تحلیلهای v1–v3 کشف نشده بودند.**
|
||
> هر آیتم با بررسی عمیق کدبیس CMS، FrontOffice و BackOffice شناسایی شده.
|
||
|
||
### ۷٫۵٫۱ Entity — WalletChangeLog بدون PackageId
|
||
|
||
> 🔴 **بحرانی — ۲۱ محل ساخت WalletChangeLog**
|
||
|
||
Entity فعلی `UserWalletChangeLog` **هیچ فیلد PackageId ندارد**. در سیستم چندپکیجی نمیتوان ردیابی کرد کدام پکیج باعث تغییر کیفپول شده.
|
||
|
||
**محلهای ساخت WalletChangeLog (۲۱ مورد):**
|
||
|
||
| # | فایل | کانتکست |
|
||
|---|------|---------|
|
||
| 1-2 | `UserOrderService.cs` | خرید فروشگاه + لغو مشتری |
|
||
| 3-4 | `VerifyPackagePurchaseCommandHandler` | Balance + DiscountBalance |
|
||
| 5 | `VerifyBasePackagePaymentCommandHandler` | Balance |
|
||
| 6-7 | `VerifyGoldenPackagePurchaseCommandHandler` | Balance + Discount |
|
||
| 8 | `VerifyMagicWalletChargeCommandHandler` | شارژ جادویی |
|
||
| 9-10 | `CheckAndProcessDayaLoansCommandHandler` | Balance + Discount |
|
||
| 11-12 | `CreateManualPaymentCommandHandler` | Balance + Discount |
|
||
| 13-18 | `ChargeUserWalletsCommandHandler` | ۶ شاخه switch |
|
||
| 19 | `CancelOrderByAdminCommandHandler` | ریفاند ادمین |
|
||
| 20 | `SpCommissionCalculationStrategy` | پرداخت پورسانت |
|
||
| 21 | `OrmCommissionCalculationStrategy` | پرداخت پورسانت |
|
||
|
||
**اقدام لازم:**
|
||
```csharp
|
||
// اضافه به UserWalletChangeLog entity:
|
||
public long? PackageId { get; set; }
|
||
public virtual Package Package { get; set; }
|
||
```
|
||
+ Migration + بروزرسانی ۲۱ محل
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۲ Validator — سقف hardcoded 1B
|
||
|
||
> 🟡 **متوسط — ۳ Validator**
|
||
|
||
| Validator | فایل | قانون | مشکل |
|
||
|-----------|------|-------|------|
|
||
| `ChargeMagicWalletCommandValidator` | FluentValidation | `.LessThanOrEqualTo(1_000_000_000)` | سقف global — پکیج نقرهای ممکنه ۱۰۰M باشه |
|
||
| `ChargeDiscountWalletCommandValidator` | FluentValidation | `.LessThanOrEqualTo(1_000_000_000)` | سقف global |
|
||
| `CreateManualPaymentCommandValidator` | FluentValidation | `.LessThanOrEqualTo(1_000_000_000)` | سقف global |
|
||
|
||
**اقدام:** Validator باید PackageId بگیره و از `Package.MagicWalletMaxDeposit` بخونه، یا حداکثر بین همه پکیجها.
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۳ Magic Wallet — ۶ Side Effect
|
||
|
||
> 🔴 **بحرانی — چرخه حیات Magic Wallet کامل global است**
|
||
|
||
| # | فایل | مشکل | شدت |
|
||
|---|------|------|------|
|
||
| **MW1** | `ChargeMagicWalletCommandHandler` | `remainingDeposit = SystemConstants.MagicWalletMaxDeposit - wallet.MagicTotalDeposited` ← global 1B | 🔴 |
|
||
| **MW2** | `VerifyMagicWalletChargeCommandHandler` | `creditAmount = depositAmount * SystemConstants.MagicWalletMultiplier` ← global ×2.5 | 🔴 |
|
||
| **MW3** | `UserOrderService.cs` L347 | EXIT trigger: `wallet.MagicTotalDeposited >= SystemConstants.MagicWalletMaxDeposit` ← global 1B | 🔴 |
|
||
| **MW4** | `UserOrderService.cs` L363 | ENTRY trigger: فقط `PackagePurchaseMethod != None` — چک نمیکنه **کدام** پکیج | 🟡 |
|
||
| **MW5** | `WalletGrpcService` | `GetMagicWalletStatus` → سقف و باقیمانده global به FrontOffice ارسال | 🟡 |
|
||
| **MW6** | `ActivateClubMembershipCommandHandler` | Guard: `WalletMode == Magic → throw` — مدت Magic وابسته به سقف per-package | 🟡 |
|
||
|
||
**فلوی مشکلدار:**
|
||
```
|
||
پکیج نقرهای (MaxDeposit=100M):
|
||
کاربر ۱۰۰M شارژ کرد → باید EXIT شه
|
||
❌ EXIT نمیشه! چون سیستم ۱B (global) چک میکنه
|
||
❌ کاربر تا ابد در Magic Mode گیر میافته!
|
||
|
||
پکیج طلایی (MaxDeposit=2B):
|
||
کاربر ۱B شارژ کرد → EXIT اشتباه!
|
||
❌ سیستم فکر میکنه سقف رسیده
|
||
❌ کاربر زودتر از موعد از Magic خارج میشه!
|
||
```
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۴ JWT Claims — بدون PackageId
|
||
|
||
> 🟡 **بالا — اطلاعات ناکافی JWT**
|
||
|
||
| Claim فعلی | نوع | مشکل |
|
||
|------------|-----|------|
|
||
| `HasPurchasedGoldenPackage` | `bool` | فقط boolean — نمیگه **کدام** پکیج |
|
||
| `PackagePurchaseMethod` | missing | در JWT نیست — فقط در DB |
|
||
| `CanRepurchase` | **وجود ندارد** | هیچجا تعریف نشده — UI نمیتونه دکمه خرید مجدد نشون بده |
|
||
| `PackageId` | **وجود ندارد** | فرانت نمیدونه کاربر کدام پکیج رو داره |
|
||
| `PackageTitle` | **وجود ندارد** | نام پکیج در JWT نیست |
|
||
|
||
**اقدام — Claims جدید:**
|
||
```csharp
|
||
claims.Add("PackageId", membership.PackageId?.ToString() ?? "");
|
||
claims.Add("PackageTitle", package?.Title ?? "");
|
||
claims.Add("CanRepurchase", HasCompletedMagicCycle(membership).ToString());
|
||
// حذف HasPurchasedGoldenPackage → جایگزین با PackageId > 0
|
||
```
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۵ Club Features — تخصیص global → DIFF (v5)
|
||
|
||
> 🔴 **بحرانی — فیچرها باید DIFF/تفاضل باشند (Q20)**
|
||
|
||
```csharp
|
||
// فعلی (اشتباه) — ActivateClubMembershipCommandHandler.cs + AcceptClubMembershipContractCommandHandler.cs:
|
||
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
|
||
// ↑ همیشه همه فیچرها (Chatika, Bime, Trip, Learn) — صرفنظر از پکیج!
|
||
```
|
||
|
||
**مشکل ۱:** پکیج نقرهای ممکنه فقط ۲ فیچر داشته باشه ولی سیستم فعلی **همه ۴ فیچر** رو فعال میکنه.
|
||
**مشکل ۲:** در خرید مجدد، باید فقط **تفاضل** اعمال بشه (Q20) — نه تخصیص مجدد همه.
|
||
|
||
**اقدام (v5):**
|
||
```csharp
|
||
// خرید اول (currentFeatures خالی → همه فیچرها اضافه):
|
||
await _featureDiffService.ApplyFeatureDiffAsync(
|
||
userId, membershipId,
|
||
oldPackageId: 0, // بدون پکیج قبلی
|
||
newPackageId: package.Id);
|
||
|
||
// خرید مجدد (مقایسه + تفاضل):
|
||
await _featureDiffService.ApplyFeatureDiffAsync(
|
||
userId, membershipId,
|
||
oldPackageId: membership.LastPackageId,
|
||
newPackageId: newPackage.Id);
|
||
```
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۶ Notification — بدون PackageId
|
||
|
||
> 🟡 **متوسط — ۴ Notification**
|
||
|
||
| # | Notification | مشکل |
|
||
|---|-------------|------|
|
||
| N1 | `CommissionDepositedNotification` | پیام: "پورسانت X ریال واریز شد" — نمیگه از **کدام پکیج** |
|
||
| N2 | `ClubActivatedNotification` | پیام: "عضویت فعال شد" — نمیگه **کدام پکیج** فعال شد |
|
||
| N3 | `CommissionPayoutFailedNotification` | بدون PackageId |
|
||
| N4 | Daya SMS | "وام دایا تایید شد" — نمیگه برای کدام پکیج |
|
||
|
||
**اقدام:** Interface `INotification` باید `PackageId` + `PackageTitle` بگیره.
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۷ Background Services — ۳ سرویس
|
||
|
||
> 🟡 **متوسط**
|
||
|
||
| # | سرویس | مشکل |
|
||
|---|--------|------|
|
||
| BG1 | `ClubMembershipCycleSeedService` | چرخهها را با `PurchaseAmount=0` seed میکنه — PackageId ندارد |
|
||
| BG2 | `DayaLoanStatusCheckWorker` | `DayaLoanAmount` global — بدون PackageId |
|
||
| BG3 | `ChatikaAccountActivationWorker` | همه اعضا — بدون فیلتر پکیج (Chatika ممکنه فقط پکیج پایه) |
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۸ Commission Reports — بدون PackageId
|
||
|
||
> 🟡 **متوسط**
|
||
|
||
| # | فایل | مشکل |
|
||
|---|------|------|
|
||
| CR1 | `CommissionWithdrawalReportService` | گزارش برداشت global — بدون فیلتر پکیج |
|
||
| CR2 | `WorkerExecutionLog` | لاگ اجرای Hangfire — نمیگه کدام پکیج محاسبه شد |
|
||
| CR3 | Admin manual trigger endpoint | `EnqueueCommissionCalculation` بدون پارامتر PackageId |
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۹ FrontOffice — متنهای hardcoded
|
||
|
||
> 🔴 **بحرانی — مسئولیت حقوقی قرارداد**
|
||
|
||
| # | فایل | مشکل | شدت |
|
||
|---|------|------|------|
|
||
| FO1 | `ClubMembershipContractDialog.razor` | **"۵۶ میلیون تومان (پکیج پایه)"** در متن قرارداد حقوقی — اگه نقرهای بخره اشتباهه! | 🔴 حقوقی |
|
||
| FO2 | `PackagePurchaseBottomSheet.razor` | **"خرید پکیج پایه ۵۶ میلیون تومان"** hardcoded | 🔴 |
|
||
| FO3 | `ActivationSection.razor` | `56_000_000` hardcoded در محاسبه هزینه | 🔴 |
|
||
| FO4 | `MagicWalletChargePage.razor` | "×۲.۵" و "سقف واریز: ۱۰۰ میلیون" hardcoded در ۶ جا | 🟡 |
|
||
| FO5 | `MagicWalletChargePage.razor` L112 | `_chargeAmount * 2.5m` hardcoded در **C# code** | 🔴 |
|
||
| FO6 | صفحات متعدد | **"پکیج طلایی"** hardcoded (حداقل ۵ جا) — نام اشتباه! | 🟡 |
|
||
| FO7 | Activation flow | `PackageId = 1` hardcoded | 🔴 |
|
||
| FO8 | `HasPurchasedPackage` claim | `bool` — UI نمیتونه multi-package نشون بده | 🟡 |
|
||
| FO9 | Network tree | بدون نمایش پکیج هر نود | 🟢 |
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۱۰ BackOffice — ۸ Side Effect
|
||
|
||
> 🟡 **بالا**
|
||
|
||
| # | فایل | مشکل | شدت |
|
||
|---|------|------|------|
|
||
| BO-S1 | `ManualActivationDialog.razor` | مبلغ `56_000_000` hardcoded + `Disabled` + **بدون package selector** | 🔴 |
|
||
| BO-S2 | `SystemConfigurationPage.razor` | تنظیمات global — MaxWithdrawal, MaxTreeDepth, CommissionPercent باید per-package بشن | 🔴 |
|
||
| BO-S3 | Commission Dashboard | یک Pool واحد نشون میده — بدون per-package | 🟡 |
|
||
| BO-S4 | User detail page | `HasPurchasedGoldenPackage` boolean — نمیگه کدام پکیج | 🟡 |
|
||
| BO-S5 | Package CRUD grid | فقط Title + Price — فیلدهای v3 ندارد | 🟡 |
|
||
| BO-S6 | Club members grid | ستون PackageTitle ✅ اما فیلتر ❌ | 🟢 |
|
||
| BO-S7 | CSV exports (۳ جا) | بدون ستون پکیج | 🟡 |
|
||
| BO-S8 | Network tree viewer | بدون پکیج هر نود + `MaxNetworkDepth` global | 🟡 |
|
||
|
||
---
|
||
|
||
### ۷٫۵٫۱۱ خلاصه آماری Side Effectها
|
||
|
||
```
|
||
═══════════════════════════════════════════════════════════════
|
||
لایه 🔴 بحرانی 🟡 بالا/متوسط 🟢 کم جمع
|
||
═══════════════════════════════════════════════════════════════
|
||
CMS Domain ۲ ۱ ۰ ۳
|
||
CMS Application ۶ ۴ ۰ ۱۰
|
||
CMS Infrastructure ۱ ۳ ۰ ۴
|
||
CMS Validators ۰ ۳ ۰ ۳
|
||
FrontOffice ۵ ۳ ۱ ۹
|
||
BackOffice ۲ ۵ ۱ ۸
|
||
Notifications ۰ ۴ ۰ ۴
|
||
Background Jobs ۰ ۳ ۰ ۳
|
||
─────────────────────────────────────────────────────────────
|
||
جمع کل ۱۶ ۲۶ ۲ ۴۴
|
||
═══════════════════════════════════════════════════════════════
|
||
```
|
||
|
||
> **مجموع سایدافکتهای کشفشده v4: ۴۴ مورد** (نسبت به ۴۸ تغییر اصلی v3)
|
||
> **بیشتری تغییرات لازم از سایدافکتها ناشی میشوند!**
|
||
|
||
---
|
||
|
||
## ۸. Seed Data
|
||
|
||
```sql
|
||
-- پکیج پایه (۵۶ میلیون تومان)
|
||
INSERT INTO "CMS"."Packages" (
|
||
"Title", "Description", "Price", "IsActive", "IsBasePackage",
|
||
"SupportsDayaPurchase", "SupportsDirectPurchase",
|
||
"ActivationFee", "DiscountMultiplier", "MagicWalletMultiplier",
|
||
"MaxBalancesPerLeg", "MaxNetworkLevel",
|
||
"MagicWalletMaxDeposit", "MagicWalletMaxCredit",
|
||
"SortOrder", "ImagePath"
|
||
) VALUES (
|
||
'پکیج پایه', 'پکیج اصلی باشگاه مشتریان کارا بازار سلامت',
|
||
56000000, true, true,
|
||
true, true,
|
||
25200000, 2.0, 2.5,
|
||
300, 15,
|
||
1000000000, 2500000000,
|
||
2, ''
|
||
);
|
||
|
||
-- پکیج نقرهای (۵.۶ میلیون تومان)
|
||
INSERT INTO "CMS"."Packages" (
|
||
"Title", "Description", "Price", "IsActive", "IsBasePackage",
|
||
"SupportsDayaPurchase", "SupportsDirectPurchase",
|
||
"ActivationFee", "DiscountMultiplier", "MagicWalletMultiplier",
|
||
"MaxBalancesPerLeg", "MaxNetworkLevel",
|
||
"MagicWalletMaxDeposit", "MagicWalletMaxCredit",
|
||
"SortOrder", "ImagePath"
|
||
) VALUES (
|
||
'پکیج نقرهای', 'پکیج سطح نقرهای باشگاه مشتریان',
|
||
5600000, true, false,
|
||
false, true,
|
||
2520000, 2.0, 2.5,
|
||
30, 15,
|
||
100000000, 250000000,
|
||
1, ''
|
||
);
|
||
|
||
-- فیچرهای پکیج پایه: همه فیچرها
|
||
INSERT INTO "CMS"."PackageFeatures" ("PackageId", "ClubFeatureId", "IsIncluded")
|
||
SELECT base."Id", cf."Id", true
|
||
FROM "CMS"."Packages" base
|
||
CROSS JOIN "CMS"."ClubFeatures" cf
|
||
WHERE base."IsBasePackage" = true AND cf."IsDeleted" = false;
|
||
|
||
-- فیچرهای پکیج نقرهای: تعیین میشود از BackOffice
|
||
```
|
||
|
||
---
|
||
|
||
## ۹. Migration دادههای فعلی
|
||
|
||
```sql
|
||
-- ========================================
|
||
-- STEP 1: مشخص کردن ID پکیج پایه
|
||
-- ========================================
|
||
DO $$
|
||
DECLARE base_pkg_id BIGINT;
|
||
BEGIN
|
||
SELECT "Id" INTO base_pkg_id
|
||
FROM "CMS"."Packages" WHERE "IsBasePackage" = true LIMIT 1;
|
||
|
||
-- STEP 2: ClubMembership — v5 migration (Q21)
|
||
-- ActivatedAt → FirstActivationDate + LastActivationDate
|
||
UPDATE "CMS"."ClubMemberships"
|
||
SET "FirstActivationDate" = "ActivatedAt",
|
||
"LastActivationDate" = "ActivatedAt",
|
||
"FirstPackageId" = base_pkg_id,
|
||
"LastPackageId" = base_pkg_id
|
||
WHERE "FirstActivationDate" IS NULL;
|
||
|
||
-- STEP 3: ClubMembershipCycle
|
||
UPDATE "CMS"."ClubMembershipCycles"
|
||
SET "PackageId" = base_pkg_id WHERE "PackageId" IS NULL;
|
||
|
||
-- STEP 4: WeeklyCommissionPool
|
||
UPDATE "CMS"."WeeklyCommissionPools"
|
||
SET "PackageId" = base_pkg_id WHERE "PackageId" IS NULL;
|
||
|
||
-- STEP 5: UserCommissionPayout
|
||
UPDATE "CMS"."UserCommissionPayouts"
|
||
SET "PackageId" = base_pkg_id WHERE "PackageId" IS NULL;
|
||
|
||
-- STEP 6: NetworkWeeklyBalance (v3 — جدید)
|
||
UPDATE "CMS"."NetworkWeeklyBalances"
|
||
SET "PackageId" = base_pkg_id WHERE "PackageId" IS NULL;
|
||
|
||
RAISE NOTICE 'Migration completed for PackageId=%', base_pkg_id;
|
||
END $$;
|
||
|
||
-- STEP 7: Verify — همه باید 0 باشند
|
||
SELECT 'ClubMemberships' AS tbl, COUNT(*) FROM "CMS"."ClubMemberships" WHERE "LastPackageId" IS NULL
|
||
UNION ALL
|
||
SELECT 'Cycles', COUNT(*) FROM "CMS"."ClubMembershipCycles" WHERE "PackageId" IS NULL
|
||
UNION ALL
|
||
SELECT 'Pools', COUNT(*) FROM "CMS"."WeeklyCommissionPools" WHERE "PackageId" IS NULL
|
||
UNION ALL
|
||
SELECT 'Payouts', COUNT(*) FROM "CMS"."UserCommissionPayouts" WHERE "PackageId" IS NULL
|
||
UNION ALL
|
||
SELECT 'Balances', COUNT(*) FROM "CMS"."NetworkWeeklyBalances" WHERE "PackageId" IS NULL;
|
||
```
|
||
|
||
---
|
||
|
||
## ۱۱. Impact Analysis — ۹۵+ تغییر در ۶ لایه (v5)
|
||
|
||
> ۵۱ تغییر اصلی (بخش ۱۰) + ۴۴ سایدافکت (بخش ۷.۵) = **۹۵ تغییر کل**
|
||
|
||
### ۱۰.۱ لایه Domain (۱۰ تغییر)
|
||
|
||
| # | فایل | نوع | شدت | v3? |
|
||
|---|------|-----|------|-----|
|
||
| D1 | `Package.cs` | اضافه **۱۱ فیلد** جدید (v2: ۷ + v3: ۴) | 🟡 | 🔄 |
|
||
| D2 | `PackageFeature.cs` | Entity جدید + EF Config | 🔴 | |
|
||
| D3 | `ClubMembership.cs` | حذف `ActivatedAt` → اضافه **۴ فیلد**: `FirstActivationDate`, `FirstPackageId`, `LastActivationDate`, `LastPackageId` (v5 — Q21) | 🔴 | 🔄 |
|
||
| D4 | `ClubMembershipCycle.cs` | اضافه `PackageId` | 🟡 | |
|
||
| D5 | `WeeklyCommissionPool.cs` | اضافه `PackageId` + Unique | 🔴 | |
|
||
| D6 | `UserCommissionPayout.cs` | اضافه `PackageId` + Unique(UserId,WeekId,PackageId) | 🟡 | 🔄 |
|
||
| D7 | `SystemConstants.cs` | حذف **۷ ثابت** (v2: ۵ + v3: ۲)، حفظ ۱۱ | 🟡 | 🔄 |
|
||
| D8 | EF Migration + Seed | schema + data migration | 🔴 | |
|
||
| **D9** | **`NetworkWeeklyBalance.cs`** | **اضافه `PackageId` + Unique(UserId,WeekId,PackageId)** | **🔴** | **🆕** |
|
||
| **D10** | **Data volume impact** | **رکوردهای تعادل ×N (تعداد پکیج)** | **🟡** | **🆕** |
|
||
| **D11** | **`UserWalletChangeLog.cs`** | **اضافه `PackageId` — ۲۱ محل ساخت باید بروزرسانی شوند** | **🔴** | **🆕 v4** |
|
||
|
||
### ۱۰.۲ لایه Application (۱۲ تغییر)
|
||
|
||
| # | فایل | نوع | شدت |
|
||
|---|------|-----|------|
|
||
| A1 | `ActivateClubMembershipCommandHandler` | فیچر از PackageFeature + ActivationFee + re-activate | 🔴 |
|
||
| A2 | `VerifyPackagePurchaseCommandHandler` | DiscountMultiplier + UserPackagePurchase + generic | 🔴 |
|
||
| A3 | `VerifyBasePackagePaymentCommandHandler` | DiscountMultiplier + UserPackagePurchase + generic | 🔴 |
|
||
| A4 | `VerifyGoldenPackagePurchaseCommandHandler` | فیکس DiscountBalance + UserPackagePurchase + generic | 🔴 |
|
||
| A5 | `InitiateBasePackagePaymentCommandHandler` | حذف ID=4 + generic | 🟡 |
|
||
| A6 | `PurchaseGoldenPackageCommandHandler` | حذف فیلتر "طلایی" + generic | 🟡 |
|
||
| A7 | `PurchasePackageCommandHandler` | اجازه re-purchase | 🟡 |
|
||
| A8 | `CreateManualPaymentCommandHandler` | DiscountMultiplier از Package | 🟡 |
|
||
| A9 | `CheckAndProcessDayaLoansCommandHandler` | حذف ID=4 + DiscountMultiplier | 🟡 |
|
||
| A10 | `AcceptClubMembershipContractCommandHandler` | حفظ guard (قرارداد یکبار — Q19). خرید مجدد Skip قرارداد | 🟡 |
|
||
| A11 | `UserOrderService` (EXIT Magic) | ریست PackagePurchaseMethod + Deactivate | 🔴 |
|
||
| A12 | Package CRUD handlers | فیلدهای جدید + PackageFeature CRUD | 🟡 |
|
||
| **A13** | **`ChargeMagicWalletCommandHandler`** | **`MagicWalletMaxDeposit` از Package بخوند (نه global)** | **🔴 v4** |
|
||
| **A14** | **`VerifyMagicWalletChargeCommandHandler`** | **`MagicWalletMultiplier` از Package بخوند (نه ×2.5 global)** | **🔴 v4** |
|
||
| **A15** | **`UserOrderService.cs` EXIT/ENTRY** | **EXIT: سقف از Package + ENTRY: چک کدام پکیج** | **🔴 v4** |
|
||
| **A16** | **Validators (۳ فایل)** | **`1_000_000_000` hardcoded → داینامیک per-package** | **🟡 v4** |
|
||
| **A17** | **`GetAllFeatureIds()` (۲ handler)** | **فیچر DIFF/تفاضل (Q20): مقایسه فعلی vs جدید، فقط اختلاف اعمال** | **🔴 v5** |
|
||
| **A18** | **JWT Token Generation** | **اضافه PackageId + PackageTitle + CanRepurchase** | **🟡 v4** |
|
||
| **A19** | **`WalletGrpcService.GetMagicWalletStatus`** | **سقف و باقیمانده per-package (نه global)** | **🟡 v4** |
|
||
| **A20** | **Notifications (۴ مورد)** | **اضافه PackageId + PackageTitle به interface** | **🟡 v4** |
|
||
| **A21** | **`FeatureDiffService` — جدید** | **سرویس DIFF فیچرها (Q20): مقایسه + اعمال تفاضل** | **🔴 v5** |
|
||
| **A22** | **`ActivateClubMembership` — FirstLast** | **بروزرسانی First/LastActivationDate + First/LastPackageId (Q21)** | **🟡 v5** |
|
||
|
||
### ۱۰.۳ لایه Infrastructure (۶ تغییر)
|
||
|
||
| # | فایل | نوع | شدت | v3? |
|
||
|---|------|-----|------|-----|
|
||
| I1 | `sp_CalculateWeeklyBalances` | پارامتر `@PackageId` + **`@MaxBalancesPerLeg`** + **`@MaxNetworkLevel`** (حذف hardcode) | 🔴 | 🔄 |
|
||
| I2 | `sp_CalculateWeeklyCommissionPool` | پارامتر `@PackageId` + فیلتر تعادلهای همان پکیج | 🔴 | 🔄 |
|
||
| I3 | `WeeklyCommissionCalculationService` | Loop روی پکیجها + ارسال تنظیمات هر پکیج | 🟡 | 🔄 |
|
||
| I4 | `OrmCommissionCalculationStrategy` | فیلتر PackageId + خواندن MaxBalancesPerLeg/MaxNetworkLevel از Package | 🔴 | 🔄 |
|
||
| I5 | `SpCommissionCalculationStrategy` | پاس دادن PackageId + MaxBalancesPerLeg + MaxNetworkLevel | 🟡 | 🔄 |
|
||
| **I6** | **Carryover logic** | **Week-shifting per-package: carryover فقط رکوردهای همان PackageId** | **🔴** | **🆕** |
|
||
|
||
### ۱۰.۴ لایه Proto/gRPC (۶ تغییر)
|
||
|
||
| # | فایل | نوع | شدت | v3? |
|
||
|---|------|-----|------|-----|
|
||
| P1 | `package.proto` | فیلدهای جدید Package (۱۱ فیلد) | 🟡 | 🔄 |
|
||
| P2 | `clubmembership.proto` | `package_id` در request/response | 🟡 | |
|
||
| P3 | `commission.proto` | **`package_id` + `package_title`** در ۴ message: UserWeeklyBalance, UserCommissionPayout, CustomerCommissionPayout, CustomerWeeklyBalance | **🔴** | **🔄** |
|
||
| P4 | `PackageGrpcService.cs` | Generic purchase + CRUD | 🟡 | |
|
||
| **P5** | **`commission.proto`** | **Message جدید: `CustomerCommissionPackageSummary`** (خلاصه per-package) | **🟡** | **🆕** |
|
||
| **P6** | **`commission.proto`** | **فیلتر اختیاری `package_id` در Requestهای** GetWeeklyBalances + GetPayouts | **🟡** | **🆕** |
|
||
|
||
### ۱۰.۵ لایه FrontOffice (۸ تغییر)
|
||
|
||
| # | فایل | نوع | شدت | v3? |
|
||
|---|------|-----|------|-----|
|
||
| F1 | `Packages.razor` | کاشیهای پکیج از API | 🔴 | |
|
||
| F2 | `PackageDetail.razor` | فیچرها از PackageFeature | 🟡 | |
|
||
| F3 | `ActivationSection.razor` | حذف hardcoded 56M | 🟡 | |
|
||
| F4 | `ClubMembershipContractDialog.razor` | متن قرارداد داینامیک | 🟡 | |
|
||
| F5 | `MyPackages.razor` | نمایش نوع پکیج + re-purchase | 🟡 | |
|
||
| F6 | `PackageService.cs` | فیکس stub GetPurchaseHistory | 🟡 | |
|
||
| **F7** | **`CommissionDashboardPage.razor`** | **خلاصه پاداش per-package (کارتهای جداگانه هر پکیج)** | **🔴** | **🆕** |
|
||
| **F8** | **`WeeklyBalancePage.razor`** | **تعادلها per-package (تیم اول/دوم بر اساس پکیج)** | **🟡** | **🆕** |
|
||
| **F9** | **`ClubMembershipContractDialog.razor`** | **متن حقوقی "۵۶ میلیون تومان" — مسئولیت حقوقی!** | **🔴** | **🆕 v4** |
|
||
| **F10** | **`MagicWalletChargePage.razor`** | **×۲.۵ و سقف واریز hardcoded در ۶ جا (متن + کد C#)** | **🔴** | **🆕 v4** |
|
||
| **F11** | **صفحات متعدد** | **"پکیج طلایی" hardcoded (۵+ جا) — نام اشتباه** | **🟡** | **🆕 v4** |
|
||
| **F12** | **Activation flow** | **`PackageId = 1` hardcoded** | **🔴** | **🆕 v4** |
|
||
|
||
### ۱۰.۶ لایه BackOffice (۸ تغییر)
|
||
|
||
| # | فایل | نوع | شدت | v3? |
|
||
|---|------|-----|------|-----|
|
||
| BO1 | `PackageCreateDialog.razor` | فیلدهای جدید (**۱۱ فیلد** شامل MaxBalancesPerLeg, MaxNetworkLevel, MagicWallet limits) | 🟡 | 🔄 |
|
||
| BO2 | `PackageEditDialog.razor` | فیلدهای جدید + **Quick Access فیچرها** (checkbox فیچرها در همان فرم) | 🟡 | 🔄 |
|
||
| BO3 | `PackageFeatureMatrixPage` — **جدید** | ماتریس پکیج×فیچر | 🔴 | |
|
||
| BO4 | `ActivateClubDialog.razor` | dropdown انتخاب پکیج | 🟡 | |
|
||
| **BO5** | **`Dashboard.razor` (Commission)** | **فیلتر dropdown پکیج + خلاصه per-package** | **🟡** | **🆕** |
|
||
| **BO6** | **`WeeklyReportsPage.razor`** | **فیلتر پکیج + CSV export per-package** | **🟡** | **🆕** |
|
||
| **BO7** | **`BalancesReportPage.razor`** | **فیلتر پکیج + ستون پکیج در جدول تعادلها** | **🟡** | **🆕** |
|
||
| **BO8** | **`UserPayoutsPage.razor`** | **ستون پکیج در لیست پرداختها + فیلتر** | **🟡** | **🆕** |
|
||
| **BO9** | **`ManualActivationDialog.razor`** | **مبلغ ۵۶M hardcoded + Disabled + بدون package selector** | **🔴** | **🆕 v4** |
|
||
| **BO10** | **`SystemConfigurationPage.razor`** | **تنظیمات global — باید per-package بشن** | **🔴** | **🆕 v4** |
|
||
| **BO11** | **CSV exports (۳ جا)** | **بدون ستون پکیج** | **🟡** | **🆕 v4** |
|
||
|
||
---
|
||
|
||
## ۱۱. فازبندی پیادهسازی
|
||
|
||
### فاز ۰ — فیکس باگهای فوری ≈ ۱ روز
|
||
|
||
| تسک | شرح |
|
||
|-----|------|
|
||
| **T0.1** | فیکس `VerifyGoldenPackagePurchase` — اضافه DiscountBalance (`Amount × 2`) |
|
||
| **T0.2** | فیکس `VerifyGoldenPackagePurchase` — ساخت `UserPackagePurchase` |
|
||
| **T0.3** | فیکس `VerifyPackagePurchase` — ساخت `UserPackagePurchase` |
|
||
| **T0.4** | فیکس `VerifyBasePackagePayment` — ساخت `UserPackagePurchase` |
|
||
|
||
### فاز ۱ — زیرساخت (Domain + DB) ≈ ۳ روز
|
||
|
||
| تسک | شرح | v3? |
|
||
|-----|----- |-----|
|
||
| **T1.1** | بروزرسانی `Package` entity (**۱۱ فیلد** جدید: v2 ۷ + v3 ۴ شامل MaxBalancesPerLeg, MaxNetworkLevel, MagicWalletMaxDeposit, MagicWalletMaxCredit) | 🔄 |
|
||
| **T1.2** | ایجاد `PackageFeature` entity + EF Config | |
|
||
| **T1.3** | حذف `ActivatedAt` → اضافه **۴ فیلد**: `FirstActivationDate`, `FirstPackageId`, `LastActivationDate`, `LastPackageId` (Q21) | 🔄 v5 |
|
||
| **T1.4** | اضافه `PackageId` به `ClubMembershipCycle` | |
|
||
| **T1.5** | اضافه `PackageId` به `WeeklyCommissionPool` + Unique(WeekId,PackageId) | |
|
||
| **T1.6** | اضافه `PackageId` به `UserCommissionPayout` + Unique(UserId,WeekId,PackageId) | 🔄 |
|
||
| **T1.7** | حذف **۷ ثابت** از `SystemConstants` (v2: ۵ + v3: MaxWeeklyBalancesPerLeg, MaxNetworkLevel) | 🔄 |
|
||
| **T1.8** | Database Migration + Seed Data (۲ پکیج + فیچرها) | |
|
||
| **T1.9** | Data Migration: کاربران فعلی → PackageId = پکیج پایه | |
|
||
| **T1.10** | بروزرسانی Protoها (package + clubmembership + commission) | 🔄 |
|
||
| **T1.11** | **اضافه `PackageId` به `NetworkWeeklyBalance` + Unique(UserId,WeekId,PackageId)** | **🆕** |
|
||
| **T1.12** | **Data Migration: `NetworkWeeklyBalance` موجود → PackageId = پکیج پایه** | **🆕** |
|
||
| **T1.13** | **اضافه `PackageId` به `UserWalletChangeLog` + Migration داده** | **🆕 v4** |
|
||
|
||
### فاز ۲ — منطق کسبوکار ≈ ۵ روز (v4: +۱)
|
||
|
||
| تسک | شرح | v4? |
|
||
|-----|----- |-----|
|
||
| **T2.1** | ادغام Verify handlers → Generic (DiscountMultiplier + UserPackagePurchase) | |
|
||
| **T2.2** | ادغام Purchase handlers → Generic (حذف "طلایی"، حذف ID=4) | |
|
||
| **T2.3** | بروزرسانی `ActivateClubMembership` — فیچر DIFF از PackageFeature (Q20) + First/LastActivation (Q21) | 🔄 v5 |
|
||
| **T2.4** | بروزرسانی `ActivateClubMembership` — ActivationFee از Package | |
|
||
| **T2.5** | اجازه re-purchase در Guards (G1–G3) | |
|
||
| **T2.6** | ریست وضعیت در EXIT Magic Mode (بدون `IsActive=false` — Q19) | |
|
||
| **T2.7** | حفظ guard قرارداد (Q19) + ایجاد `FeatureDiffService` (Q20) + بروزرسانی JWT (G7) | 🔄 v5 |
|
||
| **T2.8** | بروزرسانی `CreateManualPayment` + `DayaLoan` | |
|
||
| **T2.9** | PackageFeature CRUD | |
|
||
| **T2.10** | Event: PackageCreated → ساخت Pool خالی | |
|
||
| **T2.11** | **Magic Wallet: `ChargeMagicWallet` + `VerifyMagicWalletCharge` + `UserOrderService` EXIT/ENTRY → خواندن سقف/ضریب از Package** | **🆕 v4** |
|
||
| **T2.12** | **Validators (۳ فایل): حذف hardcoded 1B → داینامیک** | **🆕 v4** |
|
||
| **T2.13** | **JWT Claims: اضافه PackageId + PackageTitle + CanRepurchase، حذف HasPurchasedGoldenPackage** | **🆕 v4** |
|
||
| **T2.14** | **`WalletGrpcService.GetMagicWalletStatus`: سقف per-package** | **🆕 v4** |
|
||
| **T2.15** | **Notifications (۴ مورد): اضافه PackageId به interface + پیام** | **🆕 v4** |
|
||
| **T2.16** | **بروزرسانی ۲۱ محل ساخت WalletChangeLog با PackageId** | **🆕 v4** |
|
||
| **T2.17** | **ایجاد `FeatureDiffService` — سرویس DIFF فیچرها (Q20) + استفاده در Activate + AcceptContract** | **🆕 v5** |
|
||
| **T2.18** | **بروزرسانی فلوی خرید مجدد — Skip قرارداد (Q19) + فقط FeatureDiff + شارژ wallet** | **🆕 v5** |
|
||
|
||
### فاز ۳ — محاسبه پورسانت ≈ ۴ روز (موازی با فاز ۲)
|
||
|
||
| تسک | شرح | v3? |
|
||
|-----|----- |-----|
|
||
| **T3.1** | بروزرسانی `sp_CalculateWeeklyBalances` — `@PackageId` + **`@MaxBalancesPerLeg`** + **`@MaxNetworkLevel`** (حذف hardcode ۳۰۰/۱۵) | 🔄 |
|
||
| **T3.2** | بروزرسانی `sp_CalculateWeeklyCommissionPool` — `@PackageId` + فیلتر تعادلهای همان پکیج | 🔄 |
|
||
| **T3.3** | بروزرسانی `WeeklyCommissionCalculationService` — Loop روی پکیجها + ارسال تنظیمات | 🔄 |
|
||
| **T3.4** | بروزرسانی `OrmCommissionCalculationStrategy` — فیلتر PackageId + خواندن Max از Package | 🔄 |
|
||
| **T3.5** | **بروزرسانی `SpCommissionCalculationStrategy` — پاس دادن PackageId + MaxBalancesPerLeg + MaxNetworkLevel** | **🆕** |
|
||
| **T3.6** | **Carryover per-package: week-shifting فقط رکوردهای همان PackageId** | **🆕** |
|
||
| **T3.7** | تست محاسبات با داده واقعی (۲ پکیج موازی، carryover مجزا) | 🔄 |
|
||
|
||
### فاز ۴ — UI ≈ ۵ روز
|
||
|
||
| تسک | شرح | v3? |
|
||
|-----|----- |-----|
|
||
| **T4.1** | FrontOffice: کاشیهای پکیج (داینامیک) — ✅ `PackagePurchaseDialog.razor` | |
|
||
| **T4.2** | FrontOffice: مدال پرداخت (دایا+مستقیم / فقط مستقیم) — ✅ Step 2 in dialog | |
|
||
| **T4.3** | FrontOffice: MyPackages — re-purchase | |
|
||
| **T4.4** | FrontOffice: ActivationSection + Contract — داینامیک | |
|
||
| **T4.5** | BackOffice: CRUD پکیج — فیلدهای جدید (۱۱ فیلد) | 🔄 |
|
||
| **T4.6** | BackOffice: ماتریس PackageFeature | |
|
||
| **T4.7** | BackOffice: ActivateClubDialog — dropdown | |
|
||
| **T4.8** | **FrontOffice: CommissionDashboard — کارتهای خلاصه per-package + مجموع پاداش** | **🆕** |
|
||
| **T4.9** | **FrontOffice: WeeklyBalance — تعادل تیم اول/دوم per-package** | **🆕** |
|
||
| **T4.10** | **BackOffice: Commission Dashboard — فیلتر dropdown پکیج** | **🆕** |
|
||
| **T4.11** | **BackOffice: Weekly Reports + CSV export — ستون پکیج + فیلتر** | **🆕** |
|
||
| **T4.12** | **BackOffice: BalancesReport + UserPayouts — ستون پکیج + فیلتر** | **🆕** |
|
||
| **T4.13** | **BackOffice: Package Create/Edit — Quick Access فیچرها (checkbox inline)** | **🆕** |
|
||
| **T4.14** | **FrontOffice: MagicWalletChargePage — حذف ×۲.۵ و سقف hardcoded (۶ جا) → خواندن از API** | **🆕 v4** |
|
||
| **T4.15** | **FrontOffice: حذف "پکیج طلایی" hardcoded (۵+ جا) + حذف PackageId=1** | **🆕 v4** |
|
||
| **T4.16** | **FrontOffice: قرارداد حقوقی — مبلغ + نام پکیج داینامیک (مسئولیت حقوقی!)** | **🆕 v4** |
|
||
| **T4.17** | **BackOffice: ManualActivationDialog — حذف 56M hardcoded + اضافه package selector** | **🆕 v4** |
|
||
| **T4.18** | **BackOffice: SystemConfiguration — تفکیک تنظیمات global/per-package** | **🆕 v4** |
|
||
|
||
### فاز ۵ — تست و استقرار ≈ ۳ روز
|
||
|
||
| تسک | شرح | v3? |
|
||
|-----|----- |-----|
|
||
| **T5.1** | تست خرید هر پکیج | |
|
||
| **T5.2** | تست re-purchase بعد تکمیل چرخه | |
|
||
| **T5.3** | تست Commission Pool جداگانه | |
|
||
| **T5.4** | تست Migration | |
|
||
| **T5.5** | **تست تعادل per-package: carryover مجزا، MaxBalancesPerLeg متفاوت** | **🆕** |
|
||
| **T5.6** | **تست گزارش FO per-package: مشتری breakdown صحیح میبیند** | **🆕** |
|
||
| **T5.7** | **تست گزارش BO per-package: فیلتر پکیج + CSV** | **🆕** |
|
||
| **T5.8** | **تست Magic Wallet: سقف متفاوت per-package (پایه=1B, نقرهای=100M) — EXIT صحیح** | **🆕 v4** |
|
||
| **T5.9** | **تست ضریب جادویی: پکیج A ×2.5 vs پکیج B ×2.0 — اعتبار صحیح** | **🆕 v4** |
|
||
| **T5.10** | **تست قرارداد حقوقی: مبلغ و نام پکیج صحیح در متن** | **🆕 v4** |
|
||
| **T5.11** | **تست WalletChangeLog: رکوردها PackageId دارند** | **🆕 v4** |
|
||
| **T5.12** | **تست قرارداد یکبار: خرید مجدد بدون OTP/قرارداد (Q19)** | **🆕 v5** |
|
||
| **T5.13** | **تست فیچر DIFF: تغییر پکیج → فیچرهای صحیح فعال/غیرفعال (Q20)** | **🆕 v5** |
|
||
| **T5.14** | **تست First/Last: FirstActivationDate حفظ + LastActivationDate بروزرسانی (Q21)** | **🆕 v5** |
|
||
| **T5.15** | **تست carryover تغییر پکیج: carryover قبلی شمرده نمیشود (Q23)** | **🆕 v5** |
|
||
| **T5.16** | Deploy staging → production |
|
||
|
||
---
|
||
|
||
## ۱۲. ریسکها (v5)
|
||
|
||
| ریسک | احتمال | شدت | راهحل |
|
||
|------|--------|-----|--------|
|
||
| Migration دادهها — PackageId اشتباه | کم | بحرانی | Verify query + بکاپ |
|
||
| SP تغییر → محاسبات اشتباه | متوسط | بحرانی | تست staging قبل production |
|
||
| ادغام handlers → رگرسیون | متوسط | زیاد | E2E test |
|
||
| خرید مجدد بدون تکمیل چرخه | کم | زیاد | Validation: چرخه قبلی MagicCompletedAt |
|
||
| Proto breaking change | قطعی | کم | backward compatible fields |
|
||
| **Magic Wallet EXIT اشتباه — کاربر گیر میافته** | **زیاد** | **بحرانی** | **اولویت P0 — سقف از Package خوانده شود** |
|
||
| **قرارداد حقوقی با مبلغ اشتباه** | **زیاد** | **بحرانی** | **متن قرارداد داینامیک از Package** |
|
||
| **۲۱ WalletChangeLog بدون ردیابی** | **قطعی** | **متوسط** | **اضافه PackageId به entity** |
|
||
| **فیچر DIFF — حذف فیچر فعال (v5)** | **متوسط** | **بالا** | **کاربر اگر فیچر فعالی حذف بشه، باید اطلاعرسانی بشه** |
|
||
| **Migration ActivatedAt → First/Last (v5)** | **کم** | **بحرانی** | **هر دو فیلد = ActivatedAt فعلی، بعداً Last بروزرسانی** |
|
||
| **ناسازگاری AcceptContract vs Activate (v5)** | **زیاد** | **بالا** | **AcceptContract: overwrite ActivatedAt / Activate: حفظ — باید یکسان شوند** |
|
||
|
||
---
|
||
|
||
## ۱۳. تخمین زمانی (v5)
|
||
|
||
| فاز | مدت | وابستگی | v5 تغییر |
|
||
|-----|------|---------|----------|
|
||
| فاز ۰ — فیکس باگها | ۱ روز | — | |
|
||
| فاز ۱ — زیرساخت | **۵ روز** | فاز ۰ | ClubMembership: ۴ فیلد First/Last (Q21) + Migration ActivatedAt |
|
||
| فاز ۲ — منطق | **۶ روز** | فاز ۱ | +۱ (FeatureDiffService + Skip قرارداد + First/Last Activation) |
|
||
| فاز ۳ — پورسانت | ۴ روز | فاز ۱ | LastActivationDate برای تشخیص هفتگی (Q22) |
|
||
| فاز ۴ — UI | **۷ روز** | فاز ۲ | |
|
||
| فاز ۵ — تست | **۵ روز** | فاز ۳, ۴ | +۱ (تست قرارداد یکبار + فیچر DIFF + First/Last + carryover تغییر پکیج) |
|
||
| **مجموع** | **~۲۸ روز** | | |
|
||
|
||
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۲۴ روز**
|
||
> نسبت به v4 (**۲۲ روز**): **+۲ روز** بخاطر FeatureDiffService + قرارداد یکبار + First/Last Activation
|
||
> نسبت به v3 (**۱۷ روز**): **+۷ روز** — بزرگترین سهم: Magic Wallet + WalletChangeLog + فیچر DIFF + First/Last
|
||
|
||
---
|
||
|
||
## ۱۴. تصمیمات جدید v6 (Q24–Q30)
|
||
|
||
> **تاریخ:** ۸ اسفند ۱۴۰۴
|
||
> **زمینه:** مرور نهایی قبل از تست و استقرار — ۷ نکته جدید شناسایی شد
|
||
|
||
### Q24 — آستانه موجودی برای ورود به Magic و خرید مجدد 🔴
|
||
|
||
> **مشکل:** شرط فعلی `wallet.Balance == 0` عملاً غیرممکنه — چون قیمت محصولات متفاوته (مثلاً ۳۵۰,۰۰۰ تومان، ۱,۲۰۰,۰۰۰ تومان، ...) کاربر هیچوقت نمیتونه دقیقاً به صفر برسه.
|
||
|
||
**تصمیم:** آستانه ثابت **۱,۰۰۰,۰۰۰ ریال (۱۰۰,۰۰۰ تومان)** — هم برای ورود به Magic، هم برای تشخیص تکمیل چرخه.
|
||
|
||
**مکانهای تاثیر:**
|
||
|
||
| # | فایل | شرط فعلی | شرط جدید |
|
||
|---|------|----------|----------|
|
||
| 1 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — ورود به Magic | `if (wallet.Balance <= MagicWalletEntryThreshold)` |
|
||
| 2 | `UserOrderService.cs` L342 | `if (wallet.Balance == 0)` — EXIT Magic check | `if (wallet.Balance <= MagicWalletEntryThreshold)` |
|
||
| 3 | خرید مجدد guard | `PackagePurchaseMethod == None` | + `Balance <= Threshold` |
|
||
|
||
**تعریف ثابت:**
|
||
```csharp
|
||
// SystemConstants.cs یا Package entity
|
||
public const long MagicWalletEntryThreshold = 1_000_000; // 100,000 تومان = 1,000,000 ریال
|
||
```
|
||
|
||
**فلوی جدید:**
|
||
```
|
||
کاربر خرید میکند → Balance = 850,000 ریال (کمتر از 1M)
|
||
→ سیستم: Balance <= 1,000,000 ✅ → ورود به Magic Mode
|
||
→ قبلاً: Balance == 0 ❌ → کاربر گیر میافتاد!
|
||
|
||
Magic تکمیل → Balance = 200,000 ریال
|
||
→ سیستم: Balance <= 1,000,000 ✅ → EXIT Magic → مجاز به خرید مجدد
|
||
→ قبلاً: Balance == 0 ❌ → کاربر تا ابد در Magic!
|
||
```
|
||
|
||
> ⚠️ **نکته:** این آستانه **ثابت (global)** تعریف میشه — نه per-package. چون مربوط به قیمت محصولات فروشگاهه، نه پکیج.
|
||
|
||
---
|
||
|
||
### Q25 — DayaLoans فقط پکیج پایه
|
||
|
||
> **تایید:** وام دایا فعلاً فقط برای پکیج پایه فعاله و تغییر نمیکنه.
|
||
|
||
**وضعیت فعلی:**
|
||
- فیلد `Package.SupportsDayaPurchase` روی پکیج پایه = `true`، نقرهای = `false`
|
||
- `CheckAndProcessDayaLoansCommandHandler` فقط پکیجهایی که `SupportsDayaPurchase = true` دارن رو بررسی میکنه
|
||
- این یک تصمیم بیزینسی ثابته — اگر در آینده تغییر کرد، ادمین از BackOffice فلگ رو تغییر میده
|
||
|
||
---
|
||
|
||
### Q26 — SP Worker (مدیریت خودکار Stored Procedures) 🔴
|
||
|
||
> **مشکل:** الان SPها **کاملاً دستی** deploy میشن. اگه SP تغییر کنه، باید یادمون باشه دستی اجرا کنیم.
|
||
|
||
**تصمیم:** یک `IHostedService` به نام `StoredProcedureDeploymentService` ایجاد بشه.
|
||
|
||
**الگوریتم:**
|
||
```
|
||
1. Startup → خواندن فایلهای .sql از Embedded Resources
|
||
2. برای هر فایل:
|
||
a. محاسبه SHA256 checksum محتوا
|
||
b. خواندن checksum قبلی از جدول "CMS"."StoredProcedureVersions"
|
||
c. اگر وجود نداشت یا checksum فرق داشت:
|
||
→ اجرای CREATE OR ALTER PROCEDURE
|
||
→ ذخیره checksum جدید
|
||
→ لاگ: "SP {name} deployed/updated"
|
||
d. اگر checksum برابر بود:
|
||
→ لاگ: "SP {name} is up-to-date, skipping"
|
||
3. اجرا بعد از EF Migration (ترتیب startup)
|
||
```
|
||
|
||
**جدول جدید:**
|
||
```sql
|
||
CREATE TABLE "CMS"."StoredProcedureVersions" (
|
||
"Id" BIGSERIAL PRIMARY KEY,
|
||
"Name" VARCHAR(256) NOT NULL UNIQUE, -- نام SP
|
||
"Checksum" VARCHAR(64) NOT NULL, -- SHA256 hash
|
||
"DeployedAt" TIMESTAMP NOT NULL, -- آخرین deploy
|
||
"Version" INT NOT NULL DEFAULT 1 -- شمارنده نسخه
|
||
);
|
||
```
|
||
|
||
**فایلهای SP فعلی (embed شوند):**
|
||
|
||
| # | فایل | خطوط |
|
||
|---|------|------|
|
||
| 1 | `sp_CalculateWeeklyBalances.sql` | ۳۷۲ |
|
||
| 2 | `sp_CalculateWeeklyCommissionPool.sql` | ۲۶۷ |
|
||
|
||
**csproj تغییرات:**
|
||
```xml
|
||
<ItemGroup>
|
||
<EmbeddedResource Include="Persistence\StoredProcedures\*.sql" />
|
||
</ItemGroup>
|
||
```
|
||
|
||
---
|
||
|
||
### Q27 — History Tables — یکسانسازی و ثبت خودکار 🟡
|
||
|
||
> **مشکل:** نامگذاری history tableها ناسازگاره و ثبت تاریخچه دستیه.
|
||
|
||
**وضعیت فعلی:**
|
||
|
||
| Master Entity | History Entity | نامگذاری |
|
||
|---------------|---------------|----------|
|
||
| `UserCommissionPayout` | `CommissionPayoutStatusHistory` | ❌ نام متفاوت |
|
||
| `CommissionCashWithdrawal` | `CommissionCashWithdrawalStatusHistory` | ✅ مشابه |
|
||
| `DiscountOrder` | `DiscountOrderStatusHistory` | ✅ مشابه |
|
||
| `UserWallet` | `UserWalletChangeLog` | ❌ نام متفاوت |
|
||
| `Package` | ❌ ندارد | 🔴 |
|
||
| `ClubMembership` | ❌ ندارد | 🔴 |
|
||
| `ClubMembershipCycle` | ❌ ندارد | 🔴 |
|
||
|
||
**تصمیم — ۲ بخش:**
|
||
|
||
**بخش ۱: نامگذاری یکسان**
|
||
```
|
||
الگو: {MasterEntityName}History
|
||
|
||
مثال:
|
||
UserCommissionPayout → UserCommissionPayoutHistory (rename از CommissionPayoutStatusHistory)
|
||
UserWallet → UserWalletHistory (rename از UserWalletChangeLog)
|
||
Package → PackageHistory (جدید)
|
||
ClubMembership → ClubMembershipHistory (جدید)
|
||
ClubMembershipCycle → ClubMembershipCycleHistory (جدید)
|
||
```
|
||
|
||
**بخش ۲: ثبت خودکار تغییرات**
|
||
```
|
||
دو رویکرد:
|
||
|
||
A) EF Interceptor (پیشنهادی):
|
||
→ در SaveChangesInterceptor، entityهای تغییریافته شناسایی
|
||
→ برای هر entity که IHasHistory پیاده کرده:
|
||
snapshot فعلی → رکورد History جدید
|
||
→ مزیت: یکجا و خودکار
|
||
|
||
B) Domain Events:
|
||
→ هر entity یک EntityChangedEvent publish کنه
|
||
→ Handler مربوطه History ثبت کنه
|
||
→ مزیت: async + decoupled
|
||
```
|
||
|
||
**Interface پیشنهادی:**
|
||
```csharp
|
||
public interface IHasHistory<THistory> where THistory : BaseHistoryEntity
|
||
{
|
||
THistory CreateHistorySnapshot(string action, long? changedBy);
|
||
}
|
||
|
||
public abstract class BaseHistoryEntity : BaseEntity
|
||
{
|
||
public long MasterEntityId { get; set; } // FK به entity اصلی
|
||
public string Action { get; set; } // Created, Updated, StatusChanged, ...
|
||
public long? ChangedBy { get; set; } // UserId تغییردهنده
|
||
public DateTime ChangedAt { get; set; } // زمان تغییر
|
||
public string? Snapshot { get; set; } // JSON snapshot (اختیاری)
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### Q28 — UI Guidance (آموزش و هشدار در FO/BO) 🟡
|
||
|
||
> **تصمیم:** در سراسر FrontOffice و BackOffice، متنهای آموزشی و هشداری اضافه بشه.
|
||
|
||
**انواع Guidance:**
|
||
|
||
| نوع | جایگاه | مثال |
|
||
|-----|--------|------|
|
||
| **مودال آموزشی** | اولین بار باز کردن صفحه | «سیستم پکیجبیس: شما میتوانید بعد از تکمیل چرخه، پکیج جدید بخرید» |
|
||
| **متن inline** | کنار فیلد/آیتم | «💡 هزینه فعالسازی از قیمت پکیج محاسبه میشود» |
|
||
| **هشدار** | قبل از اقدام بحرانی | «⚠️ با تغییر پکیج، فیچرهای قبلی ممکنه غیرفعال بشن» |
|
||
| **Tooltip** | روی آیکون اطلاعات | «این مبلغ بر اساس پکیج فعال شما محاسبه شده» |
|
||
|
||
**صفحات هدف FrontOffice:**
|
||
|
||
| # | صفحه | نوع | محتوا |
|
||
|---|-------|-----|-------|
|
||
| G1 | صفحه پکیجها | مودال (اولین بار) | توضیح سیستم پکیجبیس، تفاوت پکیجها، نحوه خرید |
|
||
| G2 | Checkout | متن inline | «این پکیج {features} را شامل میشود» |
|
||
| G3 | MyPackages | متن inline | «بعد تکمیل چرخه جادویی، میتوانید پکیج جدید بخرید» |
|
||
| G4 | Magic Wallet | هشدار مودال | «سقف واریز و ضریب بر اساس پکیج شماست» |
|
||
| G5 | Commission Dashboard | tooltip | «پاداش بر اساس هر پکیج جداگانه محاسبه میشود» |
|
||
| G6 | قرارداد باشگاه | متن inline | «این قرارداد فقط یکبار امضا میشود» |
|
||
| G7 | ActivationSection | متن inline | «هزینه تخمینی بر اساس پکیج {name} محاسبه شده» |
|
||
|
||
**صفحات هدف BackOffice:**
|
||
|
||
| # | صفحه | نوع | محتوا |
|
||
|---|-------|-----|-------|
|
||
| G8 | Package CRUD | متن inline | «تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید» |
|
||
| G9 | Feature Matrix | tooltip | «فیچرهای تیکخورده برای خریداران این پکیج فعال میشود» |
|
||
| G10 | Manual Payment | هشدار | «مبلغ پرداخت باید مطابق قیمت پکیج انتخابی باشد» |
|
||
| G11 | Commission Reports | متن inline | «هر پکیج Pool پورسانت مستقل دارد» |
|
||
| G12 | User Detail | متن inline | «پکیج فعلی: {name} — چرخه: {n}» |
|
||
| G13 | Club Members | tooltip | «فیلتر بر اساس آخرین پکیج خریداریشده» |
|
||
|
||
**پیادهسازی فنی:**
|
||
```razor
|
||
@* FrontOffice — کامپوننت Guidance قابل استفاده مجدد *@
|
||
<PackageGuidance
|
||
Page="Packages"
|
||
Type="Modal"
|
||
ShowOnce="true"
|
||
Title="سیستم پکیجبیس"
|
||
Content="@_guidanceContent" />
|
||
|
||
@* BackOffice — متن inline *@
|
||
<MudAlert Severity="Severity.Info" Dense="true" Class="mb-2">
|
||
💡 تغییر قیمت روی کاربران فعلی اثر ندارد — فقط خریدهای جدید
|
||
</MudAlert>
|
||
```
|
||
|
||
---
|
||
|
||
### Q29 — شرط EXIT Magic — تایید: آخرین پکیج فعال
|
||
|
||
> **تایید:** شرط EXIT از `ClubMembershipCycle` فعلی (`IsCurrentCycle=true`) → `PackageId` → `Package.MagicWalletMaxDeposit` خوانده میشه.
|
||
|
||
**فلوی دقیق:**
|
||
```
|
||
1. UserOrderService.ProcessOrderAsync → wallet.Balance کم میشه
|
||
2. if (Balance <= Threshold): ← Q24 آستانه جدید
|
||
a. اگر Normal + خرید کرده + باشگاه فعال → ENTER Magic
|
||
b. اگر Magic:
|
||
→ خواندن چرخه فعلی → PackageId → Package.MagicWalletMaxDeposit
|
||
→ if (MagicTotalDeposited >= MaxDeposit) → EXIT Magic
|
||
→ else: هنوز Magic (میتونه شارژ کنه)
|
||
```
|
||
|
||
> **Fallback:** اگه چرخه فعالی نبود → `IsBasePackage = true` (پکیج پایه)
|
||
|
||
---
|
||
|
||
### Q30 — Carryover — توضیح ساده (v6 اصلاحشده)
|
||
|
||
> **Carryover = باقیمانده تعادل از هفته قبل — بر اساس پکیج زیرمجموعهها**
|
||
|
||
**مثال ساده:**
|
||
```
|
||
علی بالاسری — زیرمجموعههاش پکیج پایه دارن:
|
||
|
||
هفته ۱۰:
|
||
تیم چپ (پکیج پایه) = ۵۰ نفر، تیم راست (پکیج پایه) = ۳۰ نفر
|
||
تعادل_پایه = MIN(50, 30) = 30
|
||
باقیمانده_پایه: {چپ: 20, راست: 0} ← CARRYOVER
|
||
|
||
هفته ۱۱:
|
||
اعضای جدید (پکیج پایه): چپ = 5، راست = 10
|
||
جمع با carryover: چپ = 5+20 = 25، راست = 10+0 = 10
|
||
تعادل_پایه = MIN(25, 10) = 10
|
||
باقیمانده_پایه: {چپ: 15, راست: 0} ← CARRYOVER بعدی
|
||
```
|
||
|
||
**تغییر پکیج خود کاربر — تاثیری ندارد (Q23 اصلاحی):**
|
||
```
|
||
علی پکیج خودش رو عوض میکنه (پایه → نقرهای):
|
||
|
||
carryover_پایه = {چپ: 20, راست: 0} ← هنوز هست! حذف نمیشه!
|
||
carryover_نقرهای = {چپ: 0, راست: 0}
|
||
|
||
✅ هفته بعد، علی هنوز از Pool پایه سود میبره
|
||
(چون زیرمجموعههایی با پکیج پایه داره)
|
||
✅ همزمان از Pool نقرهای هم سود میبره
|
||
(چون زیرمجموعههایی با پکیج نقرهای هم داره)
|
||
|
||
پکیج خود علی فقط تعیین میکنه:
|
||
→ ActivationFeeاش به کدوم Pool بره
|
||
→ MagicWallet سقفش چقدره
|
||
```
|