# 🔴 تحلیل مغایرت Protobuf بین FrontOffice.BFF و CMS

> تاریخ: ۱۴ آذر ۱۴۰۴
> 
> این سند تمام مغایرت‌های موجود بین Handler های BFF و Protobuf های CMS را تحلیل می‌کند.

---

## 📊 خلاصه مشکلات

| ماژول | Handler های BFF | Proto های CMS | وضعیت | اولویت |
|------|-----------------|---------------|-------|--------|
| **ClubMembership** | ✅ 2 Handler | ⚠️ 7 RPC | نیاز به اصلاح | 🔴 بالا |
| **NetworkMembership** | ✅ 2 Handler | ⚠️ 7 RPC | نیاز به اصلاح | 🔴 بالا |
| **Commission** | ✅ 2 Handler | ⚠️ 16 RPC | نیاز به اصلاح | 🔴 بالا |
| **UserWallet** | ⚠️ 5 Handler | ✅ CMS API | نیاز به Query جدید | 🟡 متوسط |

---

## 1️⃣ ClubMembership - مغایرت‌ها

### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyClubMembership (Query)
✅ ActivateMyClubMembership (Command)
```

### 📋 CMS Protobuf (clubmembership.proto)
```protobuf
service ClubMembershipContract {
    // Commands
    rpc ActivateClubMembership(ActivateClubMembershipRequest) returns (Empty);
    rpc DeactivateClubMembership(DeactivateClubMembershipRequest) returns (Empty);
    rpc AssignFeatureToMembership(AssignFeatureToMembershipRequest) returns (Empty);
    
    // Queries
    rpc GetClubMembership(GetClubMembershipRequest) returns (GetClubMembershipResponse);
    rpc GetAllClubMemberships(GetAllClubMembershipsRequest) returns (GetAllClubMembershipsResponse);
    rpc GetClubMembershipHistory(GetClubMembershipHistoryRequest) returns (GetClubMembershipHistoryResponse);
    rpc GetClubStatistics(GetClubStatisticsRequest) returns (GetClubStatisticsResponse);
}
```

---

### ❌ مشکل 1: GetMyClubMembershipQueryHandler

**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/ClubMembershipCQ/Queries/GetMyClubMembership/GetMyClubMembershipQueryHandler.cs`

**کد فعلی**:
```csharp
var response = await _context.ClubMemberships.GetClubMembershipAsync(cmsRequest, cancellationToken: cancellationToken);

// استفاده از فیلدهای قدیمی:
var activationDate = response.ActivationDate?.ToDateTime();  // ❌ ActivationDate
var expirationDate = response.ExpirationDate?.ToDateTime();  // ❌ ExpirationDate
```

**CMS Proto**:
```protobuf
message GetClubMembershipResponse
{
    int64 id = 1;
    int64 user_id = 2;
    int64 package_id = 3;
    string package_name = 4;
    string activation_code = 5;
    google.protobuf.Timestamp activated_at = 6;  // ✅ activated_at (نام جدید)
    google.protobuf.Timestamp expires_at = 7;    // ✅ expires_at (نام جدید)
    bool is_active = 8;
    google.protobuf.Timestamp created = 9;
    repeated MembershipFeatureModel features = 10;
}
```

**🔧 راه حل**:
```csharp
// تغییر نام فیلدها:
var activationDate = response.ActivatedAt?.ToDateTime();  // ✅ ActivatedAt
var expirationDate = response.ExpiresAt?.ToDateTime();    // ✅ ExpiresAt
```

**⚠️ نکته مهم**: CMS حالا یک **لیست features** نیز بر می‌گرداند که باید به Response DTO اضافه شود:
```csharp
public class GetMyClubMembershipResponseDto
{
    // ... فیلدهای موجود
    public List<MembershipFeatureDto>? Features { get; set; }  // ✅ جدید
}

public class MembershipFeatureDto
{
    public long ProductId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public DateTime? ExpiresAt { get; set; }
    public bool IsActive { get; set; }
}
```

---

