From 1db77b1a1b00a79cb2ed46d019890137a7cf5848 Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Fri, 1 May 2026 00:07:25 +0330 Subject: [PATCH] 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. --- docs/DB-INTEGRITY-AUDIT-20260417.md | 356 +++++++++++++++++++++ technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md | 246 ++++++++++++++ technical/TECH-07-SESSION-LOG-20260430.md | 334 +++++++++++++++++++ 3 files changed, 936 insertions(+) create mode 100644 docs/DB-INTEGRITY-AUDIT-20260417.md create mode 100644 technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md create mode 100644 technical/TECH-07-SESSION-LOG-20260430.md diff --git a/docs/DB-INTEGRITY-AUDIT-20260417.md b/docs/DB-INTEGRITY-AUDIT-20260417.md new file mode 100644 index 0000000..61a4190 --- /dev/null +++ b/docs/DB-INTEGRITY-AUDIT-20260417.md @@ -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 ناسازگاری — آیا از ورود دستی موجودی بوده؟ + +--- + +*این گزارش فقط مستندات یافته‌ها است. هیچ تغییری در کد یا دیتابیس اعمال نشده است.* diff --git a/technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md b/technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md new file mode 100644 index 0000000..035f93b --- /dev/null +++ b/technical/TECH-06-CLUB-POOL-CHARGE-FLOW.md @@ -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: بازگشت از درگاه
?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 جبران می‌شد. diff --git a/technical/TECH-07-SESSION-LOG-20260430.md b/technical/TECH-07-SESSION-LOG-20260430.md new file mode 100644 index 0000000..c7da887 --- /dev/null +++ b/technical/TECH-07-SESSION-LOG-20260430.md @@ -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 + + + @if (context.Item.IsNewActivation == true) + { + 🆕 عضو جدید + } + else if (context.Item.IsNewActivation == false) + { + 🔄 خرید مجدد + } + + + + +``` + +**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 +``` + +بدون 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() + .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