1db77b1a1b
- 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.
247 lines
11 KiB
Markdown
247 lines
11 KiB
Markdown
# 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: بازگشت از درگاه<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)
|
||
|
||
```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<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 — فعالسازی دستی)
|
||
|
||
```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<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` انجام بده:
|
||
|
||
```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 صدا زده نمیشود<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 باید از **فقط یک منبع** استفاده کند:
|
||
|
||
```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 جبران میشد.
|