### ✅ ActivateMyClubMembershipCommandHandler

**وضعیت**: این Handler صحیح است، اما Response نیاز به بررسی دارد.

**کد فعلی**:
```csharp
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken: cancellationToken);

// ❌ Response Mock است:
return new ActivateMyClubMembershipResponseDto
{
    Success = true,
    Message = "عضویت باشگاه با موفقیت فعال شد",
    ActivationDate = DateTime.UtcNow,
    ExpirationDate = activationDate.AddMonths(request.DurationMonths),
    AmountPaid = 56_000_000 // ❌ Hardcoded
};
```

**مشکل**: CMS فقط `Empty` بر می‌گرداند، اطلاعات واقعی باید از `GetClubMembership` گرفته شود.

**🔧 راه حل**:
```csharp
// بعد از فعال‌سازی، GetClubMembership را صدا بزن:
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken);

var membershipRequest = new GetClubMembershipRequest { UserId = userId };
var membership = await _context.ClubMemberships.GetClubMembershipAsync(membershipRequest, cancellationToken);

return new ActivateMyClubMembershipResponseDto
{
    Success = true,
    Message = "عضویت باشگاه با موفقیت فعال شد",
    ActivationDate = membership.ActivatedAt?.ToDateTime(),
    ExpirationDate = membership.ExpiresAt?.ToDateTime(),
    AmountPaid = CalculatePackageCost(membership.PackageId, request.DurationMonths)  // محاسبه واقعی
};
```

---

## 2️⃣ NetworkMembership - مغایرت‌ها

### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyNetworkTree (Query)
✅ GetMyNetworkStatistics (Query)
```

### 📋 CMS Protobuf (networkmembership.proto)
```protobuf
service NetworkMembershipContract {
    // Commands
    rpc JoinNetwork(JoinNetworkRequest) returns (Empty);
    rpc ChangeNetworkParent(ChangeNetworkParentRequest) returns (Empty);
    rpc RemoveFromNetwork(RemoveFromNetworkRequest) returns (Empty);
    
    // Queries
    rpc GetUserNetwork(GetUserNetworkRequest) returns (GetUserNetworkResponse);
    rpc GetNetworkTree(GetNetworkTreeRequest) returns (GetNetworkTreeResponse);
    rpc GetNetworkMembershipHistory(GetNetworkMembershipHistoryRequest) returns (GetNetworkMembershipHistoryResponse);
    rpc GetNetworkStatistics(GetNetworkStatisticsRequest) returns (GetNetworkStatisticsResponse);
}
```

---

### ❌ مشکل 2: GetMyNetworkTreeQueryHandler

**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetMyNetworkTree/GetMyNetworkTreeQueryHandler.cs`

**کد فعلی**:
```csharp
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);

// استفاده از فیلد RootNode:
return new GetMyNetworkTreeResponseDto
{
    RootNode = MapToNetworkNode(response.RootNode, 0),  // ❌ RootNode
    TotalMembers = CountNodes(response.RootNode),
    CurrentDepth = CalculateDepth(response.RootNode)
};
```

**CMS Proto**:
```protobuf
message GetNetworkTreeResponse
{
    repeated NetworkTreeNodeModel nodes = 1;  // ✅ Flat list (نه Tree)
}

message NetworkTreeNodeModel
{
    int64 user_id = 1;
    string user_name = 2;
    google.protobuf.Int64Value parent_id = 3;
    int32 network_leg = 4;
    int32 network_level = 5;
    bool is_active = 6;
    google.protobuf.Timestamp joined_at = 7;
}
```

**🚨 مشکل بزرگ**: CMS حالا **Flat List** بر می‌گرداند نه **Tree Structure**!

**🔧 راه حل**: باید در BFF یک Tree Builder بسازیم:

