Files
docs/03-BACKEND/FrontOffice.BFF/protobuf-mismatch.md
T
masoodafar-web 119e870a26 feat: Complete overhaul of FourSat documentation structure and content
- Added FINAL-STATUS.md detailing project completion and key metrics
- Created QUICK-REFERENCE.md for quick access to essential documents
- Updated README.md with project overview and quick start guide
- Established STRUCTURE.md outlining the final documentation structure
- Organized and archived old files, ensuring a clean and efficient directory
- Enhanced documentation quality with comprehensive metrics and checklists
2025-12-04 17:32:31 +03:30

28 KiB

🔴 تحلیل مغایرت 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)

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

کد فعلی:

var response = await _context.ClubMemberships.GetClubMembershipAsync(cmsRequest, cancellationToken: cancellationToken);

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

CMS Proto:

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;
}

🔧 راه حل:

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

⚠️ نکته مهم: CMS حالا یک لیست features نیز بر می‌گرداند که باید به Response DTO اضافه شود:

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 نیاز به بررسی دارد.

کد فعلی:

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 گرفته شود.

🔧 راه حل:

// بعد از فعال‌سازی، 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)

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

کد فعلی:

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:

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 بسازیم:

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

کد فعلی:

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:

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 (سریع‌تر)

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 اضافه کن:

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)

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

کد فعلی:

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:

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;
}

🔧 راه حل:

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 نیز باید بروز شود:

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:

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:

rpc RequestWithdrawal(RequestWithdrawalRequest) returns (Empty);
rpc GetWithdrawalRequests(GetWithdrawalRequestsRequest) returns (GetWithdrawalRequestsResponse);

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


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

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

1.1. ClubMembershipCQ

فایل: GetMyClubMembershipQueryHandler.cs

// ❌ کد قدیمی:
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

// ✅ بعد از 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

// ✅ کامل بازنویسی با 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

// ✅ استفاده از 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

// ❌ کد قدیمی:
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:

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

// 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

// 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
  • 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)