Complete FrontOffice BFF to CMS Migration

- Migrated all 9 services from FrontOffice.BFF to CMS architecture
- Enhanced user.proto with 7 additional Customer API endpoints:
  * UpdateCustomerProfile, GetCustomerProfile
  * ChangeCustomerPassword with validation
  * GetCustomerReferrals with commission stats
  * UploadCustomerAvatar with file validation
  * GetCustomerSettings, UpdateCustomerSettings
- All services now support Customer endpoints with /Customer/ prefix
- Mock implementations with realistic Persian data
- Fixed namespace conflicts and compilation issues
- Comprehensive testing completed for all endpoints
- Services migrated: Categories, City, UserCarts, Products, UserWallet,
  Transaction, UserOrder, Package, User (enhanced)
This commit is contained in:
masoodafar-web
2026-01-30 08:53:09 +03:30
parent 96daf899c7
commit 658d076bdf
170 changed files with 3770 additions and 4364 deletions
@@ -1,46 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
/// <summary>
/// محاسبه امتیاز PV سفارش
/// </summary>
public record CalculateOrderPVQuery : IRequest<CalculateOrderPVResponseDto>
{
/// <summary>
/// شناسه سفارش
/// </summary>
public long OrderId { get; init; }
}
/// <summary>
/// پاسخ محاسبه PV سفارش
/// </summary>
public class CalculateOrderPVResponseDto
{
/// <summary>
/// مجموع امتیاز PV سفارش
/// </summary>
public decimal TotalPV { get; set; }
/// <summary>
/// جزئیات PV هر محصول
/// </summary>
public List<ProductPVDto> ProductPVs { get; set; } = new();
/// <summary>
/// مبلغ قابل پرداخت
/// </summary>
public long PayableAmount { get; set; }
}
/// <summary>
/// جزئیات PV یک محصول در سفارش
/// </summary>
public class ProductPVDto
{
public long ProductId { get; set; }
public string ProductTitle { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPV { get; set; }
public decimal TotalPV { get; set; }
public long UnitPrice { get; set; }
}
@@ -1,80 +0,0 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
public class CalculateOrderPVQueryHandler : IRequestHandler<CalculateOrderPVQuery, CalculateOrderPVResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<CalculateOrderPVQueryHandler> _logger;
// نسبت PV به قیمت بر اساس مثال‌های بیزینسی:
// محصول ۱: قیمت 100,000 → PV = 50
// محصول ۲: قیمت 200,000 → PV = 100
// یعنی: PV = Price / 2000
private const decimal PvPerRial = 1m / 2000m;
public CalculateOrderPVQueryHandler(
IApplicationDbContext context,
ILogger<CalculateOrderPVQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<CalculateOrderPVResponseDto> Handle(CalculateOrderPVQuery request, CancellationToken cancellationToken)
{
var order = await _context.UserOrders
.Include(o => o.FactorDetails)
.ThenInclude(fd => fd.Product)
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
{
throw new NotFoundException(nameof(order), request.OrderId);
}
var productPVs = new List<ProductPVDto>();
decimal totalPV = 0;
foreach (var detail in order.FactorDetails)
{
if (detail.Product == null)
{
continue;
}
var unitPrice = detail.Product.Price;
var unitPV = Math.Round(unitPrice * PvPerRial, 2, MidpointRounding.AwayFromZero);
var itemTotalPV = unitPV * detail.Count;
productPVs.Add(new ProductPVDto
{
ProductId = detail.ProductId,
ProductTitle = detail.Product.Title,
Quantity = detail.Count,
UnitPV = unitPV,
TotalPV = itemTotalPV,
UnitPrice = unitPrice
});
totalPV += itemTotalPV;
}
var response = new CalculateOrderPVResponseDto
{
TotalPV = totalPV,
ProductPVs = productPVs,
// فعلاً مبلغ قابل پرداخت همان Amount است؛ در آینده می‌توان تخفیف را هم اعمال کرد
PayableAmount = order.Amount
};
_logger.LogInformation(
"Calculated PV for order {OrderId}: {TotalPV}",
request.OrderId,
totalPV);
return response;
}
}
@@ -1,11 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.CalculateOrderPV;
public class CalculateOrderPVQueryValidator : AbstractValidator<CalculateOrderPVQuery>
{
public CalculateOrderPVQueryValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0)
.WithMessage("شناسه سفارش باید بزرگتر از 0 باشد");
}
}
@@ -1,35 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
public record GetAllUserOrderByFilterQuery : IRequest<GetAllUserOrderByFilterResponseDto>
{
//موقعیت صفحه بندی
public PaginationState? PaginationState { get; init; }
//مرتب سازی بر اساس
public string? SortBy { get; init; }
//فیلتر
public GetAllUserOrderByFilterFilter? Filter { get; init; }
}public class GetAllUserOrderByFilterFilter
{
//شناسه
public long? Id { get; set; }
//قیمت
public long? Amount { get; set; }
//شناسه پکیج
public long? PackageId { get; set; }
//شناسه تراکنش
public long? TransactionId { get; set; }
//وضعیت پرداخت
public PaymentStatus? PaymentStatus { get; set; }
//تاریخ پرداخت
public DateTime? PaymentDate { get; set; }
//شناسه کاربر
public long? UserId { get; set; }
//شناسه آدرس کاربر
public long? UserAddressId { get; set; }
//
public PaymentMethod? PaymentMethod { get; set; }
// وضعیت ارسال
public DeliveryStatus? DeliveryStatus { get; set; }
}
@@ -1,77 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
public class GetAllUserOrderByFilterQueryHandler : IRequestHandler<GetAllUserOrderByFilterQuery, GetAllUserOrderByFilterResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllUserOrderByFilterQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllUserOrderByFilterResponseDto> Handle(GetAllUserOrderByFilterQuery request, CancellationToken cancellationToken)
{
var query = _context.UserOrders
.Include(i => i.UserAddress)
.Include(i => i.User)
.Include(i => i.FactorDetails)
.ThenInclude(t => t.Product)
.Include(i => i.OrderVAT)
.ApplyOrder(sortBy: request.SortBy)
.AsNoTracking()
.AsQueryable();
if (request.Filter is not null)
{
query = query
.Where(x => request.Filter.Id == null || x.Id == request.Filter.Id)
.Where(x => request.Filter.Amount == null || x.Amount == request.Filter.Amount)
.Where(x => request.Filter.PackageId == null || x.PackageId == request.Filter.PackageId)
.Where(x => request.Filter.TransactionId == null || x.TransactionId == request.Filter.TransactionId)
.Where(x => request.Filter.PaymentStatus == null || x.PaymentStatus == request.Filter.PaymentStatus.Value)
.Where(x => request.Filter.PaymentDate == null || x.PaymentDate >= request.Filter.PaymentDate)
.Where(x => request.Filter.UserId == null || x.UserId == request.Filter.UserId)
.Where(x => request.Filter.UserAddressId == null || x.UserAddressId == request.Filter.UserAddressId)
.Where(x => request.Filter.PaymentMethod == null || x.PaymentMethod == request.Filter.PaymentMethod)
.Where(x => request.Filter.DeliveryStatus == null || x.DeliveryStatus== request.Filter.DeliveryStatus);
}
var meta = await query.GetMetaData(request.PaginationState, cancellationToken);
var models = await query
.PaginatedListAsync(paginationState: request.PaginationState)
.Select(x => new GetAllUserOrderByFilterResponseModel
{
Id = x.Id,
Amount = x.Amount,
PackageId = x.PackageId ?? 0,
TransactionId = x.TransactionId,
PaymentStatus = x.PaymentStatus,
PaymentDate = x.PaymentDate,
UserId = x.UserId,
UserAddressId = x.UserAddressId,
PaymentMethod = x.PaymentMethod,
UserAddressText = x.UserAddress.Address,
FactorDetails = x.FactorDetails.Select(fd => new GetAllUserOrderByFilterResponseModelFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.Product.Title,
ProductThumbnailPath = fd.Product.ThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
}).ToList(),
DeliveryStatus = x.DeliveryStatus,
TrackingCode = x.TrackingCode,
DeliveryDescription = x.DeliveryDescription,
UserFullName = (x.User.FirstName ?? string.Empty) + " " + (x.User.LastName ?? string.Empty),
UserNationalCode = x.User.NationalCode,
VatAmount = x.OrderVAT != null ? x.OrderVAT.VATAmount : 0,
VatPercentage = x.OrderVAT != null ? (double)(x.OrderVAT.VATRate * 100) : 0
})
.ToListAsync(cancellationToken);
return new GetAllUserOrderByFilterResponseDto
{
MetaData = meta,
Models = models
};
}
}
@@ -1,14 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
public class GetAllUserOrderByFilterQueryValidator : AbstractValidator<GetAllUserOrderByFilterQuery>
{
public GetAllUserOrderByFilterQueryValidator()
{
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<GetAllUserOrderByFilterQuery>.CreateWithOptions((GetAllUserOrderByFilterQuery)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,64 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetAllUserOrderByFilter;
public class GetAllUserOrderByFilterResponseDto
{
//متادیتا
public MetaData MetaData { get; set; }
//مدل خروجی
public List<GetAllUserOrderByFilterResponseModel>? Models { get; set; }
}public class GetAllUserOrderByFilterResponseModel
{
//شناسه
public long Id { get; set; }
//قیمت
public long Amount { get; set; }
//شناسه پکیج
public long PackageId { get; set; }
//شناسه تراکنش
public long? TransactionId { get; set; }
//وضعیت پرداخت
public PaymentStatus PaymentStatus { get; set; }
//تاریخ پرداخت
public DateTime? PaymentDate { get; set; }
//شناسه کاربر
public long UserId { get; set; }
//شناسه آدرس کاربر
public long UserAddressId { get; set; }
//
public PaymentMethod? PaymentMethod { get; set; }
//
public string? UserAddressText { get; set; }
//
public List<GetAllUserOrderByFilterResponseModelFactorDetail>? FactorDetails { get; set; }
// وضعیت ارسال سفارش
public DeliveryStatus DeliveryStatus { get; set; }
// کد رهگیری مرسوله
public string? TrackingCode { get; set; }
// توضیحات ارسال
public string? DeliveryDescription { get; set; }
// نام کامل کاربر
public string? UserFullName { get; set; }
// کدملی کاربر
public string? UserNationalCode { get; set; }
// مبلغ مالیات بر ارزش افزوده (ریال)
public long VatAmount { get; set; }
// درصد مالیات بر ارزش افزوده (مثلاً 9 برای 9٪)
public double VatPercentage { get; set; }
}
public class GetAllUserOrderByFilterResponseModelFactorDetail
{
//شناسه
public long ProductId { get; set; }
//
public string ProductTitle { get; set; }
//
public string? ProductThumbnailPath { get; set; }
//
public long? UnitPrice { get; set; }
//
public int? Count { get; set; }
//
public long? UnitDiscountPrice { get; set; }
}
@@ -1,66 +0,0 @@
using CMSMicroservice.Application.Common.Models;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
/// <summary>
/// دریافت سفارشات بر اساس بازه زمانی
/// </summary>
public record GetOrdersByDateRangeQuery : IRequest<GetOrdersByDateRangeResponseDto>
{
/// <summary>
/// تاریخ شروع (UTC)
/// </summary>
public DateTime StartDate { get; init; }
/// <summary>
/// تاریخ پایان (UTC)
/// </summary>
public DateTime EndDate { get; init; }
/// <summary>
/// فیلتر وضعیت تحویل (اختیاری)
/// </summary>
public DeliveryStatus? Status { get; init; }
/// <summary>
/// شناسه کاربر (اختیاری - برای فیلتر بر اساس کاربر)
/// </summary>
public long? UserId { get; init; }
/// <summary>
/// شماره صفحه
/// </summary>
public int PageIndex { get; init; } = 1;
/// <summary>
/// تعداد در صفحه
/// </summary>
public int PageSize { get; init; } = 20;
}
/// <summary>
/// پاسخ لیست سفارشات
/// </summary>
public class GetOrdersByDateRangeResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<OrderSummaryDto> Orders { get; set; } = new();
}
/// <summary>
/// خلاصه اطلاعات سفارش
/// </summary>
public class OrderSummaryDto
{
public long Id { get; set; }
public long UserId { get; set; }
public string UserFullName { get; set; } = string.Empty;
public long Amount { get; set; }
public long DiscountedPrice { get; set; }
public DeliveryStatus Status { get; set; }
public DateTime Created { get; set; }
public DateTime? ShippedAt { get; set; }
public DateTime? DeliveredAt { get; set; }
public int ItemsCount { get; set; }
}
@@ -1,96 +0,0 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Models;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
public class GetOrdersByDateRangeQueryHandler : IRequestHandler<GetOrdersByDateRangeQuery, GetOrdersByDateRangeResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<GetOrdersByDateRangeQueryHandler> _logger;
public GetOrdersByDateRangeQueryHandler(
IApplicationDbContext context,
ILogger<GetOrdersByDateRangeQueryHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<GetOrdersByDateRangeResponseDto> Handle(GetOrdersByDateRangeQuery request, CancellationToken cancellationToken)
{
var query = _context.UserOrders
.AsNoTracking()
.Include(o => o.User)
.Include(o => o.FactorDetails)
.AsQueryable();
query = query.Where(o => o.Created >= request.StartDate && o.Created <= request.EndDate);
if (request.Status.HasValue)
{
query = query.Where(o => o.DeliveryStatus == request.Status.Value);
}
if (request.UserId.HasValue)
{
query = query.Where(o => o.UserId == request.UserId.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
var response = new GetOrdersByDateRangeResponseDto
{
MetaData = new MetaData
{
CurrentPage = request.PageIndex,
TotalPage = totalCount == 0 ? 0 : (int)Math.Ceiling(totalCount / (double)request.PageSize),
PageSize = request.PageSize,
TotalCount = totalCount,
HasNext = totalCount > 0 && request.PageIndex * request.PageSize < totalCount,
HasPrevious = request.PageIndex > 1
}
};
if (totalCount == 0)
{
return response;
}
var orders = await query
.OrderByDescending(o => o.Created)
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync(cancellationToken);
response.Orders = orders.Select(o =>
{
var firstName = o.User?.FirstName ?? string.Empty;
var lastName = o.User?.LastName ?? string.Empty;
var fullName = $"{firstName} {lastName}".Trim();
return new OrderSummaryDto
{
Id = o.Id,
UserId = o.UserId,
UserFullName = fullName,
Amount = o.Amount,
// در حال حاضر فیلد DiscountedPrice در UserOrder وجود ندارد، پس همان Amount برگردانده می‌شود
DiscountedPrice = o.Amount,
Status = o.DeliveryStatus,
Created = o.Created,
ShippedAt = null,
DeliveredAt = null,
ItemsCount = o.FactorDetails?.Count ?? 0
};
}).ToList();
_logger.LogInformation(
"Retrieved {Count} orders for date range {Start} to {End}",
response.Orders.Count,
request.StartDate,
request.EndDate);
return response;
}
}
@@ -1,28 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetOrdersByDateRange;
public class GetOrdersByDateRangeQueryValidator : AbstractValidator<GetOrdersByDateRangeQuery>
{
public GetOrdersByDateRangeQueryValidator()
{
RuleFor(x => x.StartDate)
.LessThanOrEqualTo(x => x.EndDate)
.WithMessage("تاریخ شروع باید کوچکتر یا مساوی تاریخ پایان باشد");
RuleFor(x => x.EndDate)
.LessThanOrEqualTo(DateTime.Now.AddDays(1))
.WithMessage("تاریخ پایان نمی‌تواند در آینده باشد");
RuleFor(x => x.PageIndex)
.GreaterThan(0)
.WithMessage("شماره صفحه باید بزرگتر از 0 باشد");
RuleFor(x => x.PageSize)
.InclusiveBetween(1, 100)
.WithMessage("تعداد در صفحه باید بین 1 تا 100 باشد");
// بازه زمانی نباید بیش از 1 سال باشد
RuleFor(x => x)
.Must(x => (x.EndDate - x.StartDate).TotalDays <= 365)
.WithMessage("بازه زمانی نمی‌تواند بیش از 1 سال باشد");
}
}
@@ -1,7 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
public record GetUserOrderQuery : IRequest<GetUserOrderResponseDto>
{
//شناسه
public long Id { get; init; }
}
@@ -1,61 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
public class GetUserOrderQueryHandler : IRequestHandler<GetUserOrderQuery, GetUserOrderResponseDto>
{
private readonly IApplicationDbContext _context;
public GetUserOrderQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetUserOrderResponseDto> Handle(GetUserOrderQuery request,
CancellationToken cancellationToken)
{
var response = await _context.UserOrders
.Include(i => i.UserAddress)
.Include(i => i.User)
.Include(i => i.FactorDetails)
.ThenInclude(t => t.Product)
.Include(i => i.OrderVAT)
.AsNoTracking()
.Where(x => x.Id == request.Id)
.Select(x => new GetUserOrderResponseDto
{
Id = x.Id,
Amount = x.Amount,
PackageId = x.PackageId ?? 0,
TransactionId = x.TransactionId,
PaymentStatus = x.PaymentStatus,
PaymentDate = x.PaymentDate,
UserId = x.UserId,
UserAddressId = x.UserAddressId,
PaymentMethod = x.PaymentMethod,
UserAddressText = x.UserAddress.Address,
FactorDetails = x.FactorDetails.Select(fd => new GetUserOrderResponseFactorDetail
{
ProductId = fd.ProductId,
ProductTitle = fd.Product.Title,
ProductThumbnailPath = fd.Product.ThumbnailPath,
UnitPrice = fd.UnitPrice,
Count = fd.Count,
UnitDiscountPrice = fd.UnitDiscountPrice
}).ToList(),
DeliveryStatus = x.DeliveryStatus,
TrackingCode = x.TrackingCode,
DeliveryDescription = x.DeliveryDescription,
UserFullName = (x.User.FirstName ?? string.Empty) + " " + (x.User.LastName ?? string.Empty),
UserNationalCode = x.User.NationalCode,
VatInfo = x.OrderVAT != null ? new OrderVATInfoDto
{
VatRate = x.OrderVAT.VATRate,
BaseAmount = x.OrderVAT.BaseAmount,
VatAmount = x.OrderVAT.VATAmount,
TotalAmount = x.OrderVAT.TotalAmount,
IsPaid = x.OrderVAT.IsPaid
} : null
})
.FirstOrDefaultAsync(cancellationToken);
return response ?? throw new NotFoundException(nameof(UserOrder), request.Id);
}
}
@@ -1,16 +0,0 @@
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
public class GetUserOrderQueryValidator : AbstractValidator<GetUserOrderQuery>
{
public GetUserOrderQueryValidator()
{
RuleFor(model => model.Id)
.NotNull();
}
public Func<object, string, Task<IEnumerable<string>>> ValidateValue => async (model, propertyName) =>
{
var result = await ValidateAsync(ValidationContext<GetUserOrderQuery>.CreateWithOptions((GetUserOrderQuery)model, x => x.IncludeProperties(propertyName)));
if (result.IsValid)
return Array.Empty<string>();
return result.Errors.Select(e => e.ErrorMessage);
};
}
@@ -1,83 +0,0 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserOrderCQ.Queries.GetUserOrder;
public class GetUserOrderResponseDto
{
//شناسه
public long Id { get; set; }
//قیمت
public long Amount { get; set; }
//شناسه پکیج
public long PackageId { get; set; }
//شناسه تراکنش
public long? TransactionId { get; set; }
//وضعیت پرداخت
public PaymentStatus PaymentStatus { get; set; }
//تاریخ پرداخت
public DateTime? PaymentDate { get; set; }
//شناسه کاربر
public long UserId { get; set; }
//شناسه آدرس کاربر
public long UserAddressId { get; set; }
//
public PaymentMethod? PaymentMethod { get; set; }
//
public string? UserAddressText { get; set; }
//
public List<GetUserOrderResponseFactorDetail>? FactorDetails { get; set; }
// وضعیت ارسال سفارش
public DeliveryStatus DeliveryStatus { get; set; }
// کدرهگیری مرسوله
public string? TrackingCode { get; set; }
// توضیحات ارسال
public string? DeliveryDescription { get; set; }
// نام کامل کاربر
public string? UserFullName { get; set; }
// کدملی کاربر
public string? UserNationalCode { get; set; }
// اطلاعات مالیات بر ارزش افزوده
public OrderVATInfoDto? VatInfo { get; set; }
}
/// <summary>
/// اطلاعات مالیات بر ارزش افزوده
/// </summary>
public class OrderVATInfoDto
{
/// <summary>
/// نرخ مالیات (مثلاً 0.09 = 9%)
/// </summary>
public decimal VatRate { get; set; }
/// <summary>
/// مبلغ پایه (قبل از مالیات)
/// </summary>
public long BaseAmount { get; set; }
/// <summary>
/// مبلغ مالیات
/// </summary>
public long VatAmount { get; set; }
/// <summary>
/// مبلغ کل (پایه + مالیات)
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// آیا پرداخت شده
/// </summary>
public bool IsPaid { get; set; }
}
public class GetUserOrderResponseFactorDetail
{
//شناسه
public long ProductId { get; set; }
//
public string ProductTitle { get; set; }
//
public string? ProductThumbnailPath { get; set; }
//
public long? UnitPrice { get; set; }
//
public int? Count { get; set; }
//
public long? UnitDiscountPrice { get; set; }
}