```csharp
public async Task<GetMyNetworkTreeResponseDto> Handle(GetMyNetworkTreeQuery request, CancellationToken cancellationToken)
{
    var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();

    var cmsRequest = new GetNetworkTreeRequest
    {
        RootUserId = userId,
        MaxDepth = Math.Clamp(request.MaxDepth, 1, 10)
    };

    var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);

    // ✅ ساخت Tree از Flat List:
    var rootNode = BuildTreeFromFlatList(response.Nodes, userId);

    return new GetMyNetworkTreeResponseDto
    {
        RootNode = rootNode,
        TotalMembers = response.Nodes.Count,
        CurrentDepth = response.Nodes.Any() ? response.Nodes.Max(n => n.NetworkLevel) : 0
    };
}

private NetworkNodeDto? BuildTreeFromFlatList(IEnumerable<NetworkTreeNodeModel> nodes, long rootUserId)
{
    var nodeDict = nodes.ToDictionary(n => n.UserId);
    
    if (!nodeDict.ContainsKey(rootUserId))
        return null;

    NetworkNodeDto BuildNode(long userId, int level)
    {
        var cmsNode = nodeDict[userId];
        
        var node = new NetworkNodeDto
        {
            UserId = cmsNode.UserId,
            FullName = cmsNode.UserName,
            Mobile = string.Empty,  // CMS ندارد
            Avatar = null,
            Position = cmsNode.NetworkLeg == 0 ? "Left" : "Right",
            Level = level
        };

        // پیدا کردن children
        var leftChild = nodes.FirstOrDefault(n => n.ParentId == userId && n.NetworkLeg == 0);
        var rightChild = nodes.FirstOrDefault(n => n.ParentId == userId && n.NetworkLeg == 1);

        if (leftChild != null)
            node.LeftChild = BuildNode(leftChild.UserId, level + 1);
        
        if (rightChild != null)
            node.RightChild = BuildNode(rightChild.UserId, level + 1);

        return node;
    }

    return BuildNode(rootUserId, 0);
}
```

---

### ❌ مشکل 3: GetMyNetworkStatisticsQueryHandler

**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetMyNetworkStatistics/GetMyNetworkStatisticsQueryHandler.cs`

**کد فعلی**:
```csharp
var cmsRequest = new GetNetworkStatisticsRequest
{
    UserId = userId  // ❌ GetNetworkStatisticsRequest فیلد UserId ندارد!
};

var response = await _context.NetworkMemberships.GetNetworkStatisticsAsync(cmsRequest, cancellationToken);

// استفاده از فیلدهای قدیمی:
return new GetMyNetworkStatisticsResponseDto
{
    LeftLegCount = response.LeftLegCount,
    RightLegCount = response.RightLegCount,
    TotalMembers = response.TotalMembers,
    TreeDepth = response.TreeDepth,  // ❌ نام قدیمی
    WeakerLeg = weakerLeg,
    LastMember = response.LastMember != null ? new LastMemberDto { ... }  // ❌ LastMember وجود ندارد!
};
```

**CMS Proto**:
```protobuf
message GetNetworkStatisticsRequest
{
    // Empty - برای کل شبکه است نه یک کاربر خاص!
}

