# 🔴 تحلیل مغایرت 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? 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 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 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 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 { 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 { 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 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 { public long PayoutId { get; set; } public int WithdrawalMethod { get; set; } // 0=Cash, 1=Diamond public string? IbanNumber { get; set; } } // Handler: public async Task 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)