Files
docs/technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md
masoodafar-web 1db77b1a1b Add comprehensive database integrity audit report and fix critical bugs in commission pool charging flow
- 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.
2026-05-01 00:07:25 +03:30

11 KiB
Raw Permalink Blame History

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 جبران می‌شد.