message GetNetworkStatisticsResponse
{
    int32 total_members = 1;
    int32 active_members = 2;
    int32 left_leg_count = 3;
    int32 right_leg_count = 4;
    double left_percentage = 5;
    double right_percentage = 6;
    double average_depth = 7;
    int32 max_depth = 8;  // ✅ max_depth (نه tree_depth)
    repeated LevelDistribution level_distribution = 9;
    repeated MonthlyGrowth monthly_growth = 10;
    repeated TopNetworkUser top_users = 11;
}
```

**🚨 مشکل بزرگ**: 
1. CMS دیگر `UserId` نمی‌گیرد - این Query برای کل شبکه است
2. فیلد `LastMember` وجود ندارد
3. Response خیلی جامع‌تر شده (LevelDistribution, MonthlyGrowth, TopUsers)

**🔧 راه حل**: باید یک Query جدید در CMS اضافه شود یا از `GetUserNetwork` استفاده کنیم:

### گزینه A: استفاده از GetUserNetwork (سریع‌تر)

```csharp
public async Task<GetMyNetworkStatisticsResponseDto> Handle(GetMyNetworkStatisticsQuery request, CancellationToken cancellationToken)
{
    var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();

    // ✅ استفاده از GetUserNetwork:
    var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
    var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken);

    // ✅ استفاده از GetNetworkTree برای شمارش:
    var treeRequest = new GetNetworkTreeRequest
    {
        RootUserId = userId,
        MaxDepth = 10  // Full tree
    };
    var tree = await _context.NetworkMemberships.GetNetworkTreeAsync(treeRequest, cancellationToken);

    var leftCount = tree.Nodes.Count(n => n.ParentId == userId && n.NetworkLeg == 0);
    var rightCount = tree.Nodes.Count(n => n.ParentId == userId && n.NetworkLeg == 1);

    var lastMember = tree.Nodes
        .Where(n => n.ParentId == userId)
        .OrderByDescending(n => n.JoinedAt)
        .FirstOrDefault();

    return new GetMyNetworkStatisticsResponseDto
    {
        LeftLegCount = leftCount,
        RightLegCount = rightCount,
        TotalMembers = tree.Nodes.Count,
        TreeDepth = tree.Nodes.Any() ? tree.Nodes.Max(n => n.NetworkLevel) : 0,
        WeakerLeg = leftCount < rightCount ? "Left" : "Right",
        LastMember = lastMember != null ? new LastMemberDto
        {
            UserId = lastMember.UserId,
            FullName = lastMember.UserName,
            Position = lastMember.NetworkLeg == 0 ? "Left" : "Right",
            JoinedAt = lastMember.JoinedAt?.ToDateTime() ?? DateTime.UtcNow
        } : null
    };
}
```

### گزینه B: اضافه کردن Query جدید به CMS (بهتر)

در `networkmembership.proto` اضافه کن:

```protobuf
rpc GetUserNetworkStatistics(GetUserNetworkStatisticsRequest) returns (GetUserNetworkStatisticsResponse);

message GetUserNetworkStatisticsRequest
{
    int64 user_id = 1;
}

message GetUserNetworkStatisticsResponse
{
    int32 left_leg_count = 1;
    int32 right_leg_count = 2;
    int32 total_children = 3;
    int32 max_depth = 4;
    string weaker_leg = 5; // "Left" | "Right"
    google.protobuf.Int64Value last_member_id = 6;
    google.protobuf.StringValue last_member_name = 7;
    google.protobuf.Timestamp last_joined_at = 8;
}
```

---

## 3️⃣ Commission - مغایرت‌ها

### 🟢 BFF Handlers (2 عدد - موجود)
```
✅ GetMyCommissionPayouts (Query)
✅ GetMyWeeklyBalances (Query)
```

### 📋 CMS Protobuf (commission.proto)
```protobuf
service CommissionContract {
    // Commands
    rpc CalculateWeeklyBalances(CalculateWeeklyBalancesRequest) returns (Empty);
    rpc CalculateWeeklyCommissionPool(CalculateWeeklyCommissionPoolRequest) returns (Empty);
    rpc ProcessUserPayouts(ProcessUserPayoutsRequest) returns (Empty);
    rpc RequestWithdrawal(RequestWithdrawalRequest) returns (Empty);
    rpc ProcessWithdrawal(ProcessWithdrawalRequest) returns (Empty);
    rpc ApproveWithdrawal(ApproveWithdrawalRequest) returns (Empty);
    rpc RejectWithdrawal(RejectWithdrawalRequest) returns (Empty);
    
    // Queries
    rpc GetWeeklyCommissionPool(GetWeeklyCommissionPoolRequest) returns (GetWeeklyCommissionPoolResponse);
    rpc GetUserCommissionPayouts(GetUserCommissionPayoutsRequest) returns (GetUserCommissionPayoutsResponse);
    rpc GetCommissionPayoutHistory(GetCommissionPayoutHistoryRequest) returns (GetCommissionPayoutHistoryResponse);
    rpc GetUserWeeklyBalances(GetUserWeeklyBalancesRequest) returns (GetUserWeeklyBalancesResponse);
    rpc GetAllWeeklyPools(GetAllWeeklyPoolsRequest) returns (GetAllWeeklyPoolsResponse);
    rpc GetWithdrawalRequests(GetWithdrawalRequestsRequest) returns (GetWithdrawalRequestsResponse);
    
    // Worker Control APIs
    rpc TriggerWeeklyCalculation(TriggerWeeklyCalculationRequest) returns (TriggerWeeklyCalculationResponse);
    rpc GetWorkerStatus(GetWorkerStatusRequest) returns (GetWorkerStatusResponse);
    rpc GetWorkerExecutionLogs(GetWorkerExecutionLogsRequest) returns (GetWorkerExecutionLogsResponse);
}
```

---

### ❌ مشکل 4: GetMyCommissionPayoutsQueryHandler

**فایل**: `FrontOffice.BFF/src/FrontOffice.BFF.Application/CommissionCQ/Queries/GetMyCommissionPayouts/GetMyCommissionPayoutsQueryHandler.cs`

**کد فعلی**:
```csharp
var cmsRequest = new GetUserCommissionPayoutsRequest
{
    UserId = userId,
    PageNumber = request.PageNumber,  // ❌ نام اشتباه
    PageSize = request.PageSize       // ❌ نام اشتباه
};

