dd5a2617cf
- 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
584 lines
26 KiB
Markdown
584 lines
26 KiB
Markdown
# 📦 سیستم مبتنی بر پکیج (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 (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 خالی |
|
||
|
||
### فاز ۳ — محاسبه پورسانت ≈ ۳ روز (موازی با فاز ۲)
|
||
|
||
| تسک | شرح |
|
||
|-----|------|
|
||
| **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 | ۴ روز | فاز ۲ |
|
||
| فاز ۵ — تست | ۲ روز | فاز ۳, ۴ |
|
||
| **مجموع** | **~۱۷ روز** | |
|
||
|
||
> فاز ۲ و ۳ **موازی** → مسیر بحرانی: ۰→۱→۲→۴→۵ = **~۱۴ روز**
|