Files
docs/business/BIZ-PACKAGE-BASED-SYSTEM.md
T
masoodafar-web dd5a2617cf docs: BIZ-PACKAGE-BASED-SYSTEM v2 — deep analysis + approved decisions
- 6 bugs found (DiscountBalance, UserPackagePurchase, re-purchase blocked)
- 15 hardcodes identified for removal
- 7 guards blocking re-purchase analyzed
- 5 payment path inconsistencies documented
- 39 changes across 6 layers planned
- 5 phases: bugfix → infra → logic → commission → UI → test
- v1 draft preserved as BIZ-PACKAGE-BASED-SYSTEM-v1-draft.md
2026-02-24 21:34:59 +03:30

26 KiB
Raw Blame History

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

وضعیت: تایید‌شده — آماده پیاده‌سازی
تاریخ بروزرسانی: ۶ اسفند ۱۴۰۴
نسخه: v2 (بازنویسی کامل بعد از تحلیل عمیق کدبیس)
تاثیرگذاری: زیاد — ۳۹ فایل در ۶ لایه


۱. خلاصه فیچر

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

۳. تحلیل عمیق وضعیت فعلی (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; // ضریب کیف‌پول جادویی
    
    // === 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

Constraint جدید: WeeklyCommissionPool → Unique(WeekDefinitionId, PackageId)

۴.۴ حذف/تغییر SystemConstants

ثابت تغییر جایگزین
ClubMembershipGiftValue حذف تکراری بود
ClubActivationFee حذف Package.ActivationFee
BasePackageAmount حذف Package.Price
DayaLoanAmount حذف Package.Price (base)
MagicWalletMultiplier حذف Package.MagicWalletMultiplier
CommissionMaxWeeklyBalancesPerLeg حفظ عمومی
CommissionMaxNetworkLevel حفظ عمومی
ShopVAT حفظ عمومی

۴.۵ فرمول مالی

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_پایه

۶.۳ تغییرات SP

SP تغییر
sp_CalculateWeeklyBalances پارامتر @PackageId — فیلتر کاربران بر اساس PackageId
sp_CalculateWeeklyCommissionPool پارامتر @PackageId — Pool مخصوص آن پکیج

۶.۴ تغییرات Service

// WeeklyCommissionCalculationService — Loop روی پکیج‌ها:
var activePackages = await _context.Packages
    .Where(p => p.IsActive && !p.IsDeleted)
    .ToListAsync();

foreach (var package in activePackages)
{
    await strategy.CalculateWeeklyBalancesAsync(weekId, package.Id);
    await strategy.CalculateWeeklyPoolAsync(weekId, package.Id);
}

۷. 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

۸. Seed Data

-- پکیج پایه (۵۶ میلیون تومان)
INSERT INTO "CMS"."Packages" (
    "Title", "Description", "Price", "IsActive", "IsBasePackage",
    "SupportsDayaPurchase", "SupportsDirectPurchase", 
    "ActivationFee", "DiscountMultiplier", "MagicWalletMultiplier",
    "SortOrder", "ImagePath"
) VALUES (
    'پکیج پایه', 'پکیج اصلی باشگاه مشتریان کارا بازار سلامت',
    56000000, true, true,
    true, true,
    25200000, 2.0, 2.5,
    2, ''
);

-- پکیج نقره‌ای (۵.۶ میلیون تومان)
INSERT INTO "CMS"."Packages" (
    "Title", "Description", "Price", "IsActive", "IsBasePackage",
    "SupportsDayaPurchase", "SupportsDirectPurchase",
    "ActivationFee", "DiscountMultiplier", "MagicWalletMultiplier",
    "SortOrder", "ImagePath"
) VALUES (
    'پکیج نقره‌ای', 'پکیج سطح نقره‌ای باشگاه مشتریان',
    5600000, true, false,
    false, true,
    2520000, 2.0, 2.5,
    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;
    
    RAISE NOTICE 'Migration completed for PackageId=%', base_pkg_id;
END $$;

-- STEP 6: 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;

۱۰. Impact Analysis — ۳۹ تغییر در ۶ لایه

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

# فایل نوع شدت
D1 Package.cs اضافه ۷ فیلد جدید 🟡
D2 PackageFeature.cs Entity جدید + EF Config 🔴
D3 ClubMembership.cs اضافه PackageId 🟡
D4 ClubMembershipCycle.cs اضافه PackageId 🟡
D5 WeeklyCommissionPool.cs اضافه PackageId + Unique 🔴
D6 UserCommissionPayout.cs اضافه PackageId 🟡
D7 SystemConstants.cs حذف ۵ ثابت، حفظ بقیه 🟡
D8 EF Migration + Seed schema + data migration 🔴

۱۰.۲ لایه 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 (۵ تغییر)

# فایل نوع شدت
I1 sp_CalculateWeeklyBalances پارامتر @PackageId 🔴
I2 sp_CalculateWeeklyCommissionPool پارامتر @PackageId 🔴
I3 WeeklyCommissionCalculationService Loop روی پکیج‌ها 🟡
I4 OrmCommissionCalculationStrategy فیلتر PackageId 🔴
I5 SpCommissionCalculationStrategy پاس دادن PackageId 🟡

۱۰.۴ لایه Proto/gRPC (۴ تغییر)

# فایل نوع شدت
P1 package.proto فیلدهای جدید Package 🟡
P2 clubmembership.proto package_id در request/response 🟡
P3 commission.proto package_id در pool/payout 🟡
P4 PackageGrpcService.cs Generic purchase + CRUD 🟡

۱۰.۵ لایه FrontOffice (۶ تغییر)

# فایل نوع شدت
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 🟡

۱۰.۶ لایه BackOffice (۴ تغییر)

# فایل نوع شدت
BO1 PackageCreateDialog.razor فیلدهای جدید 🟡
BO2 PackageEditDialog.razor فیلدهای جدید 🟡
BO3 PackageFeatureMatrixPageجدید ماتریس پکیج×فیچر 🔴
BO4 ActivateClubDialog.razor dropdown انتخاب پکیج 🟡

۱۱. فازبندی پیاده‌سازی

فاز ۰ — فیکس باگ‌های فوری ≈ ۱ روز

تسک شرح
T0.1 فیکس VerifyGoldenPackagePurchase — اضافه DiscountBalance (Amount × 2)
T0.2 فیکس VerifyGoldenPackagePurchase — ساخت UserPackagePurchase
T0.3 فیکس VerifyPackagePurchase — ساخت UserPackagePurchase
T0.4 فیکس VerifyBasePackagePayment — ساخت UserPackagePurchase

فاز ۱ — زیرساخت (Domain + DB) ≈ ۳ روز

تسک شرح
T1.1 بروزرسانی Package entity (۷ فیلد جدید)
T1.2 ایجاد PackageFeature entity + EF Config
T1.3 اضافه PackageId به ClubMembership
T1.4 اضافه PackageId به ClubMembershipCycle
T1.5 اضافه PackageId به WeeklyCommissionPool + Unique
T1.6 اضافه PackageId به UserCommissionPayout
T1.7 حذف ۵ ثابت از SystemConstants
T1.8 Database Migration + Seed Data (۲ پکیج + فیچرها)
T1.9 Data Migration: کاربران فعلی → PackageId = پکیج پایه
T1.10 بروزرسانی Proto‌ها

فاز ۲ — منطق کسب‌وکار ≈ ۴ روز

تسک شرح
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 خالی

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

تسک شرح
T3.1 بروزرسانی sp_CalculateWeeklyBalances@PackageId
T3.2 بروزرسانی sp_CalculateWeeklyCommissionPool@PackageId
T3.3 بروزرسانی WeeklyCommissionCalculationService — Loop
T3.4 بروزرسانی OrmCommissionCalculationStrategy — فیلتر
T3.5 تست محاسبات با داده واقعی

فاز ۴ — UI ≈ ۴ روز

تسک شرح
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

فاز ۵ — تست و استقرار ≈ ۲ روز

تسک شرح
T5.1 تست خرید هر پکیج
T5.2 تست re-purchase بعد تکمیل چرخه
T5.3 تست Commission Pool جداگانه
T5.4 تست Migration
T5.5 Deploy staging → production

۱۲. ریسک‌ها

ریسک احتمال شدت راه‌حل
Migration داده‌ها — PackageId اشتباه کم بحرانی Verify query + بکاپ
SP تغییر → محاسبات اشتباه متوسط بحرانی تست staging قبل production
ادغام handlers → رگرسیون متوسط زیاد E2E test
خرید مجدد بدون تکمیل چرخه کم زیاد Validation: چرخه قبلی MagicCompletedAt
Proto breaking change قطعی کم backward compatible fields

۱۳. تخمین زمانی

فاز مدت وابستگی
فاز ۰ — فیکس باگ‌ها ۱ روز
فاز ۱ — زیرساخت ۳ روز فاز ۰
فاز ۲ — منطق ۴ روز فاز ۱
فاز ۳ — پورسانت ۳ روز فاز ۱
فاز ۴ — UI ۴ روز فاز ۲
فاز ۵ — تست ۲ روز فاز ۳, ۴
مجموع ~۱۷ روز

فاز ۲ و ۳ موازی → مسیر بحرانی: ۰→۱→۲→۴→۵ = ~۱۴ روز