if (request.WeekNumber.HasValue)
    cmsRequest.WeekNumber = request.WeekNumber.Value;  // ❌ نوع داده اشتباه

if (request.Status.HasValue)
    cmsRequest.Status = request.Status.Value;
```

**CMS Proto**:
```protobuf
message GetUserCommissionPayoutsRequest
{
    google.protobuf.Int64Value user_id = 1;
    google.protobuf.Int32Value status = 2;
    google.protobuf.StringValue week_number = 3;  // ✅ string است (نه int)
    int32 page_index = 4;  // ✅ page_index (نه page_number)
    int32 page_size = 5;
}

message UserCommissionPayoutModel
{
    int64 id = 1;
    int64 user_id = 2;
    string user_name = 3;
    string week_number = 4;  // ✅ string است
    int32 balances_earned = 5;
    int64 value_per_balance = 6;
    int64 total_amount = 7;
    int32 status = 8;
    google.protobuf.Int32Value withdrawal_method = 9;
    string iban_number = 10;
    google.protobuf.Timestamp created = 11;
    google.protobuf.Timestamp last_modified = 12;
}
```

**🔧 راه حل**:
```csharp
var cmsRequest = new GetUserCommissionPayoutsRequest
{
    UserId = userId,
    PageIndex = request.PageNumber,   // ✅ PageIndex
    PageSize = request.PageSize
};

if (!string.IsNullOrEmpty(request.WeekNumber))
    cmsRequest.WeekNumber = request.WeekNumber;  // ✅ string

if (request.Status.HasValue)
    cmsRequest.Status = request.Status.Value;

// در DTO نیز باید تغییر کند:
var payouts = response.Models.Select(p => new CommissionPayoutDto
{
    Id = p.Id,
    WeekNumber = p.WeekNumber,  // ✅ string
    WeekLabel = $"هفته {p.WeekNumber}",
    BalancesEarned = p.BalancesEarned,
    ValuePerBalance = p.ValuePerBalance,  // ✅ جدید
    TotalAmount = p.TotalAmount,
    AmountFormatted = FormatCurrency(p.TotalAmount),
    Status = MapStatus(p.Status),
    StatusBadgeColor = GetStatusColor(p.Status),
    WithdrawalMethod = p.WithdrawalMethod?.ToString(),  // ✅ جدید
    IbanNumber = p.IbanNumber,  // ✅ جدید
    CalculatedDate = p.Created?.ToDateTime() ?? DateTime.UtcNow,
    LastModified = p.LastModified?.ToDateTime(),  // ✅ جدید
    DatePersian = FormatPersianDate(p.Created?.ToDateTime())
}).ToList();
```

**Query DTO نیز باید بروز شود**:
```csharp
public class GetMyCommissionPayoutsQuery : IRequest<GetMyCommissionPayoutsResponseDto>
{
    public string? WeekNumber { get; set; }  // ✅ string (نه int?)
    public int? Status { get; set; }
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 10;
}
```

---

### ❌ مشکل 5: GetMyWeeklyBalancesQueryHandler

**این Handler احتمالا وجود دارد اما بررسی نشده**. باید چک شود:

**CMS Proto**:
```protobuf
message GetUserWeeklyBalancesRequest
{
    google.protobuf.Int64Value user_id = 1;
    google.protobuf.StringValue week_number = 2;  // ✅ string
    bool only_active = 3;
    int32 page_index = 4;
    int32 page_size = 5;
}

