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
+356
View File
@@ -0,0 +1,356 @@
# گزارش ممیزی صحت داده‌های دیتابیس CMS
**تاریخ بررسی:** 1405/01/28 (2026-04-17)
**فایل بکاپ:** `dbbkup/CMS-20260417.sql` (4.9MB, 21,397 خط)
**تعداد جداول:** ~47 جدول | **تعداد کاربران:** 115 | **تعداد سفارشات:** 72
---
## فهرست مطالب
1. [خلاصه اجرایی](#خلاصه-اجرایی)
2. [اصلاحیه مهم — PaymentStatus](#اصلاحیه-مهم)
3. [دسته ۱ — ورود دستی / مهاجرت دیتا](#دسته-۱--ورود-دستی--مهاجرت-دیتا)
4. [دسته ۲ — باگ‌های کد](#دسته-۲--باگهای-کد)
5. [دسته ۳ — وضعیت Stored Procedure‌ها](#دسته-۳--وضعیت-stored-procedureها)
6. [آمار کلی جداول](#آمار-کلی-جداول)
7. [خلاصه مالی](#خلاصه-مالی)
8. [اقدامات پیشنهادی](#اقدامات-پیشنهادی)
---
## خلاصه اجرایی
بکاپ دیتابیس CMS در تاریخ 17 آوریل 2026 تحلیل شد. تحلیل شامل بررسی صحت داده‌ها، ارجاعات خارجی (FK)، زنجیره مالی، و تطبیق با کد سورس C# و Stored Procedure‌ها بود.
**وضعیت کلی:** سیستم در حال مهاجرت از ساختار استاتیک به پکیج‌محور بوده. بخش عمده مشکلات ناشی از ورود دستی داده و مهاجرت سیستم دایا است. چند باگ کد نیز در SP کمیسیون و Worker دایا شناسایی شد.
---
## اصلاحیه مهم
> **`PaymentStatus=0` در enum کد یعنی `Success` نه `Pending`!**
>
> ```csharp
> // PaymentStatus.cs
> Success = 0,
> Reject = 1,
> Pending = 2
> ```
>
> بنابراین تمام 72 سفارش واقعاً **موفق** هستند. این مشکل نیست.
---
## دسته ۱ — ورود دستی / مهاجرت دیتا
### 1A) موجودی 56M بدون فلگ `HasReceivedDayaCredit`
**شدت:** 🟠 متوسط
**علت:** ورود دستی / مهاجرت
- **23 کاربر** دقیقاً 56,000,000 ریال در `Balance` دارند ولی `HasReceivedDayaCredit = 0`
- فقط **16 کاربر** از طریق Daya Worker صحیح اعتبار گرفتند (`HasReceivedDayaCredit = 1`)
- بقیه احتمالاً دستی شارژ شدند بدون ثبت تاریخچه
**کاربران آسیب‌پذیر:**
| UserId | نام | Balance | HasReceivedDayaCredit |
|--------|-----|---------|----------------------|
| 51 | مرتضی اینالو | 56,000,000 | 0 |
| 58 | وحید حق‌گو | 56,000,000 | 0 |
| 87 | امیررضا محمدی | 56,000,000 | 0 |
| 43 | کریم خادمی | 56,000,000 | 0 |
| 52 | حمیدرضا اسمعیلی | 56,000,000 | 0 |
| 88 | کریم رعیت‌پیشه | 56,000,000 | 0 |
| 91 | رحیم رعیت‌پیشه | 56,000,000 | 0 |
| 93 | ریحانه سادات هاشمی‌نصر | 56,000,000 | 0 |
| 99 | هستی خادمی | 56,000,000 | 0 |
| 110 | علی وفائی | 56,000,000 | 0 |
| 113 | کاوس بیگ‌اینالو | 56,000,000 | 0 |
| 119 | علیرضا کریمی‌پیروز | 56,000,000 | 0 |
| 122 | ابوالقاسم عابدی | 56,000,000 | 0 |
| 123 | ناصر کریمی‌پیروز | 56,000,000 | 0 |
| 124 | محمدرضا باغجری | 56,000,000 | 0 |
| 126 | امیرعباس میرزایی | 56,000,000 | 0 |
| 138 | سیما اکبرزاده | 56,000,000 | 0 |
| 139 | مسعود توسلیان | 56,000,000 | 0 |
| 142 | صغری شبانکاره | 56,000,000 | 0 |
| 170 | لیلا خدارحمی | 56,000,000 | 0 |
| 172 | مهرافشان زاهدنیا | 56,000,000 | 0 |
| 175 | ناهید حسن‌زاده | 56,000,000 | 0 |
| 176 | مهریدخت میکانیکی | 56,000,000 | 0 |
---
### 1B) کد ملی تکراری — اکانت‌های تستی
**شدت:** 🟡 پایین
**علت:** ورود دستی / تست
| کد ملی | تعداد اکانت | UserIdها | نام |
|--------|------------|----------|-----|
| مشترک #1 | 6 | 9, 11, 12, 13, 40, 41 | مهدی مرجانی |
| مشترک #2 | 3 | 7, 8, 10 | مهدی صیفی |
| مشترک #3 | 2 | 42, 43 | کریم خادمی |
| مشترک #4 | 2 | 50, 51 | مرتضی اینالو |
| مشترک #5 | 2 | 120, 190 | عسلی |
- شماره موبایل تکراری: `09038888074` بین کاربران 120 و 190
---
### 1C) `NetworkInfos` خالی — مهاجرت صحیح انجام شده
**شدت:** ✅ مشکل نیست
جدول `NetworkInfos` خالی است چون داده‌های شبکه به فیلدهای مستقیم `Users` مهاجرت شدند:
- `Users.NetworkParentId` ← FK به والد شبکه
- `Users.LegPosition` ← Left(0) / Right(1)
مهاجرت در `20250601_MigrateParentIdToNetworkParentId.sql` انجام شده. entity `NetworkInfo` در C# وجود ندارد. SP‌ها هم از `Users.NetworkParentId` استفاده می‌کنند.
---
### 1D) `PasswordHash = NULL` برای تمام 115 کاربر
**شدت:** 🟡 نیاز به بررسی
**علت:** احتمالاً بکاپ شامل فیلد پسورد نشده، یا سیستم OTP/موبایل استفاده می‌کند
---
### 1E) دوره‌های عضویت باشگاه — `PaidAmount = 0`
**شدت:** 🟡 نیاز به بررسی
- 87 دوره `ClubMembershipCycles` همه `PaidAmount = 0`
- ممکن است عضویت باشگاه خودکار با خرید پکیج فعال شود (نه پرداخت جداگانه)
---
## دسته ۲ — باگ‌های کد
### 2A) تراکنش‌های تکراری دایا — Race Condition در `CheckAndProcessDayaLoansCommandHandler`
**شدت:** 🔴 بحرانی
**فایل:** `CMSMicroservice.Application/DayaLoanCQ/Commands/CheckAndProcessDayaLoans/CheckAndProcessDayaLoansCommandHandler.cs`
**یافته‌ها:**
- **109 رکورد `DayaLoanContracts`** ولی فقط **16 کاربر** `HasReceivedDayaCredit=1`
- **64 تراکنش** با «دریافت اعتبار دایا» ساخته شده ولی فقط **11 رکورد `UserPackagePurchases`**
- تراکنش‌ها با `RefId` منحصربه‌فرد ساخته شدند (مثل `C4_T8579002`) — همه در `2025-11-19 01:24:24` ایجاد شدند
**تحلیل ریشه‌ای:**
- Daya Worker (Hangfire هر 15 دقیقه) احتمالاً برای بعضی کاربران **چند بار** اجرا شده
- `CreateTransaction` و `DayaLoanContract` ساخته شده ولی `HasReceivedDayaCredit=true` ست نشده (exception بعد از SaveChanges اول ولی قبل از SaveChanges دوم)
- یا: چون همه در یک لحظه ساخته شدند (`2025-11-19 01:24:24`)، ممکن است **یک بار bulk import دستی** بوده
**ریسک:** کاربرانی که `HasReceivedDayaCredit=0` دارند ممکن است **دوباره** از Worker اعتبار بگیرند.
---
### 2B) SP `sp_CalculateWeeklyCommissionPool` — `DistributedAmount` آپدیت نمی‌شود
**شدت:** 🔴 بحرانی
**فایل:** `dbbkup/CMS-20260417.sql` خط ~18830
در Step 10 (آپدیت نهایی Pool):
```sql
-- کد فعلی (باگ‌دار):
UPDATE CMS.WeeklyCommissionPools
SET
IsCalculated = 1,
CalculatedAt = @CalculatedAt,
TotalBalances = @TotalBalances,
ValuePerBalance = @ValuePerBalance,
LastModified = @CalculatedAt,
LastModifiedBy = 'SP'
WHERE Id = @PoolId;
```
**مشکل:** فیلد `DistributedAmount` **هرگز مقداردهی نمی‌شود** و 0 باقی می‌ماند.
**نتیجه در دیتا:**
- 15 استخر، مجموع `TotalPoolAmount = 2,016,000,000` ریال
- همه `DistributedAmount = 0`
- ولی 46 پرداخت واقعاً ثبت و به `NetworkBalance` اضافه شدند
---
### 2C) `UserWalletChangeLogs` خالی
**شدت:** 🟠 متوسط
**فایل:** SP Step 9 + `CalculateWeeklyCommissionPoolCommandHandler.cs`
- SP باید در Step 9 لاگ تغییرات کیف‌پول را در `UserWalletChangeLogs` ذخیره کند
- جدول **صفر رکورد** دارد
- **احتمال 1:** SP هرگز Step 9 را درست اجرا نکرده
- **احتمال 2:** ORM Strategy (نه SP) استفاده شده و آن `UserWalletChangeLogs` نمی‌نویسد
- **احتمال 3:** لاگ‌ها در حین ForceRecalculate حذف شدند
---
### 2D) `WalletHistory.ChangeType = NULL` در تمام 239 رکورد
**شدت:** 🟠 متوسط
**فایل:** `UserOrderService.cs` و `PackageService.cs` — هرجا `UserWalletHistory` ساخته می‌شود
- فیلد `ChangeType` هرگز ست نمی‌شود
- کد از `IsIncrease` (bool) برای تفکیک واریز/برداشت استفاده می‌کند
- `ChangeType` احتمالاً فیلد قدیمی deprecated شده
---
### 2E) `NetworkWeeklyBalances.WeeklyCommissionPoolId = NULL` (805 رکورد)
**شدت:** 🟡 پایین
- SP مقدار `WeeklyPoolContribution = 0` ثبت می‌کند و PoolId ست نمی‌شود
- ارتباط بین `NetworkWeeklyBalances` و `WeeklyCommissionPools` از طریق `WeekDefinitionId` برقرار است، نه FK مستقیم
- **عملاً مشکل عملکردی ایجاد نمی‌کند** ولی tracking سخت‌تر می‌شود
---
### 2F) 30 سفارش کیف‌پولی — بررسی WalletHistory
**شدت:** 🟠 نیاز به تأیید
- 30 سفارش با `PaymentMethod=1` (Wallet) ثبت شدند
- اولین بررسی نشان داد «هیچ برداشتی ثبت نشده» — **اما** این بررسی بر اساس `ChangeType` بود که همه NULL هستند
- **باید بر اساس `IsIncrease=0` (false = decrease)** دوباره بررسی شود
- `SubmitShopBuyOrder()` در کد `UserWalletHistory` می‌سازد — احتمالاً رکوردها وجود دارند ولی `ChangeType` NULL است
---
## دسته ۳ — وضعیت Stored Procedure‌ها
### `GetNetworkTree`
| آیتم | وضعیت |
|------|--------|
| از `Users.NetworkParentId` استفاده می‌کند | ✅ صحیح (بعد از مهاجرت) |
| JOIN با `ClubMemberships` | ✅ صحیح |
| `MAXRECURSION 0` | ✅ صحیح |
| فیلتر `IsDeleted = 0` | ✅ صحیح |
### `sp_CalculateWeeklyBalances`
| آیتم | وضعیت |
|------|--------|
| پارامتر `@PackageId` از `Packages.IsBasePackage` | ✅ صحیح |
| `MaxBalancesPerLeg` و `MaxNetworkLevel` از Package | ✅ صحیح |
| Carryover از هفته قبل | ✅ صحیح |
| CTE recursive برای چپ/راست | ✅ صحیح |
| `TotalBalances = MIN(left, right)` | ✅ صحیح |
| `SubordinateBalances` محاسبه | ✅ صحیح |
| 805 رکورد تولید شده | ✅ کار می‌کند |
### `sp_CalculateWeeklyCommissionPool`
| آیتم | وضعیت |
|------|--------|
| `ValuePerBalance = TotalPoolAmount / TotalBalances` | ✅ صحیح |
| ایجاد `UserCommissionPayouts` | ✅ صحیح (46 رکورد) |
| ثبت `CommissionPayoutHistories` | ✅ صحیح |
| شارژ `NetworkBalance` کیف‌پول | ✅ صحیح |
| آپدیت `DistributedAmount` در Pool | ❌ **انجام نمی‌شود** |
| ثبت `UserWalletChangeLogs` | ⚠️ نامشخص |
| ForceRecalculate — Revert | ✅ منطق صحیح |
---
## آمار کلی جداول
| جدول | تعداد | وضعیت |
|------|--------|--------|
| Users | 115 | |
| UserWallets | 115 | |
| UserWalletHistories | 239 | ChangeType همه NULL |
| UserWalletChangeLogs | 0 | ⚠️ خالی |
| UserOrders | 72 | همه PaymentStatus=0 (Success) |
| FactorDetails | 177 | |
| Transactions | 175 | 64 تراکنش دایا |
| PaymentTransactions | 22 | فقط DiscountOrders + شارژ |
| Products | 163 | |
| Categories | 14 | |
| InventoryItems | 175 | |
| StockMovements | 242 | 11 chain issue |
| ClubMemberships | 88 | همه IsActive=1 |
| ClubMembershipCycles | 87 | همه PaidAmount=0 |
| UserClubFeatures | 249 | |
| NetworkInfos | 0 | ✅ deprecated — مهاجرت شده |
| NetworkWeeklyBalances | 805 | PoolId همه NULL |
| WeeklyCommissionPools | 15 | DistributedAmount همه 0 |
| UserCommissionPayouts | 46 | Status=3, مبالغ کلان |
| WeekDefinitions | 59 | |
| Packages | 2 | Base=56M, Secondary=5.6M |
| DayaLoanContracts | 109 | |
| UserPackagePurchases | 11 | |
| DiscountOrders | 13 | |
| DiscountOrderDetails | 14 | |
| DiscountCategories | 8 | |
| OrderVATs | 44 | ✅ محاسبات صحیح |
| UserAddresses | 127 | |
| Roles | 3 | user, admin, Administrator |
| UserRoles | 119 | 115 user + 2 admin + 2 Administrator |
| ProductImages | 4 | |
| ShippingMethods | 0 | ⚠️ خالی |
| SitePages | 0 | ⚠️ خالی |
| SystemConfigurations | 0 | ⚠️ خالی |
| Coupons | 0 | ⚠️ خالی |
| ProductProperties | 0 | ⚠️ خالی |
---
## خلاصه مالی
### موجودی‌های کل سیستم
| فیلد | مبلغ (ریال) |
|------|-------------|
| مجموع `Balance` کل کیف‌پول‌ها | 2,548,684,394 |
| مجموع `NetworkBalance` (کمیسیون) | 453,599,993 |
| مجموع `DiscountBalance` (تخفیف) | 7,326,400,000 |
| مجموع سفارشات (UserOrders) | 1,039,410,812 |
| مجموع استخر کمیسیون (WeeklyPools) | 2,016,000,000 |
### پکیج‌ها
| Package | قیمت | IsBase | MaxBalancesPerLeg | MaxNetworkLevel | DiscountMultiplier |
|---------|-------|--------|-------------------|-----------------|-------------------|
| Package 1 | 56,000,000 | ✅ | 300 | 1,000,000 | 2.0 |
| Package 4 | 5,600,000 | ❌ | 30 | 1,000,000 | 2.0 |
### ارجاعات شکسته (FK)
| ارجاع | تعداد |
|-------|--------|
| FactorDetails → OrderId ناموجود | 2 (DetailId=22,23 → OrderId=21) |
| InventoryItems ≠ آخرین StockMovement | 3 |
| StockMovement chain breaks | 11 |
---
## اقدامات پیشنهادی
### اولویت بالا (انجام ندهید تا بررسی بیشتر)
1. **فیکس SP `sp_CalculateWeeklyCommissionPool`:** اضافه کردن `DistributedAmount` به UPDATE نهایی
2. **بررسی Daya Worker:** race condition در `CheckAndProcessDayaLoansCommandHandler` — ممکن است تراکنش تکراری بسازد
3. **23 کاربر با 56M بدون فلگ دایا:** تعیین اینکه آیا دستی شارژ شدند یا از Worker — سپس اصلاح `HasReceivedDayaCredit`
### اولویت متوسط
4. **WalletHistory ChangeType:** تأیید اینکه deprecated شده و `IsIncrease` جایگزین است
5. **UserWalletChangeLogs خالی:** بررسی اینکه ORM Strategy استفاده شده یا SP Strategy
6. **اکانت‌های تکراری:** تصمیم‌گیری درباره 13 اکانت تکراری (حذف/ادغام)
### اولویت پایین
7. **جداول خالی:** SystemConfigurations, ShippingMethods, SitePages — آیا باید از seed پر شوند؟
8. **StockMovement chain issues:** 11 ناسازگاری — آیا از ورود دستی موجودی بوده؟
---
*این گزارش فقط مستندات یافته‌ها است. هیچ تغییری در کد یا دیتابیس اعمال نشده است.*
+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 جبران می‌شد.
+334
View File
@@ -0,0 +1,334 @@
# TECH-07 — لاگ کامل Session 1404/02/10 (2026-04-30)
> نوع سند: **گزارش کار**
> تاریخ: ۱۴۰۵/۰۲/۱۰
> مرتبط با: CMS · BackOffice · FrontOffice · Database
---
## فهرست مطالب
1. [بخش اول — رفع باگ Double-Charge Pool](#۱-رفع-باگ-double-charge-pool)
2. [بخش دوم — بررسی داده‌های هفته‌های ۲۲ و ۲۳](#۲-بررسی-داده‌های-هفته‌های-۲۲-و-۲۳)
3. [بخش سوم — ویژگی Network Tree (اطلاعات هفتگی)](#۳-ویژگی-network-tree-نوع-فعالسازی--پکیج)
4. [بخش چهارم — بهبود UI نمودار درختی](#۴-بهبود-ui-نمودار-درختی)
5. [بخش پنجم — رفع باگ Pagination فروشگاه تخفیف](#۵-رفع-باگ-pagination-فروشگاه-تخفیف)
6. [خلاصه فایل‌های تغییریافته](#خلاصه-فایل‌های-تغییریافته)
7. [وظایف باقی‌مانده (Pending)](#وظایف-باقی‌مانده)
---
## ۱. رفع باگ Double-Charge Pool
### مشکل
در جریان فعال‌سازی عضویت باشگاه، Pool کمیسیون هفتگی **دوبار** شارژ می‌شد:
- بار اول: در `ActivateClubMembership` (از طریق `VerifyPackagePurchase`)
- بار دوم: در `AcceptClubMembershipContract` (تأیید قرارداد توسط کاربر)
همچنین `CreateManualPayment` هم یک مسیر مستقل داشت که بدون Check هفته، Pool اشتباه را شارژ می‌کرد.
### ریشه مشکل
تابع `GetOrCreateCurrentWeeklyPool` بدون در نظر گرفتن هفته واقعی `PackagePurchasedAt`، Pool هفته جاری را انتخاب می‌کرد.
### فایل‌های اصلاح‌شده
#### `ActivateClubMembershipCommandHandler.cs`
```csharp
// قبل: همیشه Pool هفته جاری را شارژ می‌کرد
// بعد: فقط یک‌بار در محل صحیح (AcceptContract) شارژ می‌شود
// حذف: شارژ Pool از داخل ActivateClubMembership (for isNewMembership scenario)
```
#### `AcceptClubMembershipContractCommandHandler.cs`
```csharp
// اضافه: بررسی هفته قرارداد — اگر هفته PackagePurchasedAt با هفته جاری فرق دارد
// از Pool هفته مناسب استفاده می‌کند نه Pool هفته جاری
```
#### `CreateManualPaymentCommandHandler.cs`
```csharp
// اصلاح: Cross-week fix — Pool هفته صحیح بر اساس تاریخ پرداخت دستی
```
#### `sp_CalculateWeeklyCommissionPool.sql` (SP در Infrastructure)
```sql
-- اصلاح: IsCurrentCycle check برای جلوگیری از Double-Count
-- هر کاربر فقط یک‌بار در محاسبه Pool شمرده می‌شود
```
---
## ۲. بررسی داده‌های هفته‌های ۲۲ و ۲۳
### تشخیص
با اجرای diagnostic SQL روی DB، دو anomaly کشف شد:
#### هفته ۲۲ — Pool Ghost (PoolId=10056)
| فیلد | مقدار |
|------|-------|
| TotalPoolAmount | 2,520,000 |
| AllCycles | 0 |
| ریشه | هانیه سادات عشاقی (UserId=189) — خرید 1404/01/12 (هفته ۲۱) ولی AcceptContract در 17:02 دقیقه بعد Pool هفته ۲۲ را شارژ کرد |
**دلیل:** CreatedAt و ModifiedAt timestamp مغایرت داشت — Pool در هفته ۲۲ ایجاد شد اما Cycle در هفته ۲۱ بود.
**اصلاح دستی DB (Pending):**
```sql
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 0, LastModified = GETUTCDATE()
WHERE Id = 10056 AND WeekDefinitionId = 22;
```
#### هفته ۲۳ — Pool ناقص (PoolId=10054)
| فیلد | مقدار |
|------|-------|
| TotalPoolAmount | 0 |
| IsCalculated | False |
| ریشه | محمدصادق عسلی (UserId=190) — خرید هفته ۲۳، Cycle وجود دارد ولی Pool=0 (قبل از fix) |
**اصلاح دستی DB (Pending):**
```sql
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE()
WHERE Id = 10054 AND WeekDefinitionId = 23;
```
---
## ۳. ویژگی Network Tree (نوع فعالسازی + پکیج)
### هدف
صفحه `/network/tree` در BackOffice باید در هر node نشان دهد:
- آیا این کاربر در هفته انتخابی **عضو جدید** بوده یا **تمدید کرده**؟
- نام پکیجی که خریداری کرده؟
### پیاده‌سازی Full-Stack
#### الف) SP_GetNetworkTree (dbbkup/SP_GetNetworkTree.sql)
```sql
-- اضافه شد:
OUTER APPLY (
SELECT TOP 1 cc.*
FROM CMS.ClubMembershipCycles cc
WHERE cc.ClubMembershipId = cm.Id
AND cc.PackagePurchasedAt >= @WeekStartDate
AND cc.PackagePurchasedAt < @WeekEndDate
AND (@ActivationWeekDefinitionId IS NULL OR @WeekStartDate IS NOT NULL)
) AS cc_target
-- ستون‌های جدید در output:
IsActivatedInTargetWeek -- آیا در هفته انتخابی فعال شده؟
IsNewActivation -- 1=اولین فعالسازی (CycleNumber=1), 0=تمدید, NULL=بدون Cycle
PackageName -- نام پکیج اون هفته
PackageId -- شناسه پکیج
```
**نکته:** منطق هفته‌بندی از `cm.ActivatedAt` به `Cycle.PackagePurchasedAt` تغییر کرد.
**Deploy SP:**
```
SP مستقیم روی DB اجرا شد (نه EmbeddedResource Infrastructure)
اجرا شد در: /tmp/DbDiag با C# script
تأیید شد: SELECT OBJECT_ID('[CMS].[GetNetworkTree]') → موجود
```
#### ب) Application Layer
**`NetworkTreeNodeDto.cs`** — فیلدهای جدید:
```csharp
bool? IsNewActivation
string? PackageName
long? PackageId
```
**`NetworkTreeDto.cs`** — همین فیلدها
**`GetNetworkTreeQueryHandler.cs`**:
```csharp
// خواندن از DataReader:
IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation"))
? null
: reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1,
PackageName = reader["PackageName"] as string,
PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId"))
? null
: reader.GetInt64(reader.GetOrdinal("PackageId"))
```
#### ج) Proto (networkmembership.proto)
```protobuf
// NetworkTreeNodeModel — فیلدهای جدید:
google.protobuf.BoolValue is_new_activation = 22;
string package_name = 23;
google.protobuf.Int64Value package_id = 24;
```
**NuGet Package:** `Foursat.CMSMicroservice.Protobuf` → از `0.0.194` به **`0.0.195`** bump و push شد.
#### د) Mapping (NetworkMembershipProfile.cs)
```csharp
PackageName = node.PackageName ?? string.Empty,
PackageId = node.PackageId.HasValue ? node.PackageId.Value : null,
IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null
```
#### هـ) BackOffice — NetworkTreeViewer.razor
**DataGrid — دو ستون جدید:**
```razor
<!-- ستون نوع فعالسازی -->
<PropertyColumn Property="x => x.IsNewActivation" Title="نوع فعالسازی">
@if (context.Item.IsNewActivation == true)
{
<MudChip Color="Color.Success">🆕 عضو جدید</MudChip>
}
else if (context.Item.IsNewActivation == false)
{
<MudChip Color="Color.Secondary">🔄 خرید مجدد</MudChip>
}
</PropertyColumn>
<!-- ستون پکیج -->
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
```
**JS (jsNodes):**
```js
isNewActivation: n.IsNewActivation,
packageName: n.PackageName ?? ""
```
---
## ۴. بهبود UI نمودار درختی
### مشکل اولیه
بج «🆕 جدید» با `position: absolute` از گوشه کارت بیرون می‌زد و با محتوای دیگر برخورد می‌کرد.
### فایل‌های تغییریافته
#### `admin-org-chart.js` (wwwroot/js)
**ساختار کارت بازنویسی شد:**
```
┌─────────────────────────────┐
│ [Avatar] نام کاربر │
│ پکیج نقره... │ ← inline زیر اسم
│ L13 چپ عضو جدید │ ← pill در meta row
├─────────────────────────────┤
│ ✓ فعال 1404/12/24 │
└─────────────────────────────┘
```
**تغییرات:**
- `activationTypeBadge` (absolute positioning) → `activationTypePill` (inline span)
- `highlightBadge` (✨ floating) → حذف شد
- `packageBadge` به زیر اسم کاربر منتقل شد (نه footer)
- ابعاد کارت: `160×80``178×92` px
#### `admin-org-chart.css` (wwwroot/css)
```css
/* جدید: activation pill به جای badge */
.admin-node-card .activation-pill { /* inline flex */ }
.admin-node-card .new-member-pill { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; }
.admin-node-card .renewal-pill { background: #ede7f6; color: #5e35b1; border: 1px solid #b39ddb; }
/* بهبود: پکیج روشن‌تر */
.admin-node-card .package-name-badge { background: #eceff1; color: #546e7a; border: 1px solid #b0bec5; }
```
---
## ۵. رفع باگ Pagination فروشگاه تخفیف
### مشکل
در صفحه `/discount-store/products`، دکمه «نمایش محصولات بیشتر» کار نمی‌کرد — همیشه صفحه اول برمی‌گشت.
### ریشه مشکل
**فایل غایب:** `DiscountProductProfile.cs` (Mapster) وجود نداشت.
**جریان mapping:**
```
GetDiscountProductsRequest (proto)
↓ request.Adapt<GetDiscountProductsQuery>()
GetDiscountProductsQuery
```
بدون profile، auto-mapping دو مشکل داشت:
1. `request.SearchQuery (string)``query.SearchTerm (string?)` → نامتطابق نام، NULL می‌شد
2. `request.PageNumber (int)``query.PaginationQuery.PageNumber`**Mapster نمی‌توانست به nested object مپ کند**`PaginationQuery = null` → default: `PageNumber=1` همیشه!
### راه‌حل
**فایل جدید:** `CMS/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs`
```csharp
config.NewConfig<GetDiscountProductsRequest, GetDiscountProductsQuery>()
.Map(dest => dest.SearchTerm,
src => string.IsNullOrEmpty(src.SearchQuery) ? null : src.SearchQuery)
.Map(dest => dest.CategoryId,
src => src.CategoryId != null ? src.CategoryId.Value : (long?)null)
.Map(dest => dest.IsActive,
src => src.IsActive != null ? src.IsActive.Value : (bool?)null)
.Map(dest => dest.PaginationQuery, src => new PaginationState
{
PageNumber = src.PageNumber > 0 ? src.PageNumber : 1,
PageSize = src.PageSize > 0 ? src.PageSize : 12
});
```
همچنین `GetDiscountProductsResponseDto → GetDiscountProductsResponse` هم به صورت صریح مپ شد تا `MetaData` و `Models` درست انتقال یابند.
---
## خلاصه فایل‌های تغییریافته
| فایل | نوع تغییر | پروژه |
|------|-----------|-------|
| `ActivateClubMembershipCommandHandler.cs` | Fix — حذف Double-Charge | CMS Application |
| `AcceptClubMembershipContractCommandHandler.cs` | Fix — Cross-week Pool | CMS Application |
| `CreateManualPaymentCommandHandler.cs` | Fix — Cross-week Pool | CMS Application |
| `sp_CalculateWeeklyCommissionPool.sql` | Fix — IsCurrentCycle | CMS Infrastructure |
| `SP_GetNetworkTree.sql` | Feature — IsNewActivation, PackageName, PackageId | DB/dbbkup |
| `NetworkTreeNodeDto.cs` | Feature — فیلدهای جدید | CMS Application |
| `NetworkTreeDto.cs` | Feature — فیلدهای جدید | CMS Application |
| `GetNetworkTreeQueryHandler.cs` | Feature — خواندن فیلدهای جدید | CMS Application |
| `networkmembership.proto` | Feature — ۳ فیلد جدید در NetworkTreeNodeModel | Protobuf |
| `NetworkMembershipProfile.cs` | Feature — mapping فیلدهای جدید | CMS WebApi |
| `NetworkTreeViewer.razor` | Feature — DataGrid ستون‌های جدید | BackOffice |
| `admin-org-chart.js` | Feature+Fix — inline pill، پکیج زیر اسم | BackOffice wwwroot |
| `admin-org-chart.css` | Feature+Fix — استایل pill‌های مرتب | BackOffice wwwroot |
| `DiscountProductProfile.cs` | Fix — Pagination mapping صحیح | CMS WebApi (جدید) |
### NuGet Package
| پکیج | نسخه قبل | نسخه جدید |
|------|----------|-----------|
| `Foursat.CMSMicroservice.Protobuf` | 0.0.194 | **0.0.195** |
---
## وظایف باقی‌مانده
### ضروری — اصلاح داده‌های DB
```sql
BEGIN TRANSACTION;
-- هفته ۲۲: Pool Ghost (هانیه سادات عشاقی ← AcceptContract هفته اشتباه)
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 0, LastModified = GETUTCDATE()
WHERE Id = 10056 AND WeekDefinitionId = 22;
-- هفته ۲۳: Pool ناقص (محمدصادق عسلی ← Pool قبل از Fix ایجاد شده بود)
UPDATE CMS.WeeklyCommissionPools
SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE()
WHERE Id = 10054 AND WeekDefinitionId = 23;
COMMIT;
```
### بهبود آینده
- [ ] `SP_GetNetworkTree.sql` به Infrastructure EmbeddedResource اضافه شود (auto-deploy)
- [ ] `DiscountProductDto` در Application — اضافه کردن فیلد `Created` از DB
- [ ] تست pagination فروشگاه پس از restart CMS