- Introduced a detailed audit report for the CMS database integrity, highlighting issues related to data entry, code bugs, and stored procedures. - Fixed double-charge issue in the commission pool during club membership activation. - Updated stored procedures to ensure correct pool calculations across different weeks. - Enhanced the network tree feature to include activation type and package details. - Improved UI for the network tree display and resolved pagination issues in the discount store.
11 KiB
TECH-06 — جریان شارژ Pool کمیسیون هفتگی
تاریخ: ۱۴۰۵/۰۲/۱۰
وضعیت: باگ شناساییشده — منتظر Fix
مرتبط با:ActivateClubMembershipCommandHandler.cs·AcceptClubMembershipContractCommandHandler.cs·CreateManualPaymentCommandHandler.cs
۱. مسیر مشتری جدید (اولین خرید پکیج)
sequenceDiagram
actor U as کاربر (FrontOffice)
participant CB as PaymentCallback.razor
participant PI as Profile/Index.razor
participant CD as ClubMembershipContractDialog
participant CMS as CMS (gRPC)
U->>CB: بازگشت از درگاه<br/>?type=package&orderId=X&Authority=Y
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
Note over CMS: PackageService.VerifyPackagePurchase()
CMS->>CMS: تأیید با درگاه ✓
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
Note over CMS: isNewMembership = true
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
CMS->>CMS: ClubMembershipCycle #1 ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
CMS-->>CB: Success=true
CB->>CB: RefreshToken<br/>HasPurchasedPackage=true<br/>IsClubMemberActive=false
U->>PI: کلیک "بازگشت به پروفایل"
PI->>PI: OnAfterRenderAsync<br/>CheckAndShowClubContractModal()
Note over PI: HasPurchasedPackage=true<br/>AND IsClubMemberActive=false → نمایش مودال
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
U->>CD: مطالعه قرارداد + درخواست OTP
CD->>CMS: CreateNewOtpToken(purpose=signClubContract)
CMS-->>CD: OTP ارسال شد
U->>CD: وارد کردن OTP ۶ رقمی
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
Note over CMS: AcceptClubMembershipContractCommandHandler
CMS->>CMS: IsActive == false → guard رد میشه ✓
CMS->>CMS: IsActive = true
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ دوم ❌
CMS-->>CD: Success=true
CD->>PI: dialog.Close(Ok)
PI->>PI: LoadUserAuthInfo → IsClubMemberActive=true
نتیجه: Pool برای عضو جدید ۲ برابر شارژ میشود.
۲. مسیر خرید مجدد (بعد از تکمیل چرخه Magic)
sequenceDiagram
actor U as کاربر (FrontOffice)
participant CB as PaymentCallback.razor
participant PI as Profile/Index.razor
participant CMS as CMS (gRPC)
Note over CMS: وضعیت: IsActive=true<br/>IsCurrentCycle=true (باگ B6 — ریست نشده)
U->>CB: بازگشت از درگاه (خرید مجدد)
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
Note over CMS: isNewMembership = false<br/>existingMembership.IsActive=true<br/>hasCurrentCycle=true
CMS->>CMS: return true زودهنگام ❌
Note over CMS: Cycle جدید ساخته نمیشه ❌<br/>Pool شارژ نمیشه ❌
CMS-->>CB: Success=true
CB->>CB: RefreshToken → IsClubMemberActive=true
U->>PI: بازگشت به پروفایل
PI->>PI: IsClubMemberActive=true<br/>→ مودال نمایش داده نمیشه ✓
Note over PI,CMS: Pool هرگز شارژ نشد ❌<br/>Cycle جدید وجود ندارد ❌
نتیجه: Pool برای خرید مجدد هرگز شارژ نمیشود. ریشه مشکل: باگ B6 —
IsCurrentCycleهنگام خروج از Magic ریست نمیشود.
۳. مسیر ادمین (BackOffice — فعالسازی دستی)
sequenceDiagram
actor A as ادمین (BackOffice)
participant DL as ActivateClubDialog.razor
participant CMS as CMS (gRPC)
A->>DL: باز کردن دیالوگ فعالسازی برای کاربر X
DL->>DL: انتخاب UserId و PackageId
A->>DL: کلیک "تایید و فعالسازی"
DL->>CMS: ActivateClubMembership(UserId=X, ForceActivation=true)
Note over CMS: ActivateClubMembershipCommandHandler<br/>skip همه validationهای مالی
alt کاربر جدید (isNewMembership=true)
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
CMS->>CMS: Cycle #1 ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
Note over CMS: کاربر هنوز عضو فعال نیست!<br/>IsActive=false
Note over A,CMS: کاربر باید به FO رود و قرارداد امضا کند
Note over A,CMS: AcceptContract → Pool += fee ← شارژ دوم ❌
else خرید مجدد (isNewMembership=false، IsCurrentCycle ریست شده)
CMS->>CMS: IsActive=true, hasCurrentCycle=false → ادامه میدهد
CMS->>CMS: Cycle جدید ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یک بار ✅
end
CMS-->>DL: Empty (success)
DL->>A: "عضویت با موفقیت فعال شد"
Note over A,CMS: AcceptContract از BO هرگز فراخوانی نمیشود
نتیجه: ادمین برای کاربر جدید نیز باعث double-charge میشود (چون کاربر بعداً از FO قرارداد امضا میکند). برای خرید مجدد رفتار درست است.
۵. خلاصه باگها
| سناریو | Pool شارژ واقعی | Pool شارژ انتظاری | Cycle ساخته میشود | وضعیت |
|---|---|---|---|---|
| مشتری جدید (IPG) | 2×fee | 1×fee | ✅ بله | ❌ Double-charge |
| خرید مجدد مشتری | 0×fee | 1×fee | ❌ خیر (B6) | ❌ هرگز شارژ نمیشود |
| ادمین — ForceActivate کاربر جدید | 2×fee | 1×fee | ✅ بله | ❌ Double-charge |
| ادمین — ForceActivate خرید مجدد | 1×fee | 1×fee | ✅ بله | ✅ درست |
| ادمین — ManualPayment (پرداخت دستی) | 1×fee | 1×fee | ❌ خیر | ⚠️ Pool درست، ولی والدین امتیاز نمیگیرند |
۵. ریشه مشکلات
باگ A — Double-charge در عضو جدید
فایل: ActivateClubMembershipCommandHandler.cs — بخش Pool (خط ~۳۱۱)
علت: هنگامی که isNewMembership=true، Pool شارژ میشود؛ بعداً AcceptContract هم Pool را شارژ میکند.
Fix: شارژ Pool در ActivateClubMembership را فقط برای !isNewMembership انجام بده:
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
// عضو جدید: Pool توسط AcceptClubMembershipContract شارژ میشه (هنگام امضای قرارداد)
// خرید مجدد: قرارداد مجدد امضا نمیشه — Pool همینجا شارژ میشه
if (!isNewMembership)
{
// ... کد موجود شارژ Pool ...
}
باگ B6 — خرید مجدد کار نمیکند
فایل: UserOrderService.cs — بخش خروج از Magic
علت: هنگام خروج از Magic، cycle.IsCurrentCycle به false ریست نمیشود → ActivateClubMembership با hasCurrentCycle=true زودهنگام برمیگردد.
Fix: در ExitMagicMode:
cycle.IsCurrentCycle = false; // ← اضافه شود
باگ C — پرداخت دستی: Cycle هرگز ساخته نمیشود
فایل: CreateManualPaymentCommandHandler.cs
علت: پرداخت دستی ActivateClubMembership را صدا نمیزند → هیچ ClubMembershipCycle ساخته نمیشود → SP این کاربر را به عنوان "عضو جدید" برای والدینش حساب نمیکند.
تأثیر: Pool یکبار شارژ میشود (توسط AcceptContract ✓) ولی balance والدین در sp_CalculateWeeklyBalances افزایش نمییابد (چون Cycle ندارد ❌).
۴. مسیر پرداخت دستی (BackOffice — ManualPayment)
sequenceDiagram
actor A as ادمین (BackOffice)
participant DL as ManualPaymentDialog.razor
participant CMS as CMS (gRPC)
actor U as کاربر (FrontOffice)
participant PI as Profile/Index.razor
participant CD as ClubMembershipContractDialog
A->>DL: باز کردن دیالوگ پرداخت دستی
DL->>DL: انتخاب کاربر + پکیج + نوع پرداخت + تصویر رسید
A->>DL: کلیک "ثبت پرداخت"
DL->>CMS: CreateManualPayment(userId, packageId, type, referenceNumber)
Note over CMS: CreateManualPaymentCommandHandler
CMS->>CMS: Transaction(DepositExternal1) ایجاد
CMS->>CMS: ManualPayment(Status=Approved) ایجاد ← بدون نیاز به تایید دو مرحله
CMS->>CMS: wallet.Balance += package.Price
CMS->>CMS: wallet.DiscountBalance += package.Price × DiscountMultiplier
CMS->>CMS: user.PackagePurchaseMethod = DirectPurchase
Note over CMS: ❌ ActivateClubMembership صدا زده نمیشود<br/>❌ ClubMembershipCycle ساخته نمیشود<br/>❌ Pool شارژ نمیشود
CMS-->>DL: ManualPaymentId
DL->>A: "پرداخت دستی با موفقیت ثبت شد"
Note over A,U: کاربر باید به FO مراجعه کند
U->>PI: ورود به پروفایل
PI->>PI: OnAfterRenderAsync → CheckAndShowClubContractModal()
Note over PI: HasPurchasedPackage=true (PackagePurchaseMethod=DirectPurchase)<br/>IsClubMemberActive=false → نمایش مودال
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
U->>CD: امضای قرارداد + OTP
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
Note over CMS: AcceptClubMembershipContractCommandHandler
CMS->>CMS: user.ClubMembership == null → isNewMembership = true
CMS->>CMS: ClubMembership(IsActive=true) ایجاد
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یکبار ✅
Note over CMS: ❌ ClubMembershipCycle هرگز ساخته نمیشود<br/>(AcceptContract از Cycle خبری ندارد)
CMS-->>CD: Success=true
CD->>PI: dialog.Close(Ok)
نتیجه:
- Pool: 1× شارژ میشود ✅ (درست)
ClubMembershipCycle: هرگز ساخته نمیشود ❌- در
sp_CalculateWeeklyBalances: کاربرIsActive=trueدارد → خودش میتواند کمیسیون دریافت کند ✅- ولی والدین این کاربر هیچ "عضو جدید" برای این هفته دریافت نمیکنند ❌ (چون SP از
ClubMembershipCycles.PackagePurchasedAtمیخواند)
۶. validation داشبورد کمیسیون
پس از رفع باگ A، validation باید از فقط یک منبع استفاده کند:
// درست: فقط ClubMembershipCycles.PackagePurchasedAt
// این جدول برای هر خرید (چه جدید چه مجدد) یک رکورد دارد
var activations = await _context.ClubMembershipCycles
.CountAsync(c => c.PackageId == packageId
&& c.PackagePurchasedAt >= weekDef.StartDate
&& c.PackagePurchasedAt < weekDef.EndDate);
قبل از رفع باگ A، validation فعلی (firstActivations + cycleActivations) تصادفاً با double-charge جبران میشد.