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.
This commit is contained in:
masoodafar-web
2026-05-01 00:07:25 +03:30
parent e3850f9dd8
commit 1db77b1a1b
3 changed files with 936 additions and 0 deletions
+246
View File
@@ -0,0 +1,246 @@
# 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 جبران می‌شد.