# 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