message UserWeeklyBalanceModel
{
    int64 id = 1;
    int64 user_id = 2;
    string week_number = 3;  // ✅ string
    int32 left_leg_balances = 4;
    int32 right_leg_balances = 5;
    int32 total_balances = 6;
    int64 weekly_pool_contribution = 7;
    google.protobuf.Timestamp calculated_at = 8;
    bool is_expired = 9;
    google.protobuf.Timestamp created = 10;
}
```

**مشکل احتمالی**: نام فیلدها و نوع `week_number` (string vs int)

---

## 4️⃣ UserWallet - TODO Queries

### 🟡 BFF Handlers (5 عدد - بعضی کامنت شده)
```
✅ GetUserWallet (Query) - موجود
⚠️ GetAllUserWalletChangeLog (Query) - TODO
⚠️ WithdrawBalance (Command) - TODO
⚠️ GetUserWithdrawals (Query) - TODO
⚠️ GetWithdrawalSettings (Query) - TODO
```

این ها در `/FrontOffice/src/FrontOffice.Main/Utilities/WalletService.cs` کامنت شده‌اند.

**CMS API ها موجود هستند در `Commission` proto**:
```protobuf
rpc RequestWithdrawal(RequestWithdrawalRequest) returns (Empty);
rpc GetWithdrawalRequests(GetWithdrawalRequestsRequest) returns (GetWithdrawalRequestsResponse);
```

**⚠️ نکته**: `WithdrawBalance` در `Commission` است نه `UserWallet`!

---

## 📋 خلاصه اقدامات لازم

### فاز 1: اصلاح Handler های موجود (اولویت بالا) ⚡

#### 1.1. ClubMembershipCQ

**فایل**: `GetMyClubMembershipQueryHandler.cs`

```csharp
// ❌ کد قدیمی:
var activationDate = response.ActivationDate?.ToDateTime();
var expirationDate = response.ExpirationDate?.ToDateTime();

// ✅ کد جدید:
var activationDate = response.ActivatedAt?.ToDateTime();
var expirationDate = response.ExpiresAt?.ToDateTime();

// ✅ اضافه کردن Features:
Features = response.Features.Select(f => new MembershipFeatureDto
{
    ProductId = f.ProductId,
    ProductName = f.ProductName,
    Quantity = f.Quantity,
    ExpiresAt = f.ExpiresAt?.ToDateTime(),
    IsActive = f.IsActive
}).ToList()
```

**فایل**: `ActivateMyClubMembershipCommandHandler.cs`

```csharp
// ✅ بعد از Activate، GetClubMembership را صدا بزن:
await _context.ClubMemberships.ActivateClubMembershipAsync(grpcRequest, cancellationToken);

var membershipRequest = new GetClubMembershipRequest { UserId = userId };
var membership = await _context.ClubMemberships.GetClubMembershipAsync(membershipRequest, cancellationToken);

return new ActivateMyClubMembershipResponseDto
{
    Success = true,
    Message = "عضویت باشگاه با موفقیت فعال شد",
    ActivationDate = membership.ActivatedAt?.ToDateTime(),
    ExpirationDate = membership.ExpiresAt?.ToDateTime(),
    // AmountPaid باید از Package Service گرفته شود یا محاسبه شود
};
```

---

#### 1.2. NetworkMembershipCQ

**فایل**: `GetMyNetworkTreeQueryHandler.cs`

```csharp
// ✅ کامل بازنویسی با Tree Builder:
var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken);
var rootNode = BuildTreeFromFlatList(response.Nodes, userId);

