update
This commit is contained in:
@@ -0,0 +1,851 @@
|
||||
# 🔴 تحلیل مغایرت 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)
|
||||
Reference in New Issue
Block a user