# Plan: فعال‌سازی مرحله‌ای Handler های کامنت‌شده **تاریخ:** 8 دسامبر 2025 **وضعیت:** در انتظار اجرا **تعداد کل فایل‌های کامنت‌شده:** 37 فایل در 3 دسته --- ## خلاصه اجرایی در `BackOffice.BFF.Application.csproj` سه دسته handler کامنت شده‌اند: 1. **DiscountOrderCQ/** (18 فایل) - عدم تطابق Proto با CMS 2. **DiscountShoppingCartCQ/** (16 فایل) - عدم تطابق Proto 3. **CommissionCQ/Commands/ProcessWithdrawal/** (3 فایل) - مشکل تبدیل StringValue --- ## مرحله 1: ProcessWithdrawal (30 دقیقه - ساده ✅) ### ویژگی بیزینسی مدیریت درخواست‌های برداشت کمیسیون توسط ادمین (تایید/رد) ### فایل‌های دخیل ``` CommissionCQ/Commands/ProcessWithdrawal/ ├── ProcessWithdrawalCommand.cs ├── ProcessWithdrawalCommandHandler.cs └── ProcessWithdrawalCommandValidator.cs ``` ### مشکل فعلی ```csharp // Handler - خط 25 var grpcRequest = new ProcessWithdrawalRequest { PayoutId = request.WithdrawalId, IsApproved = true, // ⚠️ هاردکد شده! Reason = request.AdminNote != null ? new StringValue { Value = request.AdminNote } : null }; ``` ### تغییرات مورد نیاز #### 1. آپدیت Command ```csharp // ProcessWithdrawalCommand.cs public record ProcessWithdrawalCommand : IRequest { public long WithdrawalId { get; init; } public bool IsApproved { get; init; } // ✅ جدید public string? Reason { get; init; } // نام‌گذاری مجدد از AdminNote } ``` #### 2. آپدیت Handler ```csharp // ProcessWithdrawalCommandHandler.cs var grpcRequest = new ProcessWithdrawalRequest { PayoutId = request.WithdrawalId, IsApproved = request.IsApproved, // ✅ از command گرفته شود Reason = !string.IsNullOrEmpty(request.Reason) ? new StringValue { Value = request.Reason } : null }; var response = await _context.Commissions.ProcessWithdrawalAsync( grpcRequest, cancellationToken); return new ProcessWithdrawalResponseDto { Success = true, Message = "Withdrawal processed successfully" }; ``` #### 3. Uncomment WebApi Service ```csharp // CommissionService.cs - خطوط 90-96 public override async Task ProcessWithdrawal( ProcessWithdrawalRequest request, ServerCallContext context) { await _dispatchRequestToCQRS.Handle< ProcessWithdrawalRequest, ProcessWithdrawalCommand, ProcessWithdrawalResponseDto>(request, context); return new Empty(); } ``` #### 4. حذف از Exclude List ```xml ``` ### چک‌لیست - [ ] آپدیت ProcessWithdrawalCommand با فیلد IsApproved - [ ] آپدیت ProcessWithdrawalCommandHandler - حذف hardcode - [ ] Uncomment متد در CommissionService.cs - [ ] حذف از exclude در Application.csproj - [ ] Build و تست --- ## مرحله 2: DiscountShoppingCartCQ (2-3 ساعت - متوسط 📊) ### ویژگی بیزینسی مدیریت سبد خرید فروشگاه تخفیف برای مشتریان ### فایل‌های دخیل ``` DiscountShoppingCartCQ/ ├── Commands/ (12 فایل) │ ├── AddToCart/ ✅ تطابق دارد │ ├── RemoveFromCart/ ✅ تطابق دارد │ ├── UpdateCartItemCount/ ✅ تطابق دارد │ └── ClearCart/ ✅ تطابق دارد └── Queries/ (4 فایل) ├── GetUserCart/ ⚠️ نیاز به Mapster └── سایر queries... ``` ### مشکل فعلی: GetUserCartResponse #### Handler انتظار دارد: ```csharp public class GetUserCartResponseDto { public long UserId { get; set; } // ❌ در CMS proto نیست public decimal TotalPrice { get; set; } public decimal TotalDiscountedPrice { get; set; } // ❌ نام متفاوت public decimal TotalSavings { get; set; } // ❌ نام متفاوت public List Items { get; set; } } public class CartItemDto { public long Id { get; set; } // ❌ در CMS proto نیست public decimal DiscountedPrice { get; set; } // ❌ نام: final_price public DateTime AddedAt { get; set; } // ❌ نام: created (Timestamp) } ``` #### CMS Proto دارد: ```protobuf message GetUserCartResponse { repeated CartItemDto items = 1; int64 total_price = 2; int64 total_discount_amount = 3; // ← TotalSavings int64 final_price = 4; // ← TotalDiscountedPrice } message CartItemDto { int64 product_id = 1; string product_title = 2; string product_image_path = 3; int64 unit_price = 4; int32 max_discount_percent = 5; int32 count = 6; int64 total_price = 7; int64 discount_amount = 8; int64 final_price = 9; // ← DiscountedPrice int32 product_remaining_count = 10; google.protobuf.Timestamp created = 11; // ← AddedAt } ``` ### تغییرات مورد نیاز #### 1. ساخت Mapster Profile ```csharp // BackOffice.BFF.WebApi/Common/Mappings/DiscountShoppingCartProfile.cs using Mapster; using BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart; using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart; namespace BackOffice.BFF.WebApi.Common.Mappings; public class DiscountShoppingCartProfile : IRegister { void IRegister.Register(TypeAdapterConfig config) { // Map GetUserCart Response config.NewConfig() .MapWith(src => new GetUserCartResponseDto { // UserId باید از request گرفته شود (در handler) TotalPrice = src.TotalPrice, TotalDiscountedPrice = src.FinalPrice, TotalSavings = src.TotalDiscountAmount, Items = src.Items.Select(item => new CartItemDto { // Id ندارد - می‌تواند 0 باشد یا از product_id استفاده شود Id = item.ProductId, ProductId = item.ProductId, ProductTitle = item.ProductTitle, ProductImagePath = item.ProductImagePath, UnitPrice = item.UnitPrice, MaxDiscountPercent = item.MaxDiscountPercent, Count = item.Count, TotalPrice = item.TotalPrice, DiscountedPrice = item.FinalPrice, AddedAt = item.Created.ToDateTime() }).ToList() }); } } ``` #### 2. آپدیت Handler ```csharp // GetUserCartQueryHandler.cs public async Task Handle( GetUserCartQuery request, CancellationToken cancellationToken) { var grpcRequest = new GetUserCartRequest { UserId = request.UserId }; var response = await _context.DiscountShoppingCarts.GetUserCartAsync( grpcRequest, cancellationToken: cancellationToken); var result = TypeAdapter.Adapt( response, response.GetType(), typeof(GetUserCartResponseDto)) as GetUserCartResponseDto; // UserId را از request می‌گیریم چون در proto نیست result.UserId = request.UserId; return result; } ``` #### 3. حذف از Exclude List ```xml ``` ### چک‌لیست - [ ] ساخت DiscountShoppingCartProfile.cs - [ ] آپدیت GetUserCartQueryHandler با Mapster - [ ] تست mapping با داده واقعی - [ ] حذف از exclude در Application.csproj - [ ] Build و تست endpoint ### سوال کلیدی ⚠️ **آیا BackOffice.BFF نیاز به مدیریت سبد خرید دارد؟** - اگر خیر: Document کنیم "Not applicable - FrontOffice only" - اگر بله: Mapster profile بسازیم --- ## مرحله 3: DiscountOrderCQ (6-8 ساعت - پیچیده 🔴) ### ویژگی بیزینسی مدیریت سفارش‌های فروشگاه تخفیف توسط ادمین ### فایل‌های دخیل ``` DiscountOrderCQ/ ├── Commands/ (10 فایل) │ ├── PlaceOrder/ ⚠️ Proto مختلف │ ├── CompletePayment/ ⚠️ Proto مختلف │ ├── UpdateOrderStatus/ ⚠️ Proto مختلف │ └── سایر commands... └── Queries/ (8 فایل) ├── GetOrderById/ ⚠️ Proto مختلف ├── GetAllUserOrders/ ⚠️ Proto مختلف └── سایر queries... ``` ### مشکل اصلی: دو Proto متفاوت #### تفاوت‌های کلیدی **1. PlaceOrderRequest** Handler انتظار دارد: ```csharp UserId, AddressId, DiscountBalanceAmount, GatewayAmount ``` CMS Proto دارد: ```protobuf user_id, user_address_id, discount_balance_to_use, notes (StringValue) ``` **2. PlaceOrderResponse** Handler انتظار دارد: ```csharp OrderId, TrackingCode, RequiresGatewayPayment, GatewayPayableAmount ``` CMS Proto دارد: ```protobuf success, message, order_id, gateway_amount, payment_url (StringValue) ``` **3. GetOrderByIdResponse - تفاوت اصلی** Handler انتظار دارد: ```csharp public class OrderDto { public long Id { get; set; } public int Status { get; set; } // ← int public string StatusTitle { get; set; } // ← محاسبه شده public bool IsPaid { get; set; } // ← bool public DateTime? PaidAt { get; set; } public string UserFullName { get; set; } public string UserMobile { get; set; } public string DeliveryAddress { get; set; } // ← string // ... } ``` CMS Proto دارد: ```protobuf message OrderDto { int64 id = 1; DeliveryStatus delivery_status = 2; // ← enum bool payment_completed = 3; // ← bool google.protobuf.Timestamp created = 4; AddressDto address = 5; // ← object // StatusTitle ندارد - باید derive شود // PaidAt ندارد - باید از created استفاده شود } enum DeliveryStatus { DELIVERY_PENDING = 0; DELIVERY_PROCESSING = 1; DELIVERY_SHIPPED = 2; DELIVERY_DELIVERED = 3; DELIVERY_CANCELLED = 4; } ``` ### تصمیم کلیدی ⚠️ **گزینه A: استفاده از CMS Proto (پیشنهادی)** - ✅ CMS قبلاً پیاده‌سازی شده - ✅ ریسک کمتر - ❌ نیاز به rewrite کردن 18 handler - ❌ 6-8 ساعت کار **گزینه B: استفاده از BFF Proto** - ✅ Handler ها آماده هستند - ❌ نیاز به آپدیت CMS microservice - ❌ ریسک بالا - ❌ تست‌های بیشتر ### تغییرات مورد نیاز (گزینه A) #### 1. ساخت Mapster Profile جامع ```csharp // DiscountOrderProfile.cs public class DiscountOrderProfile : IRegister { void IRegister.Register(TypeAdapterConfig config) { // PlaceOrder Command → CMS Request config.NewConfig() .MapWith(src => new CMSPlaceOrderRequest { UserId = src.UserId, UserAddressId = src.AddressId, DiscountBalanceToUse = src.DiscountBalanceAmount, Notes = !string.IsNullOrEmpty(src.Notes) ? new StringValue { Value = src.Notes } : null }); // CMS Response → DTO config.NewConfig() .MapWith(src => new PlaceOrderResponseDto { OrderId = src.OrderId, TrackingCode = src.OrderId.ToString(), RequiresGatewayPayment = src.GatewayAmount > 0, GatewayPayableAmount = src.GatewayAmount, PaymentUrl = src.PaymentUrl?.Value }); // GetOrderById - پیچیده‌ترین mapping config.NewConfig() .MapWith(src => new OrderDto { Id = src.Id, Status = (int)src.DeliveryStatus, StatusTitle = GetStatusTitle(src.DeliveryStatus), IsPaid = src.PaymentCompleted, PaidAt = src.PaymentCompleted ? src.Created.ToDateTime() : null, UserFullName = src.UserFullName, UserMobile = src.UserMobile, DeliveryAddress = FormatAddress(src.Address), // ... بقیه فیلدها }); // GetAllUserOrders config.NewConfig() .MapWith(src => new GetAllUserOrdersResponseDto { MetaData = new MetaData { PageNumber = src.MetaData.CurrentPage, PageSize = src.MetaData.PageSize, TotalPages = src.MetaData.TotalPage, TotalCount = src.MetaData.TotalCount }, Orders = src.Orders.Select(o => new OrderSummaryDto { Id = o.Id, Status = (int)o.DeliveryStatus, StatusTitle = GetStatusTitle(o.DeliveryStatus), // ... }).ToList() }); } private static string GetStatusTitle(DeliveryStatus status) => status switch { DeliveryStatus.DeliveryPending => "در انتظار پردازش", DeliveryStatus.DeliveryProcessing => "در حال پردازش", DeliveryStatus.DeliveryShipped => "ارسال شده", DeliveryStatus.DeliveryDelivered => "تحویل داده شده", DeliveryStatus.DeliveryCancelled => "لغو شده", _ => "نامشخص" }; private static string FormatAddress(AddressDto address) { if (address == null) return string.Empty; return $"{address.Province}, {address.City}, {address.Street}, " + $"پلاک {address.PlateNumber}, واحد {address.Unit}"; } } ``` #### 2. بازنویسی Handlers (نمونه) ```csharp // PlaceOrderCommandHandler.cs public async Task Handle( PlaceOrderCommand request, CancellationToken cancellationToken) { var grpcRequest = TypeAdapter.Adapt( request, request.GetType(), typeof(CMSPlaceOrderRequest)) as CMSPlaceOrderRequest; var response = await _context.DiscountOrders.PlaceOrderAsync( grpcRequest, cancellationToken: cancellationToken); return TypeAdapter.Adapt( response, response.GetType(), typeof(PlaceOrderResponseDto)) as PlaceOrderResponseDto; } ``` #### 3. حذف از Exclude List ```xml ``` ### چک‌لیست - [ ] تصمیم‌گیری: CMS proto یا BFF proto؟ - [ ] مقایسه دقیق line-by-line دو proto - [ ] ساخت DiscountOrderProfile.cs جامع - [ ] بازنویسی PlaceOrderCommandHandler - [ ] بازنویسی CompletePaymentCommandHandler - [ ] بازنویسی UpdateOrderStatusCommandHandler - [ ] بازنویسی GetOrderByIdQueryHandler - [ ] بازنویسی GetAllUserOrdersQueryHandler - [ ] تست کامل flow: Place → Pay → Update → Get - [ ] حذف از exclude در Application.csproj - [ ] Build و تست همه endpoints --- ## جدول خلاصه | Handler Group | تعداد فایل | Proto Source | BFF Proto | Mapster Profile | پیچیدگی | زمان تخمینی | اولویت بیزینسی | |---------------|-----------|--------------|-----------|-----------------|---------|-------------|----------------| | ProcessWithdrawal | 3 | BFF.Commission | ✅ | ❌ نیاز نیست | ساده | 30 دقیقه | متوسط ⚠️ | | DiscountShoppingCart | 16 | CMS | ✅ (متفاوت) | ❌ باید ساخت | متوسط | 2-3 ساعت | متوسط ⚠️ | | DiscountOrder | 18 | CMS | ✅ (کاملاً متفاوت) | ❌ باید ساخت | پیچیده | 6-8 ساعت | بالا 🔴 | | **جمع کل** | **37** | - | - | - | - | **8.5-11.5 ساعت** | - | --- ## سوالات کلیدی برای تصمیم‌گیری ### 1. DiscountShoppingCartCQ **سوال:** آیا BackOffice نیاز به مدیریت سبد خرید دارد؟ - اگر **خیر**: این feature فقط برای FrontOffice است → Document و نگه‌داری exclude - اگر **بله**: ادمین باید بتواند سبد خرید کاربران را ببیند → Mapster profile بسازیم ### 2. DiscountOrderCQ **سوال:** کدام proto را استفاده کنیم؟ - **گزینه A (پیشنهادی)**: CMS proto - Handler ها را rewrite می‌کنیم - 6-8 ساعت کار - ریسک کم - **گزینه B**: BFF proto - CMS microservice را آپدیت می‌کنیم - زمان نامشخص - ریسک بالا ### 3. اولویت اجرا کدام مرحله اول اجرا شود؟ - **پیشنهاد:** ProcessWithdrawal (سریع‌ترین ROI) - سپس: بر اساس نیاز بیزینسی --- ## مراحل بعدی ### فوری 1. ✅ تصمیم: DiscountShoppingCart نیاز هست؟ 2. ✅ تصمیم: DiscountOrder از کدام proto؟ 3. ✅ شروع با ProcessWithdrawal (30 دقیقه) ### کوتاه‌مدت 4. اگر نیاز: DiscountShoppingCart (2-3 ساعت) 5. Planning دقیق DiscountOrder (1 ساعت) ### میان‌مدت 6. پیاده‌سازی DiscountOrder (6-8 ساعت) 7. تست integration کامل 8. Document کردن تغییرات --- **تاریخ آخرین آپدیت:** 8 دسامبر 2025 **وضعیت:** منتظر تصمیم‌گیری و شروع اجرا **مسئول:** تیم توسعه BackOffice.BFF