Files
docs/business/BIZ-PACKAGE-BASED-SYSTEM.md
T
masoodafar-web 1885fcbd3b docs: BIZ-PACKAGE-BASED-SYSTEM v4 — comprehensive side-effect discovery
44 NEW side effects discovered across 6 layers (total: 92 changes):

Side Effects — CMS Domain (3):

Side Effects — CMS Application (10):
- ChargeMagicWalletCommandHandler: global MagicWalletMaxDeposit (1B)
- VerifyMagicWalletChargeCommandHandler: global multiplier ×2.5
- UserOrderService EXIT/ENTRY: global caps → user trapped/ejected wrong
- 3 FluentValidation validators: hardcoded 1B ceiling
- GetAllFeatureIds(): ALL features granted regardless of package
- JWT: no PackageId/CanRepurchase, just boolean HasPurchased
- WalletGrpcService.GetMagicWalletStatus: global caps to frontend
- 4 Notifications: no PackageId in interface

Side Effects — Background (3):
- ClubMembershipCycleSeedService: seeds with hardcoded amounts
- DayaLoanStatusCheckWorker: global DayaLoanAmount
- ChatikaAccountActivationWorker: no package filter

Side Effects — FrontOffice (9):
- Contract text '56M toman' = LEGAL LIABILITY
- Magic wallet ×2.5 and deposit cap hardcoded (6 places + C# code)
- 'پکیج طلایی' hardcoded (5+ places) — wrong name
- PackageId=1 hardcoded in activation flow

Side Effects — BackOffice (8):
- ManualActivationDialog: 56M hardcoded + disabled + no package selector
- SystemConfigurationPage: global settings need per-package
- CSV exports (3 places): no package column

Impact: 48 core changes + 44 side effects = 92 total
Timeline: v3 17 days → v4 22 days critical path (+5 days)
2026-02-25 00:27:14 +03:30

62 KiB
Raw Blame History

📦 سیستم مبتنی بر پکیج (Package-Based System)

وضعیت: تایید‌شده — آماده پیاده‌سازی
تاریخ بروزرسانی: ۶ اسفند ۱۴۰۴
نسخه: v4 (تحلیل جامع سایدافکت‌ها — ۴۴ مورد جدید)
تاثیرگذاری: زیاد — ۹۰+ تغییر در ۶ لایه (۴۸ اصلی + ۴۴ سایدافکت)


۱. خلاصه فیچر

وضعیت فعلی: سیستم فقط یک پکیج پایه (۵۶ میلیون تومان) دارد و همه چیز حول آن 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 فیلتر بر اساس پکیج — ادمین بر اساس پکیج فیلتر کند

۳. تحلیل عمیق وضعیت فعلی (AS-IS)

۳.۱ باگ‌های کشف‌شده

# باگ شدت فایل
B1 VerifyGoldenPackagePurchaseDiscountBalance شارژ نمی‌شود 🔴 بحرانی VerifyGoldenPackagePurchaseCommandHandler.cs
B2 VerifyGoldenPackagePurchaseUserPackagePurchase ساخته نمی‌شود 🔴 بحرانی VerifyGoldenPackagePurchaseCommandHandler.cs
B3 VerifyPackagePurchaseUserPackagePurchase ساخته نمی‌شود 🔴 بحرانی VerifyPackagePurchaseCommandHandler.cs
B4 VerifyBasePackagePaymentUserPackagePurchase ساخته نمی‌شود 🔴 بحرانی VerifyBasePackagePaymentCommandHandler.cs
B5 PackageService.CustomerPurchasePackageguard برای خرید تکراری ندارد 🟡 متوسط 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 مسدود اجازه re-contract
G6 ActivateClubMembershipCommandHandler IsActive == true → return true short-circuit باید چرخه جدید بسازه
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: membership.IsActive = false          ← Guards G5-G6 مسدود می‌مانند
    ❌ MISSING: JWT CanRepurchase = true              ← UI دکمه خرید نشان نمی‌دهد

راه‌حل: در EXIT Magic Mode، وضعیت کاربر ریست شود تا بتواند پکیج جدید بخرد.


۴. طراحی نهایی (TO-BE)

۴.۱ تغییرات Entity — Package

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

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 long? PackageId + FK آخرین پکیج خریداری‌شده
ClubMembershipCycle long PackageId + FK پکیج این چرخه
WeeklyCommissionPool long PackageId + FK Pool جداگانه هر پکیج
UserCommissionPayout long PackageId + FK از کدام Pool
NetworkWeeklyBalance long PackageId + FK ← v3 هر رکورد تعادل = per-package

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)

