📦 سیستم مبتنی بر پکیج (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، وضعیت کاربر ریست شود تا بتواند پکیج جدید بخرد.
۴. طراحی نهایی (TO-BE)
۴.۱ تغییرات Entity — Package
نسبت به v1:
- ❌
GiftValue حذف (= ActivationFee — تکراری)
- ✅
MagicWalletMultiplier اضافه (داینامیک)
- ❌
PackageType enum نیاز نیست (IsBasePackage کافیست)
۴.۲ Entity جدید — PackageFeature
۴.۳ تغییرات 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 |
✅ حفظ |
عمومی |
۴.۵ فرمول مالی
۵. فلوی خرید مجدد (Re-Purchase)
۵.۱ چرخه حیات کامل
۵.۲ ریست وضعیت بعد تکمیل چرخه (EXIT Magic Mode)
۵.۳ نکات مهم
- دایا فقط دور اول — بعد از دور اول، فقط IPG مجاز
- هر خرید = Commission contribution — ActivationFee به Pool آن پکیج
- PackagePurchaseMethod ریست بعد تکمیل چرخه
- فیچرها بر اساس پکیج جدید — ممکنه متفاوت باشه
۶. Commission Pool — تغییرات
۶.۱ ساختار جدید
۶.۲ ساختار شبکه یکیست
۶.۳ تغییرات SP
| SP |
تغییر |
sp_CalculateWeeklyBalances |
پارامتر @PackageId — فیلتر کاربران بر اساس PackageId |
sp_CalculateWeeklyCommissionPool |
پارامتر @PackageId — Pool مخصوص آن پکیج |
۶.۴ تغییرات Service
۷. Event-Driven Side Effects
۷.۱ ساخت پکیج جدید
۷.۲ جدول رویدادها
| رویداد |
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 دادههای فعلی
۱۰. 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 |
۴ روز |
فاز ۲ |
| فاز ۵ — تست |
۲ روز |
فاز ۳, ۴ |
| مجموع |
~۱۷ روز |
|
فاز ۲ و ۳ موازی → مسیر بحرانی: ۰→۱→۲→۴→۵ = ~۱۴ روز