return new GetMyNetworkTreeResponseDto
{
    RootNode = rootNode,
    TotalMembers = response.Nodes.Count,
    CurrentDepth = response.Nodes.Any() ? response.Nodes.Max(n => n.NetworkLevel) : 0
};

// اضافه کردن متد BuildTreeFromFlatList (کد کامل بالا)
```

**فایل**: `GetMyNetworkStatisticsQueryHandler.cs`

```csharp
// ✅ استفاده از GetUserNetwork + GetNetworkTree:
var userNetworkRequest = new GetUserNetworkRequest { UserId = userId };
var userNetwork = await _context.NetworkMemberships.GetUserNetworkAsync(userNetworkRequest, cancellationToken);

var treeRequest = new GetNetworkTreeRequest { RootUserId = userId, MaxDepth = 10 };
var tree = await _context.NetworkMemberships.GetNetworkTreeAsync(treeRequest, cancellationToken);

// محاسبه آمار (کد کامل بالا)
```

---

#### 1.3. CommissionCQ

**فایل**: `GetMyCommissionPayoutsQueryHandler.cs`

```csharp
// ❌ کد قدیمی:
var cmsRequest = new GetUserCommissionPayoutsRequest
{
    UserId = userId,
    PageNumber = request.PageNumber,
    PageSize = request.PageSize
};
if (request.WeekNumber.HasValue)
    cmsRequest.WeekNumber = request.WeekNumber.Value;

// ✅ کد جدید:
var cmsRequest = new GetUserCommissionPayoutsRequest
{
    UserId = userId,
    PageIndex = request.PageNumber,  // PageIndex
    PageSize = request.PageSize
};
if (!string.IsNullOrEmpty(request.WeekNumber))
    cmsRequest.WeekNumber = request.WeekNumber;  // string

// ✅ Response Mapping:
var payouts = response.Models.Select(p => new CommissionPayoutDto
{
    Id = p.Id,
    WeekNumber = p.WeekNumber,  // string
    ValuePerBalance = p.ValuePerBalance,  // جدید
    WithdrawalMethod = p.WithdrawalMethod,  // جدید
    IbanNumber = p.IbanNumber,  // جدید
    LastModified = p.LastModified?.ToDateTime()  // جدید
    // ... بقیه فیلدها
}).ToList();
```

**Query DTO**:
```csharp
public class GetMyCommissionPayoutsQuery
{
    public string? WeekNumber { get; set; }  // ✅ string
    // ...
}

public class CommissionPayoutDto
{
    public long ValuePerBalance { get; set; }  // ✅ جدید
    public string? WithdrawalMethod { get; set; }  // ✅ جدید
    public string? IbanNumber { get; set; }  // ✅ جدید
    public DateTime? LastModified { get; set; }  // ✅ جدید
    // ...
}
```

---

### فاز 2: اضافه کردن Handler های جدید (اولویت متوسط) 🟡

#### 2.1. UserWalletCQ - GetAllUserWalletChangeLog

```csharp
// Query:
public class GetAllUserWalletChangeLogQuery : IRequest<GetAllUserWalletChangeLogResponseDto>
{
    public long? ReferenceId { get; set; }
    public bool? IsIncrease { get; set; }
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}

// Handler:
public async Task<GetAllUserWalletChangeLogResponseDto> Handle(...)
{
    var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();

    // TODO: باید در CMS یک Query اضافه شود
    // فعلا از GetUserCommissionPayouts استفاده کنیم برای تاریخچه برداشت
    var request = new GetWithdrawalRequestsRequest
    {
        UserId = userId,
        PageIndex = request.PageNumber,
        PageSize = request.PageSize
    };

    var response = await _context.Commission.GetWithdrawalRequestsAsync(request, cancellationToken);

    // Mapping...
}
```

#### 2.2. UserWalletCQ - WithdrawBalance Command

```csharp
// Command:
public class WithdrawBalanceCommand : IRequest<WithdrawBalanceResponseDto>
{
    public long PayoutId { get; set; }
    public int WithdrawalMethod { get; set; }  // 0=Cash, 1=Diamond
    public string? IbanNumber { get; set; }
}