۵.۱ چرخه حیات کامل

stateDiagram-v2
    [*] --> NoPurchase: کاربر ثبت‌نام کرده
    
    NoPurchase --> PackagePurchased: خرید پکیج\n(هر پکیجی)
    
    PackagePurchased --> ClubActivated: فعالسازی باشگاه\n(OTP + قرارداد)
    
    ClubActivated --> Shopping: خرج Balance\nدر فروشگاه
    
    Shopping --> MagicMode: Balance == 0
    
    MagicMode --> MagicCharging: شارژ + خرج\n(تا سقف 1B)
    
    MagicCharging --> MagicMode: ادامه
    
    MagicMode --> CycleComplete: Balance == 0\nAND Deposit ≥ 1B
    
    CycleComplete --> NoPurchase: ریست وضعیت\nآماده خرید مجدد

۵.۲ ریست وضعیت بعد تکمیل چرخه (EXIT Magic Mode)

// UserOrderService.cs — EXIT Magic Mode — تغییرات لازم:
wallet.WalletMode = WalletMode.Normal;
wallet.MagicCompletedAt = DateTime.UtcNow;
cycle.MagicCompletedAt = DateTime.UtcNow;

// ✅ اضافه شود:
user.PackagePurchaseMethod = PackagePurchaseMethod.None;   // اجازه خرید مجدد
membership.IsActive = false;                                // اجازه re-contract
cycle.IsCurrentCycle = false;                               // آماده چرخه جدید

۵.۳ نکات مهم

  1. دایا فقط دور اول — بعد از دور اول، فقط IPG مجاز
  2. هر خرید = Commission contribution — ActivationFee به Pool آن پکیج
  3. PackagePurchaseMethod ریست بعد تکمیل چرخه
  4. فیچرها بر اساس پکیج جدید — ممکنه متفاوت باشه

۶. Commission Pool — تغییرات

۶.۱ ساختار جدید

هفته ۱:
    Pool_نقره‌ای (PackageId=X): TotalAmount = Σ ActivationFee نقره‌ای
    Pool_پایه   (PackageId=Y): TotalAmount = Σ ActivationFee پایه
    
هر Pool مستقل:
    ValuePerBalance = TotalPoolAmount ÷ TotalBalances
    (فقط کاربران همان پکیج)

۶.۲ ساختار شبکه یکی‌ست

                [Ali - پایه]
               /             \
        [Sara - نقره‌ای]    [Reza - پایه]
        
Commission:
  Ali → پاداش از Pool_پایه
  Sara → پاداش از Pool_نقره‌ای  
  Reza → پاداش از Pool_پایه

۶.۳ فلوی کامل چرخه خرید مجدد و تاثیر بر پورسانت (v3)

چرخه ۱ — کاربر "علی" پکیج پایه می‌خرد (۵۶M):
────────────────────────────────────────────────
    ✅ Balance += 56M
    ✅ DiscountBalance += 112M (×2)
    ✅ ActivationFee → Pool_پایه هفته جاری
    ✅ فیچرهای پکیج پایه فعال
    ✅ NetworkWeeklyBalance ساخته می‌شود (PackageId=پایه)
    ✅ بالاسری‌ها: تعادل‌ها بر اساس MaxBalancesPerLeg=300 + MaxNetworkLevel=15
    ✅ پورسانت بالاسری‌ها از Pool_پایه

    ... خرید از فروشگاه → Balance صفر شد → Magic Mode فعال ...
    ... خرید جادویی → MagicBalance صفر + Deposit ≥ MaxDeposit → EXIT Magic ...

چرخه ۲ — کاربر "علی" دوباره پکیج پایه می‌خرد:
──────────────────────────────────────────────
    ✅ ریست وضعیت: PackagePurchaseMethod=None, membership.IsActive=false
    ✅ Balance += 56M (مجدد شارژ)
    ✅ DiscountBalance += 112M (مجدد شارژ)
    ✅ ActivationFee → Pool_پایه هفته جاری
    ✅ فیچرهای پکیج پایه فعال (مجدد)
    ✅ NetworkWeeklyBalance جدید (PackageId=پایه, WeekId=هفته جاری)
    ✅ carryover از هفته قبل: فقط carryover پکیج پایه (نه نقره‌ای!)
    ✅ بالاسری‌ها: محاسبه مجدد از Pool_پایه
    
    ... همان چرخه Magic Wallet تکرار ...

