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

584 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📦 سیستم مبتنی بر پکیج (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** | `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; // ضریب کیف‌پول جادویی
// === Navigation ===
public virtual ICollection<PackageFeature> PackageFeatures { get; set; }
public virtual ICollection<UserPackagePurchase> Purchases { get; set; }
public virtual ICollection<UserOrder> UserOrders { get; set; }
}
```
**نسبت به v1:**
-`GiftValue` حذف (= ActivationFee — تکراری)
-`MagicWalletMultiplier` اضافه (داینامیک)
-`PackageType` enum نیاز نیست (`IsBasePackage` کافیست)
### ۴.۲ Entity جدید — PackageFeature
```csharp
public class PackageFeature : BaseAuditableEntity
{
public long PackageId { get; set; }
public virtual Package Package { get; set; }
public long ClubFeatureId { get; set; }
public virtual ClubFeature ClubFeature { get; set; }
public bool IsIncluded { get; set; } = true;
}
```
### ۴.۳ تغییرات Entity — سایر
| Entity | فیلد جدید | توضیح |
|--------|-----------|-------|
| `ClubMembership` | `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)
### ۵.۱ چرخه حیات کامل
```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_پایه
```
### ۶.۳ تغییرات SP
| SP | تغییر |
|----|-------|
| `sp_CalculateWeeklyBalances` | پارامتر `@PackageId` — فیلتر کاربران بر اساس PackageId |
| `sp_CalculateWeeklyCommissionPool` | پارامتر `@PackageId` — Pool مخصوص آن پکیج |
### ۶.۴ تغییرات Service
```csharp
// 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
### ۷.۱ ساخت پکیج جدید
```mermaid
flowchart LR
A["ساخت پکیج جدید<br/>(از BackOffice)"] --> B["PackageCreatedEvent"]
B --> C["ایجاد Pool خالی<br/>برای هفته جاری"]
B --> D["لاگ ادمین"]
```
### ۷.۲ جدول رویدادها
| رویداد | Side Effect |
|--------|------------|
| `PackageCreated` | ساخت WeeklyCommissionPool خالی هفته جاری |
| `PackageDeactivated` | هشدار ادمین — Pool موجود تکمیل شود |
| `PackagePurchased` | ActivationFee → Pool پکیج + شارژ wallets |
| `MagicCycleCompleted` | ریست PackagePurchaseMethod + Deactivate membership |
---
## ۸. Seed Data
```sql
-- پکیج پایه (۵۶ میلیون تومان)
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 داده‌های فعلی
```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;
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 | ۴ روز | فاز ۲ |
| فاز ۵ — تست | ۲ روز | فاز ۳, ۴ |
| **مجموع** | **~۱۷ روز** | |
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۱۴ روز**