// Handler:
public async Task<WithdrawBalanceResponseDto> Handle(...)
{
    var userId = _currentUserService.UserId ?? throw new UnauthorizedAccessException();

    var request = new RequestWithdrawalRequest
    {
        PayoutId = command.PayoutId,
        WithdrawalMethod = command.WithdrawalMethod,
        IbanNumber = command.IbanNumber
    };

    await _context.Commission.RequestWithdrawalAsync(request, cancellationToken);

    return new WithdrawBalanceResponseDto
    {
        Success = true,
        Message = "درخواست برداشت ثبت شد"
    };
}
```

---

## ⏱️ تخمین زمان اجرا

| مرحله | فایل‌ها | زمان | اولویت |
|-------|---------|------|--------|
| ClubMembership fix | 2 Handler | 2 ساعت | 🔴 بالا |
| NetworkMembership fix | 2 Handler | 3-4 ساعت | 🔴 بالا |
| Commission fix | 2 Handler | 2 ساعت | 🔴 بالا |
| UserWallet new Queries | 4 Handler | 3 ساعت | 🟡 متوسط |
| Testing & Build | - | 2 ساعت | 🟢 پایین |
| **جمع کل** | **10 Handler** | **12-15 ساعت** | |

---

## ✅ Checklist اجرایی

### مرحله 1: ClubMembershipCQ
- [ ] GetMyClubMembershipQueryHandler: تغییر ActivationDate → ActivatedAt
- [ ] GetMyClubMembershipQueryHandler: تغییر ExpirationDate → ExpiresAt
- [ ] GetMyClubMembershipResponseDto: اضافه کردن List<MembershipFeatureDto>
- [ ] ActivateMyClubMembershipCommandHandler: گرفتن داده واقعی از GetClubMembership

### مرحله 2: NetworkMembershipCQ
- [ ] GetMyNetworkTreeQueryHandler: پیاده‌سازی BuildTreeFromFlatList
- [ ] GetMyNetworkTreeQueryHandler: حذف استفاده از response.RootNode
- [ ] GetMyNetworkStatisticsQueryHandler: حذف فیلد UserId از Request
- [ ] GetMyNetworkStatisticsQueryHandler: استفاده از GetUserNetwork + GetNetworkTree
- [ ] GetMyNetworkStatisticsResponseDto: نام TreeDepth → MaxDepth

### مرحله 3: CommissionCQ
- [ ] GetMyCommissionPayoutsQuery: تغییر WeekNumber از int? به string?
- [ ] GetMyCommissionPayoutsQueryHandler: PageNumber → PageIndex
- [ ] CommissionPayoutDto: اضافه کردن ValuePerBalance, WithdrawalMethod, IbanNumber, LastModified
- [ ] GetMyWeeklyBalancesQueryHandler: بررسی و اصلاح (اگر لازم باشد)

### مرحله 4: UserWalletCQ
- [ ] Query: GetAllUserWalletChangeLog ساخته شود
- [ ] Command: WithdrawBalance ساخته شود
- [ ] Query: GetUserWithdrawals ساخته شود (از GetWithdrawalRequests استفاده کند)
- [ ] Query: GetWithdrawalSettings ساخته شود
- [ ] WalletService.cs: uncomment کردن متدها

### مرحله 5: Build & Test
- [ ] dotnet build FrontOffice.BFF.sln
- [ ] dotnet build FrontOffice/src/FrontOffice.sln
- [ ] تست هر Handler با Postman/Swagger
- [ ] تست UI با داده واقعی

---

**📅 آخرین بروزرسانی**: ۱۴ آذر ۱۴۰۴  
**👤 توسط**: GitHub Copilot (Claude Sonnet 4.5)