چرخه ۵ — کاربر "علی" پکیج نقره‌ای می‌خرد (۵.۶M):
────────────────────────────────────────────────────
    ✅ Balance += 5.6M
    ✅ DiscountBalance += 11.2M (×2)
    ✅ ActivationFee → Pool_نقره‌ای هفته جاری (۲,۵۲۰,۰۰۰)
    ✅ فیچرهای پکیج نقره‌ای فعال (ممکنه کمتر از پایه باشه!)
    ✅ NetworkWeeklyBalance جدید (PackageId=نقره‌ای, WeekId=هفته جاری)
    ✅ تعادل‌ها: MaxBalancesPerLeg=30 (نه 300!) + MaxNetworkLevel=15 (از پکیج)
    ✅ carryover: فقط carryover نقره‌ای (جداگانه از پایه)
    ✅ بالاسری‌ها: محاسبه از Pool_نقره‌ای → ValuePerBalance کمتر
    ✅ پاداش بالاسری: ~۲,۵۲۰,۰۰۰ ÷ TotalBalances_نقره‌ای × BalancesEarned

۶.۴ تعادل‌ها (NetworkWeeklyBalance) — per-package (v3)

⚠️ تغییر اساسی: هر کاربر به‌ازای هر پکیج فعال یک رکورد تعادل جداگانه دارد.

قبل (تک‌پکیج):
    NetworkWeeklyBalance: [UserId, WeekId] → یک رکورد

بعد (چند‌پکیج):
    NetworkWeeklyBalance: [UserId, WeekId, PackageId] → N رکورد (N = تعداد پکیج)

الگوریتم محاسبه تعادل per-package:

برای هر پکیج فعال:
    1. واکشی کاربرانی که این پکیج را دارند (PackageId = X)
    2. carryover از هفته قبل: فقط رکوردهای PackageId = X
    3. اعضای جدید: فقط کسانی که PackageId = X خریدند + JoinedAt در بازه هفته
    4. LeftLegTotal = NewLeft + CarryoverLeft
    5. RightLegTotal = NewRight + CarryoverRight
    6. TotalBalances = MIN(Left, Right) → cap at Package.MaxBalancesPerLeg
    7. Remainder → carryover هفته بعد (فقط برای PackageId = X)
    8. SubordinateBalances: مجموع TotalBalances زیرمجموعه (تا Package.MaxNetworkLevel)

مثال عملی:

هفته ۱۰:
    علی (پکیج پایه):
        تعادل_پایه:   Left=45, Right=52, Min=45 (cap 300) → OK
        تعادل_نقره‌ای: Left=0, Right=0 (علی پکیج نقره‌ای نداره)
    
    سارا (پکیج نقره‌ای):
        تعادل_پایه:   Left=0, Right=0
        تعادل_نقره‌ای: Left=12, Right=8, Min=8 (cap 30) → OK
    
    رضا (پکیج پایه + قبلاً نقره‌ای داشته):
        تعادل_پایه:   Left=30, Right=25, Min=25 (cap 300) → OK
        تعادل_نقره‌ای: Left=2 (carryover), Right=0 (carryover) → Min=0
        
هفته ۱۱ (Shift):
    علی: carryover_پایه = {Left: surplus_left, Right: surplus_right}
    سارا: carryover_نقره‌ای = {Left: surplus_left, Right: surplus_right}
    رضا: carryover_پایه = {...}, carryover_نقره‌ای = {Left:2, Right:0}

۶.۵ تغییرات SP (v3 — بروزرسانی)

SP تغییرات v2 تغییرات اضافی v3
sp_CalculateWeeklyBalances پارامتر @PackageId @MaxBalancesPerLeg و @MaxNetworkLevel هم پارامتر شوند (نه hardcoded)
sp_CalculateWeeklyCommissionPool پارامتر @PackageId Pool فقط از تعادل‌های همان PackageId

sp_CalculateWeeklyBalances — تغییرات ساختاری:

-- قبل (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 — تغییرات:

// قبل: فقط 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)

// 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):

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

۷.۱ ساخت پکیج جدید

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 + Deactivate membership

۷٫۵ تحلیل جامع 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 پرداخت پورسانت

اقدام لازم:

// اضافه به 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 جدید:

claims.Add("PackageId", membership.PackageId?.ToString() ?? "");
claims.Add("PackageTitle", package?.Title ?? "");
claims.Add("CanRepurchase", HasCompletedMagicCycle(membership).ToString());
// حذف HasPurchasedGoldenPackage → جایگزین با PackageId > 0

۷٫۵٫۵ Club Features — تخصیص global

🔴 بحرانی — همه فیچرها به همه کاربران

// ActivateClubMembershipCommandHandler.cs + AcceptClubMembershipContractCommandHandler.cs:
var featureIds = ClubFeatureTypeExtensions.GetAllFeatureIds();
// ↑ همیشه همه فیچرها (Chatika, Bime, Trip, Learn) — صرف‌نظر از پکیج!

مشکل: پکیج نقره‌ای ممکنه فقط ۲ فیچر داشته باشه ولی سیستم فعلی همه ۴ فیچر رو فعال می‌کنه.

اقدام:

// باید بشه:
var featureIds = await _context.PackageFeatures
    .Where(pf => pf.PackageId == package.Id && pf.IsIncluded)
    .Select(pf => pf.ClubFeatureId)
    .ToListAsync();

۷٫۵٫۶ 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

-- پکیج پایه (۵۶ میلیون تومان)
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 داده‌های فعلی

-- ========================================
-- 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
    UPDATE "CMS"."ClubMemberships" 
    SET "PackageId" = base_pkg_id WHERE "PackageId" 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 "PackageId" 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 — ۹۰+ تغییر در ۶ لایه (v4)

۴۸ تغییر اصلی (بخش ۱۰) + ۴۴ سایدافکت (بخش ۷.۵) = ۹۲ تغییر کل

۱۰.۱ لایه Domain (۱۰ تغییر)

# فایل نوع شدت v3?
D1 Package.cs اضافه ۱۱ فیلد جدید (v2: ۷ + v3: ۴) 🟡 🔄
D2 PackageFeature.cs Entity جدید + EF Config 🔴
D3 ClubMembership.cs اضافه PackageId 🟡
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 اجازه re-contract بعد چرخه 🟡
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) همه فیچرها global → per-package از PackageFeature 🔴 v4
A18 JWT Token Generation اضافه PackageId + PackageTitle + CanRepurchase 🟡 v4
A19 WalletGrpcService.GetMagicWalletStatus سقف و باقیمانده per-package (نه global) 🟡 v4
A20 Notifications (۴ مورد) اضافه PackageId + PackageTitle به interface 🟡 v4

۱۰.۳ لایه 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 اضافه PackageId به ClubMembership
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 — فیچر از PackageFeature
T2.4 بروزرسانی ActivateClubMembership — ActivationFee از Package
T2.5 اجازه re-purchase در Guards (G1G3)
T2.6 ریست وضعیت در EXIT Magic Mode
T2.7 اجازه re-contract (G5) + بروزرسانی JWT (G7)
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

فاز ۳ — محاسبه پورسانت ≈ ۴ روز (موازی با فاز ۲)

تسک شرح 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: کاشی‌های پکیج (داینامیک)
T4.2 FrontOffice: مدال پرداخت (دایا+مستقیم / فقط مستقیم)
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 Deploy staging → production

۱۲. ریسک‌ها (v4)

ریسک احتمال شدت راه‌حل
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

۱۳. تخمین زمانی (v4)

فاز مدت وابستگی v4 تغییر
فاز ۰ — فیکس باگ‌ها ۱ روز
فاز ۱ — زیرساخت ۵ روز فاز ۰ +۱ (WalletChangeLog PackageId + ۲۱ محل)
فاز ۲ — منطق ۵ روز فاز ۱ +۱ (Magic Wallet + Validators + JWT + Notifications)
فاز ۳ — پورسانت ۴ روز فاز ۱
فاز ۴ — UI ۷ روز فاز ۲ +۲ (Magic Wallet UI + قرارداد + ManualActivation + SystemConfig)
فاز ۵ — تست ۴ روز فاز ۳, ۴ +۱ (تست Magic per-package + قرارداد + WalletLog)
مجموع ~۲۶ روز

فاز ۲ و ۳ موازی → مسیر بحرانی: ۰→۱→۲→۴→۵ = ~۲۲ روز
نسبت به v3 (۱۷ روز): +۵ روز بخاطر ۴۴ سایدافکت کشف‌شده
نسبت به v2 (۱۴ روز): +۸ روز — بزرگ‌ترین سهم: Magic Wallet + WalletChangeLog + قرارداد حقوقی