Files
BackOffice.BFF/docs/EXCLUDED-HANDLERS-FIX-PLAN.md
masoodafar-web ce3b5db822
All checks were successful
Build and Deploy / build (push) Successful in 2m14s
feat: add Mapster profiles and enable DiscountOrder handlers
2025-12-08 21:10:21 +03:30

18 KiB
Raw Blame History

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

مشکل فعلی

// Handler - خط 25
var grpcRequest = new ProcessWithdrawalRequest
{
    PayoutId = request.WithdrawalId,
    IsApproved = true,  // ⚠️ هاردکد شده!
    Reason = request.AdminNote != null 
        ? new StringValue { Value = request.AdminNote } 
        : null
};

تغییرات مورد نیاز

1. آپدیت Command

// ProcessWithdrawalCommand.cs
public record ProcessWithdrawalCommand : IRequest<ProcessWithdrawalResponseDto>
{
    public long WithdrawalId { get; init; }
    public bool IsApproved { get; init; }  // ✅ جدید
    public string? Reason { get; init; }   // نام‌گذاری مجدد از AdminNote
}

2. آپدیت Handler

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

// CommissionService.cs - خطوط 90-96
public override async Task<Empty> ProcessWithdrawal(
    ProcessWithdrawalRequest request,
    ServerCallContext context)
{
    await _dispatchRequestToCQRS.Handle<
        ProcessWithdrawalRequest, 
        ProcessWithdrawalCommand, 
        ProcessWithdrawalResponseDto>(request, context);
    return new Empty();
}

4. حذف از Exclude List

<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="CommissionCQ/Commands/ProcessWithdrawal/**/*.cs" />

چک‌لیست

  • آپدیت 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 انتظار دارد:

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<CartItemDto> 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 دارد:

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

// 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<GetUserCartResponse, GetUserCartResponseDto>()
            .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

// GetUserCartQueryHandler.cs
public async Task<GetUserCartResponseDto> 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

<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="DiscountShoppingCartCQ/**/*.cs" />

چک‌لیست

  • ساخت 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 انتظار دارد:

UserId, AddressId, DiscountBalanceAmount, GatewayAmount

CMS Proto دارد:

user_id, user_address_id, discount_balance_to_use, notes (StringValue)

2. PlaceOrderResponse

Handler انتظار دارد:

OrderId, TrackingCode, RequiresGatewayPayment, GatewayPayableAmount

CMS Proto دارد:

success, message, order_id, gateway_amount, payment_url (StringValue)

3. GetOrderByIdResponse - تفاوت اصلی

Handler انتظار دارد:

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 دارد:

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 جامع

// DiscountOrderProfile.cs
public class DiscountOrderProfile : IRegister
{
    void IRegister.Register(TypeAdapterConfig config)
    {
        // PlaceOrder Command → CMS Request
        config.NewConfig<PlaceOrderCommand, CMSPlaceOrderRequest>()
            .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<CMSPlaceOrderResponse, PlaceOrderResponseDto>()
            .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<CMSOrderDto, OrderDto>()
            .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<CMSGetAllUserOrdersResponse, GetAllUserOrdersResponseDto>()
            .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 (نمونه)

// PlaceOrderCommandHandler.cs
public async Task<PlaceOrderResponseDto> 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

<!-- BackOffice.BFF.Application.csproj -->
<!-- حذف این خط: -->
<Compile Remove="DiscountOrderCQ/**/*.cs" />

چک‌لیست

  • تصمیم‌گیری: 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 دقیقه)

کوتاه‌مدت

  1. اگر نیاز: DiscountShoppingCart (2-3 ساعت)
  2. Planning دقیق DiscountOrder (1 ساعت)

میان‌مدت

  1. پیاده‌سازی DiscountOrder (6-8 ساعت)
  2. تست integration کامل
  3. Document کردن تغییرات

تاریخ آخرین آپدیت: 8 دسامبر 2025
وضعیت: منتظر تصمیم‌گیری و شروع اجرا
مسئول: تیم توسعه BackOffice.BFF