# 📦 سیستم مبتنی بر پکیج (Package-Based System) > **وضعیت:** تایید‌شده — آماده پیاده‌سازی > **تاریخ بروزرسانی:** ۶ اسفند ۱۴۰۴ > **نسخه:** v3 (تکمیل تحلیل پورسانت per-package + گزارش‌دهی + SystemConstants کامل) > **تاثیرگذاری:** زیاد — ۵۵+ فایل در ۶ لایه --- ## ۱. خلاصه فیچر **وضعیت فعلی:** سیستم فقط یک پکیج پایه (۵۶ میلیون تومان) دارد و همه چیز حول آن 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** | `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 | مسدود | ✅ اجازه 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 ```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 PackageFeatures { get; set; } public virtual ICollection Purchases { get; set; } public virtual ICollection 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` | `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) ### ۵.۱ چرخه حیات کامل ```mermaid 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) ```csharp // 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 — تغییرات ساختاری:** ```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["ساخت پکیج جدید
(از BackOffice)"] --> B["PackageCreatedEvent"] B --> C["ایجاد Pool خالی
برای هفته جاری"] B --> D["لاگ ادمین"] ``` ### ۷.۲ جدول رویدادها | رویداد | Side Effect | |--------|------------| | `PackageCreated` | ساخت WeeklyCommissionPool خالی هفته جاری | | `PackageDeactivated` | هشدار ادمین — Pool موجود تکمیل شود | | `PackagePurchased` | ActivationFee → Pool پکیج + شارژ wallets | | `MagicCycleCompleted` | ریست PackagePurchaseMethod + Deactivate membership | --- ## ۸. 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 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 — ۴۸+ تغییر در ۶ لایه (v3) ### ۱۰.۱ لایه 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 (تعداد پکیج)** | **🟡** | **🆕** | ### ۱۰.۲ لایه 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 | 🟡 | ### ۱۰.۳ لایه 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 (تیم اول/دوم بر اساس پکیج)** | **🟡** | **🆕** | ### ۱۰.۶ لایه 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`** | **ستون پکیج در لیست پرداخت‌ها + فیلتر** | **🟡** | **🆕** | --- ## ۱۱. فازبندی پیاده‌سازی ### فاز ۰ — فیکس باگ‌های فوری ≈ ۱ روز | تسک | شرح | |-----|------| | **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 = پکیج پایه** | **🆕** | ### فاز ۲ — منطق کسب‌وکار ≈ ۴ روز | تسک | شرح | |-----|------| | **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 (G1–G3) | | **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 خالی | ### فاز ۳ — محاسبه پورسانت ≈ ۴ روز (موازی با فاز ۲) | تسک | شرح | 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)** | **🆕** | ### فاز ۵ — تست و استقرار ≈ ۳ روز | تسک | شرح | 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** | Deploy staging → production | --- ## ۱۲. ریسک‌ها | ریسک | احتمال | شدت | راه‌حل | |------|--------|-----|--------| | Migration داده‌ها — PackageId اشتباه | کم | بحرانی | Verify query + بکاپ | | SP تغییر → محاسبات اشتباه | متوسط | بحرانی | تست staging قبل production | | ادغام handlers → رگرسیون | متوسط | زیاد | E2E test | | خرید مجدد بدون تکمیل چرخه | کم | زیاد | Validation: چرخه قبلی MagicCompletedAt | | Proto breaking change | قطعی | کم | backward compatible fields | --- ## ۱۳. تخمین زمانی (v3) | فاز | مدت | وابستگی | v3 تغییر | |-----|------|---------|----------| | فاز ۰ — فیکس باگ‌ها | ۱ روز | — | | | فاز ۱ — زیرساخت | ۴ روز | فاز ۰ | +۱ روز (NetworkWeeklyBalance + Unique) | | فاز ۲ — منطق | ۴ روز | فاز ۱ | | | فاز ۳ — پورسانت | **۴ روز** | فاز ۱ | **+۱ روز** (SP params + carryover per-package) | | فاز ۴ — UI | **۵ روز** | فاز ۲ | **+۱ روز** (FO/BO گزارش per-package) | | فاز ۵ — تست | **۳ روز** | فاز ۳, ۴ | **+۱ روز** (تست carryover + گزارش) | | **مجموع** | **~۲۱ روز** | | | > فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۱۷ روز** > نسبت به v2 (**۱۴ روز**): +۳ روز بخاطر per-package commission + گزارش‌دهی + carryover