# TECH-06 — جریان شارژ Pool کمیسیون هفتگی > تاریخ: ۱۴۰۵/۰۲/۱۰ > وضعیت: **باگ شناسایی‌شده — منتظر Fix** > مرتبط با: `ActivateClubMembershipCommandHandler.cs` · `AcceptClubMembershipContractCommandHandler.cs` · `CreateManualPaymentCommandHandler.cs` --- ## ۱. مسیر مشتری جدید (اولین خرید پکیج) ```mermaid 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: بازگشت از درگاه
?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
HasPurchasedPackage=true
IsClubMemberActive=false U->>PI: کلیک "بازگشت به پروفایل" PI->>PI: OnAfterRenderAsync
CheckAndShowClubContractModal() Note over PI: HasPurchasedPackage=true
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) ```mermaid 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
IsCurrentCycle=true (باگ B6 — ریست نشده) U->>CB: بازگشت از درگاه (خرید مجدد) CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority) CMS->>CMS: ActivateClubMembership(ForceActivation=false) Note over CMS: isNewMembership = false
existingMembership.IsActive=true
hasCurrentCycle=true CMS->>CMS: return true زودهنگام ❌ Note over CMS: Cycle جدید ساخته نمی‌شه ❌
Pool شارژ نمی‌شه ❌ CMS-->>CB: Success=true CB->>CB: RefreshToken → IsClubMemberActive=true U->>PI: بازگشت به پروفایل PI->>PI: IsClubMemberActive=true
→ مودال نمایش داده نمی‌شه ✓ Note over PI,CMS: Pool هرگز شارژ نشد ❌
Cycle جدید وجود ندارد ❌ ``` > **نتیجه**: Pool برای خرید مجدد **هرگز** شارژ نمی‌شود. ریشه مشکل: باگ B6 — `IsCurrentCycle` هنگام خروج از Magic ریست نمی‌شود. --- ## ۳. مسیر ادمین (BackOffice — فعال‌سازی دستی) ```mermaid 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
skip همه validation‌های مالی alt کاربر جدید (isNewMembership=true) CMS->>CMS: ClubMembership(IsActive=false) ایجاد CMS->>CMS: Cycle #1 ایجاد CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌ Note over CMS: کاربر هنوز عضو فعال نیست!
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` انجام بده: ```csharp // ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری // عضو جدید: Pool توسط AcceptClubMembershipContract شارژ می‌شه (هنگام امضای قرارداد) // خرید مجدد: قرارداد مجدد امضا نمی‌شه — Pool همین‌جا شارژ می‌شه if (!isNewMembership) { // ... کد موجود شارژ Pool ... } ``` ### باگ B6 — خرید مجدد کار نمی‌کند **فایل**: `UserOrderService.cs` — بخش خروج از Magic **علت**: هنگام خروج از Magic، `cycle.IsCurrentCycle` به `false` ریست نمی‌شود → `ActivateClubMembership` با `hasCurrentCycle=true` زودهنگام برمی‌گردد. **Fix**: در `ExitMagicMode`: ```csharp cycle.IsCurrentCycle = false; // ← اضافه شود ``` ### باگ C — پرداخت دستی: Cycle هرگز ساخته نمی‌شود **فایل**: `CreateManualPaymentCommandHandler.cs` **علت**: پرداخت دستی `ActivateClubMembership` را صدا نمی‌زند → هیچ `ClubMembershipCycle` ساخته نمی‌شود → SP این کاربر را به عنوان "عضو جدید" برای والدینش حساب نمی‌کند. **تأثیر**: Pool یک‌بار شارژ می‌شود (توسط AcceptContract ✓) ولی balance والدین در sp_CalculateWeeklyBalances افزایش نمی‌یابد (چون Cycle ندارد ❌). --- ## ۴. مسیر پرداخت دستی (BackOffice — ManualPayment) ```mermaid 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 صدا زده نمی‌شود
❌ ClubMembershipCycle ساخته نمی‌شود
❌ Pool شارژ نمی‌شود CMS-->>DL: ManualPaymentId DL->>A: "پرداخت دستی با موفقیت ثبت شد" Note over A,U: کاربر باید به FO مراجعه کند U->>PI: ورود به پروفایل PI->>PI: OnAfterRenderAsync → CheckAndShowClubContractModal() Note over PI: HasPurchasedPackage=true (PackagePurchaseMethod=DirectPurchase)
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 هرگز ساخته نمی‌شود
(AcceptContract از Cycle خبری ندارد) CMS-->>CD: Success=true CD->>PI: dialog.Close(Ok) ``` > **نتیجه**: > - Pool: **1×** شارژ می‌شود ✅ (درست) > - `ClubMembershipCycle`: **هرگز ساخته نمی‌شود** ❌ > - در `sp_CalculateWeeklyBalances`: کاربر `IsActive=true` دارد → خودش می‌تواند کمیسیون دریافت کند ✅ > - ولی والدین این کاربر **هیچ "عضو جدید" برای این هفته دریافت نمی‌کنند** ❌ (چون SP از `ClubMembershipCycles.PackagePurchasedAt` می‌خواند) --- ## ۶. validation داشبورد کمیسیون پس از رفع باگ A، validation باید از **فقط یک منبع** استفاده کند: ```csharp // درست: فقط 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 جبران می‌شد.