diff --git a/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQuery.cs b/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQuery.cs
new file mode 100644
index 0000000..a79e7d9
--- /dev/null
+++ b/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQuery.cs
@@ -0,0 +1,181 @@
+namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalReports;
+
+///
+/// گزارش برداشتها بر اساس بازه زمانی و دستهبندی
+///
+public record GetWithdrawalReportsQuery : IRequest
+{
+ ///
+ /// تاریخ شروع (اختیاری - پیشفرض: 30 روز قبل)
+ ///
+ public DateTime? StartDate { get; init; }
+
+ ///
+ /// تاریخ پایان (اختیاری - پیشفرض: امروز)
+ ///
+ public DateTime? EndDate { get; init; }
+
+ ///
+ /// نوع دوره گزارش: 1=روزانه, 2=هفتگی, 3=ماهانه
+ ///
+ public ReportPeriodType PeriodType { get; init; } = ReportPeriodType.Daily;
+
+ ///
+ /// فیلتر وضعیت (اختیاری)
+ ///
+ public int? Status { get; init; }
+
+ ///
+ /// فیلتر شناسه کاربر (اختیاری)
+ ///
+ public long? UserId { get; init; }
+}
+
+///
+/// نوع دستهبندی گزارش
+///
+public enum ReportPeriodType
+{
+ ///
+ /// روزانه
+ ///
+ Daily = 1,
+
+ ///
+ /// هفتگی
+ ///
+ Weekly = 2,
+
+ ///
+ /// ماهانه
+ ///
+ Monthly = 3
+}
+
+///
+/// پاسخ گزارش برداشتها
+///
+public class GetWithdrawalReportsResponseDto
+{
+ ///
+ /// گزارشهای هر دوره
+ ///
+ public List PeriodReports { get; set; } = new();
+
+ ///
+ /// خلاصه کل گزارش
+ ///
+ public WithdrawalSummaryDto Summary { get; set; } = new();
+}
+
+///
+/// گزارش یک دوره زمانی
+///
+public class PeriodReportDto
+{
+ ///
+ /// برچسب دوره (مثلاً "2025-01-15" یا "هفته 3" یا "فروردین 1404")
+ ///
+ public string PeriodLabel { get; set; } = string.Empty;
+
+ ///
+ /// تاریخ شروع دوره
+ ///
+ public DateTime StartDate { get; set; }
+
+ ///
+ /// تاریخ پایان دوره
+ ///
+ public DateTime EndDate { get; set; }
+
+ ///
+ /// تعداد کل درخواستها
+ ///
+ public int TotalRequests { get; set; }
+
+ ///
+ /// تعداد در انتظار
+ ///
+ public int PendingCount { get; set; }
+
+ ///
+ /// تعداد تایید شده
+ ///
+ public int ApprovedCount { get; set; }
+
+ ///
+ /// تعداد رد شده
+ ///
+ public int RejectedCount { get; set; }
+
+ ///
+ /// تعداد تکمیل شده
+ ///
+ public int CompletedCount { get; set; }
+
+ ///
+ /// تعداد ناموفق
+ ///
+ public int FailedCount { get; set; }
+
+ ///
+ /// مبلغ کل (ریال)
+ ///
+ public long TotalAmount { get; set; }
+
+ ///
+ /// مبلغ پرداخت شده (ریال)
+ ///
+ public long PaidAmount { get; set; }
+
+ ///
+ /// مبلغ در انتظار (ریال)
+ ///
+ public long PendingAmount { get; set; }
+}
+
+///
+/// خلاصه کل گزارش برداشتها
+///
+public class WithdrawalSummaryDto
+{
+ ///
+ /// تعداد کل درخواستها
+ ///
+ public int TotalRequests { get; set; }
+
+ ///
+ /// مبلغ کل درخواستها (ریال)
+ ///
+ public long TotalAmount { get; set; }
+
+ ///
+ /// مبلغ کل پرداخت شده (ریال)
+ ///
+ public long TotalPaid { get; set; }
+
+ ///
+ /// مبلغ کل در انتظار (ریال)
+ ///
+ public long TotalPending { get; set; }
+
+ ///
+ /// مبلغ کل رد شده (ریال)
+ ///
+ public long TotalRejected { get; set; }
+
+ ///
+ /// میانگین مبلغ درخواست (ریال)
+ ///
+ public long AverageAmount { get; set; }
+
+ ///
+ /// تعداد کاربران منحصر به فرد
+ ///
+ public int UniqueUsers { get; set; }
+
+ ///
+ /// درصد موفقیت (0-100)
+ ///
+ public decimal SuccessRate { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQueryHandler.cs b/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQueryHandler.cs
new file mode 100644
index 0000000..8f202f3
--- /dev/null
+++ b/src/BackOffice.BFF.Application/CommissionCQ/Queries/GetWithdrawalReports/GetWithdrawalReportsQueryHandler.cs
@@ -0,0 +1,78 @@
+using BackOffice.BFF.Commission.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalReports;
+
+public class GetWithdrawalReportsQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetWithdrawalReportsQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetWithdrawalReportsQuery request, CancellationToken cancellationToken)
+ {
+ var grpcRequest = new GetWithdrawalReportsRequest
+ {
+ PeriodType = (int)request.PeriodType
+ };
+
+ // تنظیم بازه زمانی
+ if (request.StartDate.HasValue)
+ {
+ grpcRequest.StartDate = Timestamp.FromDateTime(request.StartDate.Value.ToUniversalTime());
+ }
+
+ if (request.EndDate.HasValue)
+ {
+ grpcRequest.EndDate = Timestamp.FromDateTime(request.EndDate.Value.ToUniversalTime());
+ }
+
+ // فیلتر وضعیت
+ if (request.Status.HasValue)
+ {
+ grpcRequest.Status = request.Status.Value;
+ }
+
+ // فیلتر کاربر
+ if (request.UserId.HasValue)
+ {
+ grpcRequest.UserId = request.UserId.Value;
+ }
+
+ var response = await _context.Commissions.GetWithdrawalReportsAsync(grpcRequest, cancellationToken: cancellationToken);
+
+ // تبدیل به DTO
+ return new GetWithdrawalReportsResponseDto
+ {
+ PeriodReports = response.PeriodReports.Select(p => new PeriodReportDto
+ {
+ PeriodLabel = p.PeriodLabel,
+ StartDate = p.StartDate.ToDateTime(),
+ EndDate = p.EndDate.ToDateTime(),
+ TotalRequests = p.TotalRequests,
+ PendingCount = p.PendingCount,
+ ApprovedCount = p.ApprovedCount,
+ RejectedCount = p.RejectedCount,
+ CompletedCount = p.CompletedCount,
+ FailedCount = p.FailedCount,
+ TotalAmount = p.TotalAmount,
+ PaidAmount = p.PaidAmount,
+ PendingAmount = p.PendingAmount
+ }).ToList(),
+ Summary = new WithdrawalSummaryDto
+ {
+ TotalRequests = response.Summary.TotalRequests,
+ TotalAmount = response.Summary.TotalAmount,
+ TotalPaid = response.Summary.TotalPaid,
+ TotalPending = response.Summary.TotalPending,
+ TotalRejected = response.Summary.TotalRejected,
+ AverageAmount = response.Summary.AverageAmount,
+ UniqueUsers = response.Summary.UniqueUsers,
+ SuccessRate = (decimal)response.Summary.SuccessRate
+ }
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs
index 15dad85..326d191 100644
--- a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs
+++ b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs
@@ -6,14 +6,20 @@ using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserRole;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductImages;
-using CMSMicroservice.Protobuf.Protos.ProductGallerys;
+using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.Category;
-using CMSMicroservice.Protobuf.Protos.PruductCategory;
+using CMSMicroservice.Protobuf.Protos.ProductCategory;
using BackOffice.BFF.Commission.Protobuf;
using BackOffice.BFF.NetworkMembership.Protobuf;
using BackOffice.BFF.ClubMembership.Protobuf;
using BackOffice.BFF.Configuration.Protobuf;
using FMSMicroservice.Protobuf.Protos.FileInfo;
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+using CMSMicroservice.Protobuf.Protos.Tag;
+using CMSMicroservice.Protobuf.Protos.ProductTag;
namespace BackOffice.BFF.Application.Common.Interfaces;
@@ -26,10 +32,10 @@ public interface IApplicationContractContext
PackageContract.PackageContractClient Packages { get; }
ProductsContract.ProductsContractClient Products { get; }
ProductImagesContract.ProductImagesContractClient ProductImages { get; }
- ProductGallerysContract.ProductGallerysContractClient ProductGallerys { get; }
+ ProductGalleriesContract.ProductGalleriesContractClient ProductGalleries { get; }
RoleContract.RoleContractClient Roles { get; }
CategoryContract.CategoryContractClient Categories { get; }
- PruductCategoryContract.PruductCategoryContractClient ProductCategories { get; }
+ ProductCategoryContract.ProductCategoryContractClient ProductCategories { get; }
UserAddressContract.UserAddressContractClient UserAddress { get; }
UserContract.UserContractClient Users { get; }
UserOrderContract.UserOrderContractClient UserOrders { get; }
@@ -40,6 +46,16 @@ public interface IApplicationContractContext
NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships { get; }
ClubMembershipContract.ClubMembershipContractClient ClubMemberships { get; }
ConfigurationContract.ConfigurationContractClient Configurations { get; }
+
+ // Discount Shop System (Phase 9)
+ DiscountProductContract.DiscountProductContractClient DiscountProducts { get; }
+ DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories { get; }
+ DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountShoppingCarts { get; }
+ DiscountOrderContract.DiscountOrderContractClient DiscountOrders { get; }
+
+ // Tag Management System
+ TagContract.TagContractClient Tags { get; }
+ ProductTagContract.ProductTagContractClient ProductTags { get; }
#endregion
}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommand.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommand.cs
new file mode 100644
index 0000000..52ef2ca
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommand.cs
@@ -0,0 +1,37 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
+
+public record CreateDiscountCategoryCommand : IRequest
+{
+ /// نام دستهبندی (انگلیسی)
+ public string Name { get; init; }
+
+ /// عنوان دستهبندی (فارسی)
+ public string Title { get; init; }
+
+ /// توضیحات دستهبندی
+ public string Description { get; init; }
+
+ /// شناسه دستهبندی والد (null برای دسته اصلی)
+ public long? ParentCategoryId { get; init; }
+
+ /// ترتیب نمایش
+ public int SortOrder { get; init; }
+
+ /// وضعیت فعال/غیرفعال
+ public bool IsActive { get; init; }
+
+ /// فایل تصویر دستهبندی
+ public DiscountCategoryFileModel ImageFile { get; init; }
+}
+
+public class DiscountCategoryFileModel
+{
+ /// فایل
+ public byte[] File { get; set; }
+
+ /// نام فایل
+ public string FileName { get; set; }
+
+ /// نوع فایل
+ public string Mime { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandHandler.cs
new file mode 100644
index 0000000..d2d28cf
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandHandler.cs
@@ -0,0 +1,53 @@
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
+
+public class CreateDiscountCategoryCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public CreateDiscountCategoryCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(CreateDiscountCategoryCommand request, CancellationToken cancellationToken)
+ {
+ var createRequest = new CreateDiscountCategoryRequest
+ {
+ Name = request.Name,
+ Title = request.Title,
+ Description = request.Description ?? string.Empty,
+ SortOrder = request.SortOrder,
+ IsActive = request.IsActive
+ };
+
+ if (request.ParentCategoryId.HasValue)
+ createRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
+
+ // Upload Image
+ if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
+ {
+ var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Categories",
+ IsBase64 = false,
+ MIME = request.ImageFile.Mime,
+ FileName = request.ImageFile.FileName,
+ File = ByteString.CopyFrom(request.ImageFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
+ createRequest.ImagePath = imageFileInfo.File;
+ }
+
+ var response = await _context.DiscountCategories.CreateDiscountCategoryAsync(createRequest, cancellationToken: cancellationToken);
+
+ return new CreateDiscountCategoryResponseDto
+ {
+ CategoryId = response.CategoryId
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandValidator.cs
new file mode 100644
index 0000000..544c071
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryCommandValidator.cs
@@ -0,0 +1,19 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
+
+public class CreateDiscountCategoryCommandValidator : AbstractValidator
+{
+ public CreateDiscountCategoryCommandValidator()
+ {
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("نام دستهبندی الزامی است")
+ .MaximumLength(100).WithMessage("نام دستهبندی نباید بیشتر از 100 کاراکتر باشد")
+ .Matches("^[a-zA-Z0-9_-]+$").WithMessage("نام دستهبندی فقط باید شامل حروف انگلیسی، اعداد، خط تیره و زیرخط باشد");
+
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
+ .MaximumLength(200).WithMessage("عنوان دستهبندی نباید بیشتر از 200 کاراکتر باشد");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryResponseDto.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryResponseDto.cs
new file mode 100644
index 0000000..75e85a3
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/CreateDiscountCategory/CreateDiscountCategoryResponseDto.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
+
+public class CreateDiscountCategoryResponseDto
+{
+ /// شناسه دستهبندی ایجاد شده
+ public long CategoryId { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommand.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommand.cs
new file mode 100644
index 0000000..02c588c
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommand.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
+
+public record DeleteDiscountCategoryCommand : IRequest
+{
+ /// شناسه دستهبندی
+ public long CategoryId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandHandler.cs
new file mode 100644
index 0000000..ab7eefd
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandHandler.cs
@@ -0,0 +1,22 @@
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
+
+public class DeleteDiscountCategoryCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public DeleteDiscountCategoryCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(DeleteDiscountCategoryCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountCategories.DeleteDiscountCategoryAsync(
+ new DeleteDiscountCategoryRequest { CategoryId = request.CategoryId },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandValidator.cs
new file mode 100644
index 0000000..a800530
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/DeleteDiscountCategory/DeleteDiscountCategoryCommandValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
+
+public class DeleteDiscountCategoryCommandValidator : AbstractValidator
+{
+ public DeleteDiscountCategoryCommandValidator()
+ {
+ RuleFor(x => x.CategoryId)
+ .GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommand.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommand.cs
new file mode 100644
index 0000000..7e47b09
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommand.cs
@@ -0,0 +1,40 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
+
+public record UpdateDiscountCategoryCommand : IRequest
+{
+ /// شناسه دستهبندی
+ public long CategoryId { get; init; }
+
+ /// نام دستهبندی (انگلیسی)
+ public string Name { get; init; }
+
+ /// عنوان دستهبندی (فارسی)
+ public string Title { get; init; }
+
+ /// توضیحات دستهبندی
+ public string Description { get; init; }
+
+ /// شناسه دستهبندی والد (null برای دسته اصلی)
+ public long? ParentCategoryId { get; init; }
+
+ /// ترتیب نمایش
+ public int SortOrder { get; init; }
+
+ /// وضعیت فعال/غیرفعال
+ public bool IsActive { get; init; }
+
+ /// فایل تصویر دستهبندی (اختیاری - برای تغییر تصویر)
+ public DiscountCategoryFileModel ImageFile { get; init; }
+}
+
+public class DiscountCategoryFileModel
+{
+ /// فایل
+ public byte[] File { get; set; }
+
+ /// نام فایل
+ public string FileName { get; set; }
+
+ /// نوع فایل
+ public string Mime { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandHandler.cs
new file mode 100644
index 0000000..42b2fd2
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandHandler.cs
@@ -0,0 +1,51 @@
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+using Google.Protobuf;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
+
+public class UpdateDiscountCategoryCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public UpdateDiscountCategoryCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(UpdateDiscountCategoryCommand request, CancellationToken cancellationToken)
+ {
+ var updateRequest = new UpdateDiscountCategoryRequest
+ {
+ CategoryId = request.CategoryId,
+ Name = request.Name,
+ Title = request.Title,
+ Description = request.Description ?? string.Empty,
+ SortOrder = request.SortOrder,
+ IsActive = request.IsActive
+ };
+
+ if (request.ParentCategoryId.HasValue)
+ updateRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
+
+ // Upload new Image if provided
+ if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
+ {
+ var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Categories",
+ IsBase64 = false,
+ MIME = request.ImageFile.Mime,
+ FileName = request.ImageFile.FileName,
+ File = ByteString.CopyFrom(request.ImageFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
+ updateRequest.ImagePath = imageFileInfo.File;
+ }
+
+ await _context.DiscountCategories.UpdateDiscountCategoryAsync(updateRequest, cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandValidator.cs
new file mode 100644
index 0000000..4b57a41
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Commands/UpdateDiscountCategory/UpdateDiscountCategoryCommandValidator.cs
@@ -0,0 +1,26 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
+
+public class UpdateDiscountCategoryCommandValidator : AbstractValidator
+{
+ public UpdateDiscountCategoryCommandValidator()
+ {
+ RuleFor(x => x.CategoryId)
+ .GreaterThan(0).WithMessage("شناسه دستهبندی نامعتبر است");
+
+ RuleFor(x => x.Name)
+ .NotEmpty().WithMessage("نام دستهبندی الزامی است")
+ .MaximumLength(100).WithMessage("نام دستهبندی نباید بیشتر از 100 کاراکتر باشد")
+ .Matches("^[a-zA-Z0-9_-]+$").WithMessage("نام دستهبندی فقط باید شامل حروف انگلیسی، اعداد، خط تیره و زیرخط باشد");
+
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("عنوان دستهبندی الزامی است")
+ .MaximumLength(200).WithMessage("عنوان دستهبندی نباید بیشتر از 200 کاراکتر باشد");
+
+ RuleFor(x => x.Description)
+ .MaximumLength(1000).WithMessage("توضیحات نباید بیشتر از 1000 کاراکتر باشد");
+
+ RuleFor(x => x.ParentCategoryId)
+ .NotEqual(x => x.CategoryId).When(x => x.ParentCategoryId.HasValue)
+ .WithMessage("دستهبندی نمیتواند والد خودش باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQuery.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQuery.cs
new file mode 100644
index 0000000..d68b21b
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQuery.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
+
+public record GetDiscountCategoriesQuery : IRequest
+{
+ /// فیلتر براساس دسته والد (null برای ریشه)
+ public long? ParentCategoryId { get; init; }
+
+ /// فقط دستههای فعال
+ public bool? IsActive { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs
new file mode 100644
index 0000000..c805683
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesQueryHandler.cs
@@ -0,0 +1,50 @@
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
+
+public class GetDiscountCategoriesQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetDiscountCategoriesQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetDiscountCategoriesQuery request, CancellationToken cancellationToken)
+ {
+ var grpcRequest = new GetDiscountCategoriesRequest();
+
+ if (request.ParentCategoryId.HasValue)
+ grpcRequest.ParentCategoryId = new Int64Value { Value = request.ParentCategoryId.Value };
+
+ if (request.IsActive.HasValue)
+ grpcRequest.IsActive = new BoolValue { Value = request.IsActive.Value };
+
+ var response = await _context.DiscountCategories.GetDiscountCategoriesAsync(grpcRequest, cancellationToken: cancellationToken);
+
+ return new GetDiscountCategoriesResponseDto
+ {
+ Categories = response.Categories.Select(MapCategory).ToList()
+ };
+ }
+
+ private DiscountCategoryTreeDto MapCategory(DiscountCategoryDto node)
+ {
+ return new DiscountCategoryTreeDto
+ {
+ Id = node.Id,
+ Name = node.Name,
+ Title = node.Title,
+ Description = node.HasDescription ? node.Description.Value : string.Empty,
+ ImagePath = node.HasImagePath ? node.ImagePath.Value : string.Empty,
+ ParentCategoryId = node.HasParentCategoryId ? node.ParentCategoryId.Value : null,
+ SortOrder = node.SortOrder,
+ IsActive = node.IsActive,
+ ProductCount = node.ProductCount,
+ Created = DateTime.UtcNow, // Proto doesn't have created field
+ ChildCategories = node.Children.Select(MapCategory).ToList()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesResponseDto.cs b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesResponseDto.cs
new file mode 100644
index 0000000..36f8c3e
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountCategoryCQ/Queries/GetDiscountCategories/GetDiscountCategoriesResponseDto.cs
@@ -0,0 +1,24 @@
+namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
+
+public class GetDiscountCategoriesResponseDto
+{
+ /// لیست درختی دستهبندیها
+ public List Categories { get; set; } = new();
+}
+
+public class DiscountCategoryTreeDto
+{
+ public long Id { get; set; }
+ public string Name { get; set; }
+ public string Title { get; set; }
+ public string Description { get; set; }
+ public string ImagePath { get; set; }
+ public long? ParentCategoryId { get; set; }
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; }
+ public int ProductCount { get; set; }
+ public DateTime Created { get; set; }
+
+ /// زیردستههای این دستهبندی
+ public List ChildCategories { get; set; } = new();
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommand.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommand.cs
new file mode 100644
index 0000000..0477a8b
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommand.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
+
+public record CompleteOrderPaymentCommand : IRequest
+{
+ /// شناسه سفارش
+ public long OrderId { get; init; }
+
+ /// کد تراکنش پرداخت
+ public string PaymentTransactionCode { get; init; }
+
+ /// مبلغ پرداخت شده
+ public long PaidAmount { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs
new file mode 100644
index 0000000..0f9f76a
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandHandler.cs
@@ -0,0 +1,27 @@
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
+
+public class CompleteOrderPaymentCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public CompleteOrderPaymentCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(CompleteOrderPaymentCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountOrders.CompleteOrderPaymentAsync(
+ new CompleteOrderPaymentRequest
+ {
+ OrderId = request.OrderId,
+ PaymentTransactionCode = request.PaymentTransactionCode,
+ PaidAmount = request.PaidAmount
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandValidator.cs
new file mode 100644
index 0000000..cc89e54
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/CompleteOrderPayment/CompleteOrderPaymentCommandValidator.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
+
+public class CompleteOrderPaymentCommandValidator : AbstractValidator
+{
+ public CompleteOrderPaymentCommandValidator()
+ {
+ RuleFor(x => x.OrderId)
+ .GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
+
+ RuleFor(x => x.PaymentTransactionCode)
+ .NotEmpty().WithMessage("کد تراکنش پرداخت الزامی است");
+
+ RuleFor(x => x.PaidAmount)
+ .GreaterThan(0).WithMessage("مبلغ پرداخت شده باید بیشتر از صفر باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommand.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommand.cs
new file mode 100644
index 0000000..2812106
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommand.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
+
+public record PlaceOrderCommand : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+
+ /// شناسه آدرس ارسال
+ public long AddressId { get; init; }
+
+ /// مبلغ پرداخت از موجودی تخفیف
+ public long DiscountBalanceAmount { get; init; }
+
+ /// مبلغ پرداخت از درگاه
+ public long GatewayAmount { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
new file mode 100644
index 0000000..06a20c7
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandHandler.cs
@@ -0,0 +1,34 @@
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
+
+public class PlaceOrderCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public PlaceOrderCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(PlaceOrderCommand request, CancellationToken cancellationToken)
+ {
+ var response = await _context.DiscountOrders.PlaceOrderAsync(
+ new PlaceOrderRequest
+ {
+ UserId = request.UserId,
+ AddressId = request.AddressId,
+ DiscountBalanceAmount = request.DiscountBalanceAmount,
+ GatewayAmount = request.GatewayAmount
+ },
+ cancellationToken: cancellationToken);
+
+ return new PlaceOrderResponseDto
+ {
+ OrderId = response.OrderId,
+ TrackingCode = response.TrackingCode,
+ RequiresGatewayPayment = response.RequiresGatewayPayment,
+ GatewayPayableAmount = response.GatewayPayableAmount
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandValidator.cs
new file mode 100644
index 0000000..087e2f8
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderCommandValidator.cs
@@ -0,0 +1,23 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
+
+public class PlaceOrderCommandValidator : AbstractValidator
+{
+ public PlaceOrderCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+
+ RuleFor(x => x.AddressId)
+ .GreaterThan(0).WithMessage("شناسه آدرس نامعتبر است");
+
+ RuleFor(x => x.DiscountBalanceAmount)
+ .GreaterThanOrEqualTo(0).WithMessage("مبلغ موجودی تخفیف نمیتواند منفی باشد");
+
+ RuleFor(x => x.GatewayAmount)
+ .GreaterThanOrEqualTo(0).WithMessage("مبلغ درگاه نمیتواند منفی باشد");
+
+ RuleFor(x => x)
+ .Must(x => x.DiscountBalanceAmount + x.GatewayAmount > 0)
+ .WithMessage("مجموع مبالغ پرداخت باید بیشتر از صفر باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderResponseDto.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderResponseDto.cs
new file mode 100644
index 0000000..4d8e1d5
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/PlaceOrder/PlaceOrderResponseDto.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
+
+public class PlaceOrderResponseDto
+{
+ /// شناسه سفارش
+ public long OrderId { get; set; }
+
+ /// کد پیگیری سفارش
+ public string TrackingCode { get; set; }
+
+ /// آیا نیاز به پرداخت از درگاه دارد
+ public bool RequiresGatewayPayment { get; set; }
+
+ /// مبلغ قابل پرداخت از درگاه
+ public long GatewayPayableAmount { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommand.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommand.cs
new file mode 100644
index 0000000..6394d8f
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommand.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
+
+public record UpdateOrderStatusCommand : IRequest
+{
+ /// شناسه سفارش
+ public long OrderId { get; init; }
+
+ /// وضعیت جدید
+ public int NewStatus { get; init; }
+
+ /// یادداشت مدیر (اختیاری)
+ public string AdminNote { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs
new file mode 100644
index 0000000..d950a18
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandHandler.cs
@@ -0,0 +1,27 @@
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
+
+public class UpdateOrderStatusCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public UpdateOrderStatusCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(UpdateOrderStatusCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountOrders.UpdateOrderStatusAsync(
+ new UpdateOrderStatusRequest
+ {
+ OrderId = request.OrderId,
+ NewStatus = request.NewStatus,
+ AdminNote = request.AdminNote ?? string.Empty
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandValidator.cs
new file mode 100644
index 0000000..3462fbb
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Commands/UpdateOrderStatus/UpdateOrderStatusCommandValidator.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
+
+public class UpdateOrderStatusCommandValidator : AbstractValidator
+{
+ public UpdateOrderStatusCommandValidator()
+ {
+ RuleFor(x => x.OrderId)
+ .GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
+
+ RuleFor(x => x.NewStatus)
+ .InclusiveBetween(0, 6).WithMessage("وضعیت سفارش نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQuery.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQuery.cs
new file mode 100644
index 0000000..220eca9
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQuery.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
+
+public record GetOrderByIdQuery : IRequest
+{
+ /// شناسه سفارش
+ public long OrderId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryHandler.cs
new file mode 100644
index 0000000..4bc89e7
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryHandler.cs
@@ -0,0 +1,58 @@
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
+
+public class GetOrderByIdQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetOrderByIdQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
+ {
+ var response = await _context.DiscountOrders.GetOrderByIdAsync(
+ new GetOrderByIdRequest { OrderId = request.OrderId },
+ cancellationToken: cancellationToken);
+
+ return new GetOrderByIdResponseDto
+ {
+ Id = response.Id,
+ UserId = response.UserId,
+ TrackingCode = response.TrackingCode,
+ Status = response.Status,
+ StatusTitle = response.StatusTitle,
+ TotalPrice = response.TotalPrice,
+ DiscountBalanceUsed = response.DiscountBalanceUsed,
+ GatewayPayment = response.GatewayPayment,
+ FinalPrice = response.FinalPrice,
+ PaymentTransactionCode = response.PaymentTransactionCode,
+ AdminNote = response.AdminNote,
+ CreatedAt = response.CreatedAt.ToDateTime(),
+ PaidAt = response.HasPaidAt ? response.PaidAt.ToDateTime() : null,
+ ShippingAddress = new AddressInfoDto
+ {
+ Id = response.ShippingAddress.Id,
+ RecipientName = response.ShippingAddress.RecipientName,
+ RecipientPhone = response.ShippingAddress.RecipientPhone,
+ Province = response.ShippingAddress.Province,
+ City = response.ShippingAddress.City,
+ PostalCode = response.ShippingAddress.PostalCode,
+ FullAddress = response.ShippingAddress.FullAddress
+ },
+ Items = response.Items.Select(item => new OrderItemDto
+ {
+ Id = item.Id,
+ ProductId = item.ProductId,
+ ProductTitle = item.ProductTitle,
+ UnitPrice = item.UnitPrice,
+ DiscountPercent = item.DiscountPercent,
+ Quantity = item.Quantity,
+ TotalPrice = item.TotalPrice,
+ DiscountedPrice = item.DiscountedPrice
+ }).ToList()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryValidator.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryValidator.cs
new file mode 100644
index 0000000..bec3285
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdQueryValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
+
+public class GetOrderByIdQueryValidator : AbstractValidator
+{
+ public GetOrderByIdQueryValidator()
+ {
+ RuleFor(x => x.OrderId)
+ .GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdResponseDto.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdResponseDto.cs
new file mode 100644
index 0000000..ec00168
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetOrderById/GetOrderByIdResponseDto.cs
@@ -0,0 +1,44 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
+
+public class GetOrderByIdResponseDto
+{
+ public long Id { get; set; }
+ public long UserId { get; set; }
+ public string TrackingCode { get; set; }
+ public int Status { get; set; }
+ public string StatusTitle { get; set; }
+ public long TotalPrice { get; set; }
+ public long DiscountBalanceUsed { get; set; }
+ public long GatewayPayment { get; set; }
+ public long FinalPrice { get; set; }
+ public string PaymentTransactionCode { get; set; }
+ public string AdminNote { get; set; }
+ public DateTime CreatedAt { get; set; }
+ public DateTime? PaidAt { get; set; }
+
+ public AddressInfoDto ShippingAddress { get; set; }
+ public List Items { get; set; } = new();
+}
+
+public class AddressInfoDto
+{
+ public long Id { get; set; }
+ public string RecipientName { get; set; }
+ public string RecipientPhone { get; set; }
+ public string Province { get; set; }
+ public string City { get; set; }
+ public string PostalCode { get; set; }
+ public string FullAddress { get; set; }
+}
+
+public class OrderItemDto
+{
+ public long Id { get; set; }
+ public long ProductId { get; set; }
+ public string ProductTitle { get; set; }
+ public long UnitPrice { get; set; }
+ public int DiscountPercent { get; set; }
+ public int Quantity { get; set; }
+ public long TotalPrice { get; set; }
+ public long DiscountedPrice { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQuery.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQuery.cs
new file mode 100644
index 0000000..d8cb2fd
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQuery.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
+
+public record GetUserOrdersQuery : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+
+ /// فیلتر براساس وضعیت سفارش
+ public int? Status { get; init; }
+
+ /// شماره صفحه
+ public int PageNumber { get; init; } = 1;
+
+ /// تعداد در هر صفحه
+ public int PageSize { get; init; } = 20;
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs
new file mode 100644
index 0000000..b05689c
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryHandler.cs
@@ -0,0 +1,54 @@
+using BackOffice.BFF.Application.Common.Models;
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
+
+public class GetUserOrdersQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetUserOrdersQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetUserOrdersQuery request, CancellationToken cancellationToken)
+ {
+ var grpcRequest = new GetUserOrdersRequest
+ {
+ UserId = request.UserId,
+ PageNumber = request.PageNumber,
+ PageSize = request.PageSize
+ };
+
+ if (request.Status.HasValue)
+ grpcRequest.Status = new Int32Value { Value = request.Status.Value };
+
+ var response = await _context.DiscountOrders.GetUserOrdersAsync(grpcRequest, cancellationToken: cancellationToken);
+
+ return new GetUserOrdersResponseDto
+ {
+ MetaData = new MetaData
+ {
+ CurrentPage = response.MetaData.CurrentPage,
+ TotalPage = response.MetaData.TotalPages,
+ PageSize = response.MetaData.PageSize,
+ TotalCount = response.MetaData.TotalCount,
+ HasPrevious = response.MetaData.HasPrevious,
+ HasNext = response.MetaData.HasNext
+ },
+ Orders = response.Orders.Select(order => new UserOrderDto
+ {
+ Id = order.Id,
+ TrackingCode = order.TrackingCode,
+ Status = order.Status,
+ StatusTitle = order.StatusTitle,
+ TotalPrice = order.TotalPrice,
+ FinalPrice = order.FinalPrice,
+ ItemCount = order.ItemCount,
+ CreatedAt = order.CreatedAt.ToDateTime()
+ }).ToList()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryValidator.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryValidator.cs
new file mode 100644
index 0000000..6235271
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersQueryValidator.cs
@@ -0,0 +1,21 @@
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
+
+public class GetUserOrdersQueryValidator : AbstractValidator
+{
+ public GetUserOrdersQueryValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+
+ RuleFor(x => x.PageNumber)
+ .GreaterThan(0).WithMessage("شماره صفحه باید بیشتر از صفر باشد");
+
+ RuleFor(x => x.PageSize)
+ .GreaterThan(0).WithMessage("تعداد در صفحه باید بیشتر از صفر باشد")
+ .LessThanOrEqualTo(100).WithMessage("حداکثر تعداد در صفحه 100 است");
+
+ RuleFor(x => x.Status)
+ .InclusiveBetween(0, 6).When(x => x.Status.HasValue)
+ .WithMessage("وضعیت سفارش نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersResponseDto.cs b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersResponseDto.cs
new file mode 100644
index 0000000..f7af583
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountOrderCQ/Queries/GetUserOrders/GetUserOrdersResponseDto.cs
@@ -0,0 +1,21 @@
+using BackOffice.BFF.Application.Common.Models;
+
+namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
+
+public class GetUserOrdersResponseDto
+{
+ public MetaData MetaData { get; set; }
+ public List Orders { get; set; } = new();
+}
+
+public class UserOrderDto
+{
+ public long Id { get; set; }
+ public string TrackingCode { get; set; }
+ public int Status { get; set; }
+ public string StatusTitle { get; set; }
+ public long TotalPrice { get; set; }
+ public long FinalPrice { get; set; }
+ public int ItemCount { get; set; }
+ public DateTime CreatedAt { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommand.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommand.cs
new file mode 100644
index 0000000..1c30b06
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommand.cs
@@ -0,0 +1,49 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
+
+public record CreateDiscountProductCommand : IRequest
+{
+ /// عنوان محصول
+ public string Title { get; init; }
+
+ /// اطلاعات کوتاه محصول
+ public string ShortInformation { get; init; }
+
+ /// اطلاعات کامل محصول
+ public string FullInformation { get; init; }
+
+ /// قیمت محصول
+ public long Price { get; init; }
+
+ /// حداکثر درصد تخفیف (0-100)
+ public int MaxDiscountPercent { get; init; }
+
+ /// تعداد اولیه موجودی
+ public int InitialCount { get; init; }
+
+ /// ترتیب نمایش
+ public int SortOrder { get; init; }
+
+ /// وضعیت فعال/غیرفعال
+ public bool IsActive { get; init; }
+
+ /// آیدی دستهبندیها
+ public List CategoryIds { get; init; } = new();
+
+ /// فایل تصویر اصلی
+ public DiscountProductFileModel ImageFile { get; init; }
+
+ /// فایل تصویر کوچک
+ public DiscountProductFileModel ThumbnailFile { get; init; }
+}
+
+public class DiscountProductFileModel
+{
+ /// فایل
+ public byte[] File { get; set; }
+
+ /// نام فایل
+ public string FileName { get; set; }
+
+ /// نوع فایل
+ public string Mime { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs
new file mode 100644
index 0000000..d505af7
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandHandler.cs
@@ -0,0 +1,70 @@
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+using Google.Protobuf;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
+
+public class CreateDiscountProductCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public CreateDiscountProductCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(CreateDiscountProductCommand request, CancellationToken cancellationToken)
+ {
+ var createRequest = new CreateDiscountProductRequest
+ {
+ Title = request.Title,
+ ShortInfomation = request.ShortInformation,
+ FullInformation = request.FullInformation,
+ Price = request.Price,
+ MaxDiscountPercent = request.MaxDiscountPercent,
+ InitialCount = request.InitialCount,
+ SortOrder = request.SortOrder,
+ IsActive = request.IsActive
+ };
+
+ createRequest.CategoryIds.AddRange(request.CategoryIds);
+
+ // Upload Image
+ if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
+ {
+ var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Products",
+ IsBase64 = false,
+ MIME = request.ImageFile.Mime,
+ FileName = request.ImageFile.FileName,
+ File = ByteString.CopyFrom(request.ImageFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
+ createRequest.ImagePath = imageFileInfo.File;
+ }
+
+ // Upload Thumbnail
+ if (request.ThumbnailFile != null && request.ThumbnailFile.File != null && request.ThumbnailFile.File.Length > 0)
+ {
+ var thumbnailFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Products/Thumbnails",
+ IsBase64 = false,
+ MIME = request.ThumbnailFile.Mime,
+ FileName = request.ThumbnailFile.FileName,
+ File = ByteString.CopyFrom(request.ThumbnailFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (thumbnailFileInfo != null && !string.IsNullOrWhiteSpace(thumbnailFileInfo.File))
+ createRequest.ThumbnailPath = thumbnailFileInfo.File;
+ }
+
+ var response = await _context.DiscountProducts.CreateDiscountProductAsync(createRequest, cancellationToken: cancellationToken);
+
+ return new CreateDiscountProductResponseDto
+ {
+ ProductId = response.ProductId
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs
new file mode 100644
index 0000000..55de238
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductCommandValidator.cs
@@ -0,0 +1,30 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
+
+public class CreateDiscountProductCommandValidator : AbstractValidator
+{
+ public CreateDiscountProductCommandValidator()
+ {
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("عنوان محصول الزامی است")
+ .MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
+
+ RuleFor(x => x.ShortInformation)
+ .NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")
+ .MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد");
+
+ RuleFor(x => x.FullInformation)
+ .NotEmpty().WithMessage("اطلاعات کامل محصول الزامی است");
+
+ RuleFor(x => x.Price)
+ .GreaterThan(0).WithMessage("قیمت محصول باید بیشتر از صفر باشد");
+
+ RuleFor(x => x.MaxDiscountPercent)
+ .InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
+
+ RuleFor(x => x.InitialCount)
+ .GreaterThanOrEqualTo(0).WithMessage("تعداد اولیه نمیتواند منفی باشد");
+
+ RuleFor(x => x.CategoryIds)
+ .NotEmpty().WithMessage("حداقل یک دستهبندی باید انتخاب شود");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductResponseDto.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductResponseDto.cs
new file mode 100644
index 0000000..d24c30b
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/CreateDiscountProduct/CreateDiscountProductResponseDto.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
+
+public class CreateDiscountProductResponseDto
+{
+ /// شناسه محصول ایجاد شده
+ public long ProductId { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommand.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommand.cs
new file mode 100644
index 0000000..e537a23
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommand.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
+
+public record DeleteDiscountProductCommand : IRequest
+{
+ /// شناسه محصول
+ public long ProductId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandHandler.cs
new file mode 100644
index 0000000..7a63c35
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandHandler.cs
@@ -0,0 +1,22 @@
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
+
+public class DeleteDiscountProductCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public DeleteDiscountProductCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(DeleteDiscountProductCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountProducts.DeleteDiscountProductAsync(
+ new DeleteDiscountProductRequest { ProductId = request.ProductId },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandValidator.cs
new file mode 100644
index 0000000..774b126
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/DeleteDiscountProduct/DeleteDiscountProductCommandValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
+
+public class DeleteDiscountProductCommandValidator : AbstractValidator
+{
+ public DeleteDiscountProductCommandValidator()
+ {
+ RuleFor(x => x.ProductId)
+ .GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommand.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommand.cs
new file mode 100644
index 0000000..5b94fb1
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommand.cs
@@ -0,0 +1,49 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
+
+public record UpdateDiscountProductCommand : IRequest
+{
+ /// شناسه محصول
+ public long ProductId { get; init; }
+
+ /// عنوان محصول
+ public string Title { get; init; }
+
+ /// اطلاعات کوتاه محصول
+ public string ShortInformation { get; init; }
+
+ /// اطلاعات کامل محصول
+ public string FullInformation { get; init; }
+
+ /// قیمت محصول
+ public long Price { get; init; }
+
+ /// حداکثر درصد تخفیف (0-100)
+ public int MaxDiscountPercent { get; init; }
+
+ /// ترتیب نمایش
+ public int SortOrder { get; init; }
+
+ /// وضعیت فعال/غیرفعال
+ public bool IsActive { get; init; }
+
+ /// آیدی دستهبندیها
+ public List CategoryIds { get; init; } = new();
+
+ /// فایل تصویر اصلی (اختیاری - برای تغییر تصویر)
+ public DiscountProductFileModel ImageFile { get; init; }
+
+ /// فایل تصویر کوچک (اختیاری - برای تغییر تصویر)
+ public DiscountProductFileModel ThumbnailFile { get; init; }
+}
+
+public class DiscountProductFileModel
+{
+ /// فایل
+ public byte[] File { get; set; }
+
+ /// نام فایل
+ public string FileName { get; set; }
+
+ /// نوع فایل
+ public string Mime { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs
new file mode 100644
index 0000000..0791cc6
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandHandler.cs
@@ -0,0 +1,67 @@
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+using Google.Protobuf;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
+
+public class UpdateDiscountProductCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public UpdateDiscountProductCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(UpdateDiscountProductCommand request, CancellationToken cancellationToken)
+ {
+ var updateRequest = new UpdateDiscountProductRequest
+ {
+ ProductId = request.ProductId,
+ Title = request.Title,
+ ShortInfomation = request.ShortInformation,
+ FullInformation = request.FullInformation,
+ Price = request.Price,
+ MaxDiscountPercent = request.MaxDiscountPercent,
+ SortOrder = request.SortOrder,
+ IsActive = request.IsActive
+ };
+
+ updateRequest.CategoryIds.AddRange(request.CategoryIds);
+
+ // Upload new Image if provided
+ if (request.ImageFile != null && request.ImageFile.File != null && request.ImageFile.File.Length > 0)
+ {
+ var imageFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Products",
+ IsBase64 = false,
+ MIME = request.ImageFile.Mime,
+ FileName = request.ImageFile.FileName,
+ File = ByteString.CopyFrom(request.ImageFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (imageFileInfo != null && !string.IsNullOrWhiteSpace(imageFileInfo.File))
+ updateRequest.ImagePath = imageFileInfo.File;
+ }
+
+ // Upload new Thumbnail if provided
+ if (request.ThumbnailFile != null && request.ThumbnailFile.File != null && request.ThumbnailFile.File.Length > 0)
+ {
+ var thumbnailFileInfo = await _context.FileInfos.CreateNewFileInfoAsync(new()
+ {
+ Directory = "Images/DiscountShop/Products/Thumbnails",
+ IsBase64 = false,
+ MIME = request.ThumbnailFile.Mime,
+ FileName = request.ThumbnailFile.FileName,
+ File = ByteString.CopyFrom(request.ThumbnailFile.File)
+ }, cancellationToken: cancellationToken);
+
+ if (thumbnailFileInfo != null && !string.IsNullOrWhiteSpace(thumbnailFileInfo.File))
+ updateRequest.ThumbnailPath = thumbnailFileInfo.File;
+ }
+
+ await _context.DiscountProducts.UpdateDiscountProductAsync(updateRequest, cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs
new file mode 100644
index 0000000..80f6bcb
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Commands/UpdateDiscountProduct/UpdateDiscountProductCommandValidator.cs
@@ -0,0 +1,30 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
+
+public class UpdateDiscountProductCommandValidator : AbstractValidator
+{
+ public UpdateDiscountProductCommandValidator()
+ {
+ RuleFor(x => x.ProductId)
+ .GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
+
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("عنوان محصول الزامی است")
+ .MaximumLength(200).WithMessage("عنوان محصول نباید بیشتر از 200 کاراکتر باشد");
+
+ RuleFor(x => x.ShortInformation)
+ .NotEmpty().WithMessage("اطلاعات کوتاه محصول الزامی است")
+ .MaximumLength(500).WithMessage("اطلاعات کوتاه نباید بیشتر از 500 کاراکتر باشد");
+
+ RuleFor(x => x.FullInformation)
+ .NotEmpty().WithMessage("اطلاعات کامل محصول الزامی است");
+
+ RuleFor(x => x.Price)
+ .GreaterThan(0).WithMessage("قیمت محصول باید بیشتر از صفر باشد");
+
+ RuleFor(x => x.MaxDiscountPercent)
+ .InclusiveBetween(0, 100).WithMessage("درصد تخفیف باید بین 0 تا 100 باشد");
+
+ RuleFor(x => x.CategoryIds)
+ .NotEmpty().WithMessage("حداقل یک دستهبندی باید انتخاب شود");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQuery.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQuery.cs
new file mode 100644
index 0000000..bf2f1a2
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQuery.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
+
+public record GetDiscountProductByIdQuery : IRequest
+{
+ /// شناسه محصول
+ public long ProductId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryHandler.cs
new file mode 100644
index 0000000..e25f588
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryHandler.cs
@@ -0,0 +1,43 @@
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
+
+public class GetDiscountProductByIdQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetDiscountProductByIdQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetDiscountProductByIdQuery request, CancellationToken cancellationToken)
+ {
+ var response = await _context.DiscountProducts.GetDiscountProductByIdAsync(
+ new GetDiscountProductByIdRequest { ProductId = request.ProductId },
+ cancellationToken: cancellationToken);
+
+ return new GetDiscountProductByIdResponseDto
+ {
+ Id = response.Id,
+ Title = response.Title,
+ ShortInformation = response.ShortInfomation,
+ FullInformation = response.FullInformation,
+ Price = response.Price,
+ MaxDiscountPercent = response.MaxDiscountPercent,
+ ImagePath = response.ImagePath,
+ ThumbnailPath = response.ThumbnailPath,
+ RemainingCount = response.RemainingCount,
+ ViewCount = response.ViewCount,
+ SortOrder = response.SortOrder,
+ IsActive = response.IsActive,
+ Categories = response.Categories.Select(c => new CategoryInfoDto
+ {
+ Id = c.Id,
+ Name = c.Name,
+ Title = c.Title
+ }).ToList(),
+ Created = response.Created.ToDateTime()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryValidator.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryValidator.cs
new file mode 100644
index 0000000..4bb5760
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdQueryValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
+
+public class GetDiscountProductByIdQueryValidator : AbstractValidator
+{
+ public GetDiscountProductByIdQueryValidator()
+ {
+ RuleFor(x => x.ProductId)
+ .GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdResponseDto.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdResponseDto.cs
new file mode 100644
index 0000000..13e3dbc
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProductById/GetDiscountProductByIdResponseDto.cs
@@ -0,0 +1,26 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
+
+public class GetDiscountProductByIdResponseDto
+{
+ public long Id { get; set; }
+ public string Title { get; set; }
+ public string ShortInformation { get; set; }
+ public string FullInformation { get; set; }
+ public long Price { get; set; }
+ public int MaxDiscountPercent { get; set; }
+ public string ImagePath { get; set; }
+ public string ThumbnailPath { get; set; }
+ public int RemainingCount { get; set; }
+ public int ViewCount { get; set; }
+ public int SortOrder { get; set; }
+ public bool IsActive { get; set; }
+ public List Categories { get; set; } = new();
+ public DateTime Created { get; set; }
+}
+
+public class CategoryInfoDto
+{
+ public long Id { get; set; }
+ public string Name { get; set; }
+ public string Title { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQuery.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQuery.cs
new file mode 100644
index 0000000..ab3fdd3
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQuery.cs
@@ -0,0 +1,28 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
+
+public record GetDiscountProductsQuery : IRequest
+{
+ /// فیلتر براساس دستهبندی
+ public long? CategoryId { get; init; }
+
+ /// جستجو در عنوان و توضیحات
+ public string SearchQuery { get; init; }
+
+ /// حداقل قیمت
+ public long? MinPrice { get; init; }
+
+ /// حداکثر قیمت
+ public long? MaxPrice { get; init; }
+
+ /// فقط محصولات فعال
+ public bool? IsActive { get; init; }
+
+ /// فقط محصولات موجود در انبار
+ public bool? InStock { get; init; }
+
+ /// شماره صفحه
+ public int PageNumber { get; init; } = 1;
+
+ /// تعداد در هر صفحه
+ public int PageSize { get; init; } = 20;
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs
new file mode 100644
index 0000000..be24a15
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryHandler.cs
@@ -0,0 +1,71 @@
+using BackOffice.BFF.Application.Common.Models;
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+using Google.Protobuf.WellKnownTypes;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
+
+public class GetDiscountProductsQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetDiscountProductsQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetDiscountProductsQuery request, CancellationToken cancellationToken)
+ {
+ var grpcRequest = new GetDiscountProductsRequest
+ {
+ PageNumber = request.PageNumber,
+ PageSize = request.PageSize
+ };
+
+ if (request.CategoryId.HasValue)
+ grpcRequest.CategoryId = new Int64Value { Value = request.CategoryId.Value };
+
+ if (!string.IsNullOrWhiteSpace(request.SearchQuery))
+ grpcRequest.SearchQuery = new StringValue { Value = request.SearchQuery };
+
+ if (request.MinPrice.HasValue)
+ grpcRequest.MinPrice = new Int64Value { Value = request.MinPrice.Value };
+
+ if (request.MaxPrice.HasValue)
+ grpcRequest.MaxPrice = new Int64Value { Value = request.MaxPrice.Value };
+
+ if (request.IsActive.HasValue)
+ grpcRequest.IsActive = new BoolValue { Value = request.IsActive.Value };
+
+ if (request.InStock.HasValue)
+ grpcRequest.InStock = new BoolValue { Value = request.InStock.Value };
+
+ var response = await _context.DiscountProducts.GetDiscountProductsAsync(grpcRequest, cancellationToken: cancellationToken);
+
+ return new GetDiscountProductsResponseDto
+ {
+ MetaData = new MetaData
+ {
+ CurrentPage = response.MetaData.CurrentPage,
+ TotalPage = response.MetaData.TotalPages,
+ PageSize = response.MetaData.PageSize,
+ TotalCount = response.MetaData.TotalCount,
+ HasPrevious = response.MetaData.HasPrevious,
+ HasNext = response.MetaData.HasNext
+ },
+ Products = response.Models.Select(p => new DiscountProductDto
+ {
+ Id = p.Id,
+ Title = p.Title,
+ ShortInformation = p.ShortInfomation,
+ Price = p.Price,
+ MaxDiscountPercent = p.MaxDiscountPercent,
+ ImagePath = p.ImagePath,
+ ThumbnailPath = p.ThumbnailPath,
+ RemainingCount = p.RemainingCount,
+ ViewCount = p.ViewCount,
+ IsActive = p.IsActive,
+ Created = p.Created.ToDateTime()
+ }).ToList()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryValidator.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryValidator.cs
new file mode 100644
index 0000000..dc9701d
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsQueryValidator.cs
@@ -0,0 +1,24 @@
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
+
+public class GetDiscountProductsQueryValidator : AbstractValidator
+{
+ public GetDiscountProductsQueryValidator()
+ {
+ RuleFor(x => x.PageNumber)
+ .GreaterThan(0).WithMessage("شماره صفحه باید بیشتر از صفر باشد");
+
+ RuleFor(x => x.PageSize)
+ .GreaterThan(0).WithMessage("تعداد در صفحه باید بیشتر از صفر باشد")
+ .LessThanOrEqualTo(100).WithMessage("حداکثر تعداد در صفحه 100 است");
+
+ RuleFor(x => x.MinPrice)
+ .GreaterThanOrEqualTo(0).When(x => x.MinPrice.HasValue)
+ .WithMessage("حداقل قیمت نمیتواند منفی باشد");
+
+ RuleFor(x => x.MaxPrice)
+ .GreaterThanOrEqualTo(0).When(x => x.MaxPrice.HasValue)
+ .WithMessage("حداکثر قیمت نمیتواند منفی باشد")
+ .GreaterThanOrEqualTo(x => x.MinPrice).When(x => x.MinPrice.HasValue && x.MaxPrice.HasValue)
+ .WithMessage("حداکثر قیمت باید بیشتر از حداقل قیمت باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsResponseDto.cs b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsResponseDto.cs
new file mode 100644
index 0000000..01d875d
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountProductCQ/Queries/GetDiscountProducts/GetDiscountProductsResponseDto.cs
@@ -0,0 +1,24 @@
+using BackOffice.BFF.Application.Common.Models;
+
+namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
+
+public class GetDiscountProductsResponseDto
+{
+ public MetaData MetaData { get; set; }
+ public List Products { get; set; } = new();
+}
+
+public class DiscountProductDto
+{
+ public long Id { get; set; }
+ public string Title { get; set; }
+ public string ShortInformation { get; set; }
+ public long Price { get; set; }
+ public int MaxDiscountPercent { get; set; }
+ public string ImagePath { get; set; }
+ public string ThumbnailPath { get; set; }
+ public int RemainingCount { get; set; }
+ public int ViewCount { get; set; }
+ public bool IsActive { get; set; }
+ public DateTime Created { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommand.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommand.cs
new file mode 100644
index 0000000..fd8d84a
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommand.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
+
+public record AddToCartCommand : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+
+ /// شناسه محصول
+ public long ProductId { get; init; }
+
+ /// تعداد
+ public int Count { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandHandler.cs
new file mode 100644
index 0000000..6518a4c
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandHandler.cs
@@ -0,0 +1,27 @@
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
+
+public class AddToCartCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public AddToCartCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(AddToCartCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountShoppingCarts.AddToCartAsync(
+ new AddToCartRequest
+ {
+ UserId = request.UserId,
+ ProductId = request.ProductId,
+ Count = request.Count
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandValidator.cs
new file mode 100644
index 0000000..87bc881
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/AddToCart/AddToCartCommandValidator.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
+
+public class AddToCartCommandValidator : AbstractValidator
+{
+ public AddToCartCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+
+ RuleFor(x => x.ProductId)
+ .GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
+
+ RuleFor(x => x.Count)
+ .GreaterThan(0).WithMessage("تعداد باید بیشتر از صفر باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommand.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommand.cs
new file mode 100644
index 0000000..be5bccd
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommand.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
+
+public record ClearCartCommand : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandHandler.cs
new file mode 100644
index 0000000..9308677
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandHandler.cs
@@ -0,0 +1,22 @@
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
+
+public class ClearCartCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public ClearCartCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(ClearCartCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountShoppingCarts.ClearCartAsync(
+ new ClearCartRequest { UserId = request.UserId },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandValidator.cs
new file mode 100644
index 0000000..9927c01
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/ClearCart/ClearCartCommandValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
+
+public class ClearCartCommandValidator : AbstractValidator
+{
+ public ClearCartCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommand.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommand.cs
new file mode 100644
index 0000000..c433270
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommand.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
+
+public record RemoveFromCartCommand : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+
+ /// شناسه آیتم سبد خرید
+ public long CartItemId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandHandler.cs
new file mode 100644
index 0000000..88c3f29
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandHandler.cs
@@ -0,0 +1,26 @@
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
+
+public class RemoveFromCartCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public RemoveFromCartCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(RemoveFromCartCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountShoppingCarts.RemoveFromCartAsync(
+ new RemoveFromCartRequest
+ {
+ UserId = request.UserId,
+ CartItemId = request.CartItemId
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandValidator.cs
new file mode 100644
index 0000000..749ea69
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/RemoveFromCart/RemoveFromCartCommandValidator.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
+
+public class RemoveFromCartCommandValidator : AbstractValidator
+{
+ public RemoveFromCartCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+
+ RuleFor(x => x.CartItemId)
+ .GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommand.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommand.cs
new file mode 100644
index 0000000..1827200
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommand.cs
@@ -0,0 +1,13 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
+
+public record UpdateCartItemCountCommand : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+
+ /// شناسه آیتم سبد خرید
+ public long CartItemId { get; init; }
+
+ /// تعداد جدید
+ public int NewCount { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandHandler.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandHandler.cs
new file mode 100644
index 0000000..5a9e815
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandHandler.cs
@@ -0,0 +1,27 @@
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
+
+public class UpdateCartItemCountCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public UpdateCartItemCountCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(UpdateCartItemCountCommand request, CancellationToken cancellationToken)
+ {
+ await _context.DiscountShoppingCarts.UpdateCartItemCountAsync(
+ new UpdateCartItemCountRequest
+ {
+ UserId = request.UserId,
+ CartItemId = request.CartItemId,
+ NewCount = request.NewCount
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandValidator.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandValidator.cs
new file mode 100644
index 0000000..644fb49
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Commands/UpdateCartItemCount/UpdateCartItemCountCommandValidator.cs
@@ -0,0 +1,16 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
+
+public class UpdateCartItemCountCommandValidator : AbstractValidator
+{
+ public UpdateCartItemCountCommandValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+
+ RuleFor(x => x.CartItemId)
+ .GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
+
+ RuleFor(x => x.NewCount)
+ .GreaterThan(0).WithMessage("تعداد جدید باید بیشتر از صفر باشد");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQuery.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQuery.cs
new file mode 100644
index 0000000..3c7697f
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQuery.cs
@@ -0,0 +1,7 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
+
+public record GetUserCartQuery : IRequest
+{
+ /// شناسه کاربر
+ public long UserId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryHandler.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryHandler.cs
new file mode 100644
index 0000000..48541ed
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryHandler.cs
@@ -0,0 +1,41 @@
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
+
+public class GetUserCartQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetUserCartQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetUserCartQuery request, CancellationToken cancellationToken)
+ {
+ var response = await _context.DiscountShoppingCarts.GetUserCartAsync(
+ new GetUserCartRequest { UserId = request.UserId },
+ cancellationToken: cancellationToken);
+
+ return new GetUserCartResponseDto
+ {
+ UserId = response.UserId,
+ TotalPrice = response.TotalPrice,
+ TotalDiscountedPrice = response.TotalDiscountedPrice,
+ TotalSavings = response.TotalSavings,
+ Items = response.Items.Select(item => new CartItemDto
+ {
+ Id = item.Id,
+ ProductId = item.ProductId,
+ ProductTitle = item.ProductTitle,
+ ProductImagePath = item.ProductImagePath,
+ UnitPrice = item.UnitPrice,
+ MaxDiscountPercent = item.MaxDiscountPercent,
+ Count = item.Count,
+ TotalPrice = item.TotalPrice,
+ DiscountedPrice = item.DiscountedPrice,
+ AddedAt = item.AddedAt.ToDateTime()
+ }).ToList()
+ };
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryValidator.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryValidator.cs
new file mode 100644
index 0000000..58712fa
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartQueryValidator.cs
@@ -0,0 +1,10 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
+
+public class GetUserCartQueryValidator : AbstractValidator
+{
+ public GetUserCartQueryValidator()
+ {
+ RuleFor(x => x.UserId)
+ .GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
+ }
+}
diff --git a/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartResponseDto.cs b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartResponseDto.cs
new file mode 100644
index 0000000..3f81de8
--- /dev/null
+++ b/src/BackOffice.BFF.Application/DiscountShoppingCartCQ/Queries/GetUserCart/GetUserCartResponseDto.cs
@@ -0,0 +1,24 @@
+namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
+
+public class GetUserCartResponseDto
+{
+ public long UserId { get; set; }
+ public List Items { get; set; } = new();
+ public long TotalPrice { get; set; }
+ public long TotalDiscountedPrice { get; set; }
+ public long TotalSavings { get; set; }
+}
+
+public class CartItemDto
+{
+ public long Id { get; set; }
+ public long ProductId { get; set; }
+ public string ProductTitle { get; set; }
+ public string ProductImagePath { get; set; }
+ public long UnitPrice { get; set; }
+ public int MaxDiscountPercent { get; set; }
+ public int Count { get; set; }
+ public long TotalPrice { get; set; }
+ public long DiscountedPrice { get; set; }
+ public DateTime AddedAt { get; set; }
+}
diff --git a/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommand.cs b/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommand.cs
new file mode 100644
index 0000000..a1622f7
--- /dev/null
+++ b/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommand.cs
@@ -0,0 +1,9 @@
+using CMSMicroservice.Protobuf.Protos.ProductTag;
+
+namespace BackOffice.BFF.Application.ProductTagCQ.Commands.AssignTagToProduct;
+
+public record AssignTagToProductCommand : IRequest
+{
+ public long ProductId { get; init; }
+ public long TagId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommandHandler.cs b/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommandHandler.cs
new file mode 100644
index 0000000..c60f812
--- /dev/null
+++ b/src/BackOffice.BFF.Application/ProductTagCQ/Commands/AssignTagToProduct/AssignTagToProductCommandHandler.cs
@@ -0,0 +1,26 @@
+using CMSMicroservice.Protobuf.Protos.ProductTag;
+
+namespace BackOffice.BFF.Application.ProductTagCQ.Commands.AssignTagToProduct;
+
+public class AssignTagToProductCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public AssignTagToProductCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(AssignTagToProductCommand request, CancellationToken cancellationToken)
+ {
+ var response = await _context.ProductTags.CreateNewProductTagAsync(
+ new CreateNewProductTagRequest
+ {
+ ProductId = request.ProductId,
+ TagId = request.TagId
+ },
+ cancellationToken: cancellationToken);
+
+ return response.Id;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs
index 15a8ce6..d679f29 100644
--- a/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs
+++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/AddProductImage/AddProductImageCommandHandler.cs
@@ -1,7 +1,7 @@
using System.IO;
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ProductImages;
-using CMSMicroservice.Protobuf.Protos.ProductGallerys;
+using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using Google.Protobuf;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
@@ -59,7 +59,7 @@ public class AddProductImageCommandHandler : IRequestHandler Handle(RemoveProductImageCommand request, CancellationToken cancellationToken)
{
- await _context.ProductGallerys.DeleteProductGallerysAsync(new DeleteProductGallerysRequest
+ await _context.ProductGalleries.DeleteProductGalleriesAsync(new DeleteProductGalleriesRequest
{
Id = request.ProductGalleryId
}, cancellationToken: cancellationToken);
diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Commands/UpdateCategoryProducts/UpdateCategoryProductsCommandHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Commands/UpdateCategoryProducts/UpdateCategoryProductsCommandHandler.cs
index a17d32c..95bcebc 100644
--- a/src/BackOffice.BFF.Application/ProductsCQ/Commands/UpdateCategoryProducts/UpdateCategoryProductsCommandHandler.cs
+++ b/src/BackOffice.BFF.Application/ProductsCQ/Commands/UpdateCategoryProducts/UpdateCategoryProductsCommandHandler.cs
@@ -1,5 +1,5 @@
using BackOffice.BFF.Application.Common.Interfaces;
-using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory;
+using CMSProductCategory = CMSMicroservice.Protobuf.Protos.ProductCategory;
namespace BackOffice.BFF.Application.ProductsCQ.Commands.UpdateCategoryProducts;
@@ -17,9 +17,9 @@ public class UpdateCategoryProductsCommandHandler : IRequestHandler();
// load existing links for this category
- var existingRequest = new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
+ var existingRequest = new CMSProductCategory.GetAllProductCategoryByFilterRequest
{
- Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
+ Filter = new CMSProductCategory.GetAllProductCategoryByFilterFilter
{
CategoryId = request.CategoryId
},
@@ -30,7 +30,7 @@ public class UpdateCategoryProductsCommandHandler : IRequestHandler x.ProductId).ToHashSet();
@@ -40,19 +40,19 @@ public class UpdateCategoryProductsCommandHandler : IRequestHandler();
// Load existing product-category relations
- var existingRequest = new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
+ var existingRequest = new CMSProductCategory.GetAllProductCategoryByFilterRequest
{
- Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
+ Filter = new CMSProductCategory.GetAllProductCategoryByFilterFilter
{
ProductId = request.ProductId
},
@@ -30,7 +30,7 @@ public class UpdateProductCategoriesCommandHandler : IRequestHandler x.CategoryId).ToHashSet();
@@ -41,19 +41,19 @@ public class UpdateProductCategoriesCommandHandler : IRequestHandler Handle(GetProductGalleryQuery request, CancellationToken cancellationToken)
{
- var galleryRequest = new GetAllProductGallerysByFilterRequest
+ var grpcRequest = new GetAllProductGalleriesByFilterRequest
{
- Filter = new GetAllProductGallerysByFilterFilter()
+ Filter = new GetAllProductGalleriesByFilterFilter()
};
- var galleryResponse = await _context.ProductGallerys.GetAllProductGallerysByFilterAsync(galleryRequest, cancellationToken: cancellationToken);
+ var galleryResponse = await _context.ProductGalleries.GetAllProductGalleriesByFilterAsync(grpcRequest, cancellationToken: cancellationToken);
// Filter by product id on client side because generated type may not support assigning Int64Value directly
var filteredModels = galleryResponse?.Models?.Where(x => x.ProductId == request.ProductId).ToList();
diff --git a/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductsForCategory/GetProductsForCategoryQueryHandler.cs b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductsForCategory/GetProductsForCategoryQueryHandler.cs
index 54962cc..2dcec8c 100644
--- a/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductsForCategory/GetProductsForCategoryQueryHandler.cs
+++ b/src/BackOffice.BFF.Application/ProductsCQ/Queries/GetProductsForCategory/GetProductsForCategoryQueryHandler.cs
@@ -1,6 +1,6 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSProducts = CMSMicroservice.Protobuf.Protos.Products;
-using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory;
+using CMSProductCategory = CMSMicroservice.Protobuf.Protos.ProductCategory;
namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetProductsForCategory;
@@ -30,9 +30,9 @@ public class GetProductsForCategoryQueryHandler : IRequestHandler l.ProductId).ToHashSet();
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommand.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommand.cs
new file mode 100644
index 0000000..8940910
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommand.cs
@@ -0,0 +1,12 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Commands.CreateTag;
+
+public record CreateTagCommand : IRequest
+{
+ public string Name { get; init; } = string.Empty;
+ public string Title { get; init; } = string.Empty;
+ public string? Description { get; init; }
+ public bool IsActive { get; init; } = true;
+ public int SortOrder { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommandHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommandHandler.cs
new file mode 100644
index 0000000..6ddcc07
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/CreateTag/CreateTagCommandHandler.cs
@@ -0,0 +1,29 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Commands.CreateTag;
+
+public class CreateTagCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public CreateTagCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(CreateTagCommand request, CancellationToken cancellationToken)
+ {
+ var response = await _context.Tags.CreateNewTagAsync(
+ new CreateNewTagRequest
+ {
+ Name = request.Name,
+ Title = request.Title,
+ Description = request.Description,
+ IsActive = request.IsActive,
+ SortOrder = request.SortOrder
+ },
+ cancellationToken: cancellationToken);
+
+ return response.Id;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommand.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommand.cs
new file mode 100644
index 0000000..45af23b
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommand.cs
@@ -0,0 +1,6 @@
+namespace BackOffice.BFF.Application.TagCQ.Commands.DeleteTag;
+
+public record DeleteTagCommand : IRequest
+{
+ public long Id { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommandHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommandHandler.cs
new file mode 100644
index 0000000..b4ff0e0
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/DeleteTag/DeleteTagCommandHandler.cs
@@ -0,0 +1,25 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Commands.DeleteTag;
+
+public class DeleteTagCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public DeleteTagCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(DeleteTagCommand request, CancellationToken cancellationToken)
+ {
+ await _context.Tags.DeleteTagAsync(
+ new DeleteTagRequest
+ {
+ Id = request.Id
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommand.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommand.cs
new file mode 100644
index 0000000..784db27
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommand.cs
@@ -0,0 +1,13 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Commands.UpdateTag;
+
+public record UpdateTagCommand : IRequest
+{
+ public long Id { get; init; }
+ public string Name { get; init; } = string.Empty;
+ public string Title { get; init; } = string.Empty;
+ public string? Description { get; init; }
+ public bool IsActive { get; init; }
+ public int SortOrder { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommandHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommandHandler.cs
new file mode 100644
index 0000000..99cbdd9
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Commands/UpdateTag/UpdateTagCommandHandler.cs
@@ -0,0 +1,30 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Commands.UpdateTag;
+
+public class UpdateTagCommandHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public UpdateTagCommandHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(UpdateTagCommand request, CancellationToken cancellationToken)
+ {
+ await _context.Tags.UpdateTagAsync(
+ new UpdateTagRequest
+ {
+ Id = request.Id,
+ Name = request.Name,
+ Title = request.Title,
+ Description = request.Description,
+ IsActive = request.IsActive,
+ SortOrder = request.SortOrder
+ },
+ cancellationToken: cancellationToken);
+
+ return Unit.Value;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQuery.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQuery.cs
new file mode 100644
index 0000000..5b1b387
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQuery.cs
@@ -0,0 +1,11 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetAllTags;
+
+public record GetAllTagsQuery : IRequest
+{
+ public int PageNumber { get; init; } = 1;
+ public int PageSize { get; init; } = 10;
+ public string? SearchTerm { get; init; }
+ public bool? IsActive { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQueryHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQueryHandler.cs
new file mode 100644
index 0000000..fa893e5
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetAllTags/GetAllTagsQueryHandler.cs
@@ -0,0 +1,41 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+using Google.Protobuf.WellKnownTypes;
+using CMSMicroservice.Protobuf.Protos;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetAllTags;
+
+public class GetAllTagsQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetAllTagsQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetAllTagsQuery request, CancellationToken cancellationToken)
+ {
+ var grpcRequest = new GetAllTagByFilterRequest
+ {
+ PaginationState = new CMSMicroservice.Protobuf.Protos.PaginationState
+ {
+ PageNumber = request.PageNumber,
+ PageSize = request.PageSize
+ },
+ Filter = new GetAllTagByFilterFilter()
+ };
+
+ if (!string.IsNullOrEmpty(request.SearchTerm))
+ {
+ grpcRequest.Filter.Name = request.SearchTerm;
+ }
+
+ if (request.IsActive.HasValue)
+ {
+ grpcRequest.Filter.IsActive = request.IsActive.Value;
+ }
+
+ var response = await _context.Tags.GetAllTagByFilterAsync(grpcRequest, cancellationToken: cancellationToken);
+ return response;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQuery.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQuery.cs
new file mode 100644
index 0000000..faa2224
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQuery.cs
@@ -0,0 +1,8 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetProductsByTag;
+
+public record GetProductsByTagQuery : IRequest
+{
+ public long TagId { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQueryHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQueryHandler.cs
new file mode 100644
index 0000000..c0b2843
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetProductsByTag/GetProductsByTagQueryHandler.cs
@@ -0,0 +1,22 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetProductsByTag;
+
+public class GetProductsByTagQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetProductsByTagQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetProductsByTagQuery request, CancellationToken cancellationToken)
+ {
+ var response = await _context.Tags.GetProductsByTagAsync(
+ new GetProductsByTagRequest { TagId = request.TagId },
+ cancellationToken: cancellationToken);
+
+ return response;
+ }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQuery.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQuery.cs
new file mode 100644
index 0000000..6bec908
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQuery.cs
@@ -0,0 +1,8 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetTag;
+
+public record GetTagQuery : IRequest
+{
+ public long Id { get; init; }
+}
diff --git a/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQueryHandler.cs b/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQueryHandler.cs
new file mode 100644
index 0000000..3424753
--- /dev/null
+++ b/src/BackOffice.BFF.Application/TagCQ/Queries/GetTag/GetTagQueryHandler.cs
@@ -0,0 +1,22 @@
+using CMSMicroservice.Protobuf.Protos.Tag;
+
+namespace BackOffice.BFF.Application.TagCQ.Queries.GetTag;
+
+public class GetTagQueryHandler : IRequestHandler
+{
+ private readonly IApplicationContractContext _context;
+
+ public GetTagQueryHandler(IApplicationContractContext context)
+ {
+ _context = context;
+ }
+
+ public async Task Handle(GetTagQuery request, CancellationToken cancellationToken)
+ {
+ var response = await _context.Tags.GetTagAsync(
+ new GetTagRequest { Id = request.Id },
+ cancellationToken: cancellationToken);
+
+ return response;
+ }
+}
diff --git a/src/BackOffice.BFF.Domain/BackOffice.BFF.Domain.csproj b/src/BackOffice.BFF.Domain/BackOffice.BFF.Domain.csproj
index f674477..520241c 100644
--- a/src/BackOffice.BFF.Domain/BackOffice.BFF.Domain.csproj
+++ b/src/BackOffice.BFF.Domain/BackOffice.BFF.Domain.csproj
@@ -7,11 +7,15 @@
-
+
+
+
+
+
diff --git a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs
index 108d080..1edf49f 100644
--- a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs
+++ b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs
@@ -7,14 +7,20 @@ using CMSMicroservice.Protobuf.Protos.UserOrder;
using CMSMicroservice.Protobuf.Protos.UserRole;
using CMSMicroservice.Protobuf.Protos.Products;
using CMSMicroservice.Protobuf.Protos.ProductImages;
-using CMSMicroservice.Protobuf.Protos.ProductGallerys;
+using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.Category;
-using CMSMicroservice.Protobuf.Protos.PruductCategory;
+using CMSMicroservice.Protobuf.Protos.ProductCategory;
using BackOffice.BFF.Commission.Protobuf;
using BackOffice.BFF.NetworkMembership.Protobuf;
using BackOffice.BFF.ClubMembership.Protobuf;
using BackOffice.BFF.Configuration.Protobuf;
using FMSMicroservice.Protobuf.Protos.FileInfo;
+using CMSMicroservice.Protobuf.Protos.DiscountProduct;
+using CMSMicroservice.Protobuf.Protos.DiscountCategory;
+using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
+using CMSMicroservice.Protobuf.Protos.DiscountOrder;
+using CMSMicroservice.Protobuf.Protos.Tag;
+using CMSMicroservice.Protobuf.Protos.ProductTag;
using Microsoft.Extensions.DependencyInjection;
namespace BackOffice.BFF.Infrastructure.Services;
@@ -50,10 +56,10 @@ public class ApplicationContractContext : IApplicationContractContext
public PackageContract.PackageContractClient Packages => GetService();
public ProductsContract.ProductsContractClient Products => GetService();
public ProductImagesContract.ProductImagesContractClient ProductImages => GetService();
- public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService();
+ public ProductGalleriesContract.ProductGalleriesContractClient ProductGalleries => GetService();
public RoleContract.RoleContractClient Roles => GetService();
public CategoryContract.CategoryContractClient Categories => GetService();
- public PruductCategoryContract.PruductCategoryContractClient ProductCategories => GetService();
+ public ProductCategoryContract.ProductCategoryContractClient ProductCategories => GetService();
public UserAddressContract.UserAddressContractClient UserAddress => GetService();
public UserContract.UserContractClient Users => GetService();
public UserOrderContract.UserOrderContractClient UserOrders => GetService();
@@ -64,5 +70,15 @@ public class ApplicationContractContext : IApplicationContractContext
public NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships => GetService();
public ClubMembershipContract.ClubMembershipContractClient ClubMemberships => GetService();
public ConfigurationContract.ConfigurationContractClient Configurations => GetService();
+
+ // Discount Shop System (Phase 9)
+ public DiscountProductContract.DiscountProductContractClient DiscountProducts => GetService();
+ public DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories => GetService();
+ public DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountShoppingCarts => GetService();
+ public DiscountOrderContract.DiscountOrderContractClient DiscountOrders => GetService();
+
+ // Tag Management System
+ public TagContract.TagContractClient Tags => GetService();
+ public ProductTagContract.ProductTagContractClient ProductTags => GetService();
#endregion
}
diff --git a/src/Protobufs/BackOffice.BFF.Commission.Protobuf/Protos/commission.proto b/src/Protobufs/BackOffice.BFF.Commission.Protobuf/Protos/commission.proto
index c8523ab..2faab23 100644
--- a/src/Protobufs/BackOffice.BFF.Commission.Protobuf/Protos/commission.proto
+++ b/src/Protobufs/BackOffice.BFF.Commission.Protobuf/Protos/commission.proto
@@ -48,6 +48,8 @@ service CommissionContract
};
rpc GetWorkerExecutionLogs(GetWorkerExecutionLogsRequest) returns (GetWorkerExecutionLogsResponse){
};
+ rpc GetWithdrawalReports(GetWithdrawalReportsRequest) returns (GetWithdrawalReportsResponse){
+ };
}
// ============ Commands ============
@@ -347,3 +349,47 @@ message WorkerExecutionLogModel
int32 records_processed = 9;
google.protobuf.StringValue details = 10; // JSON or text details
}
+
+// GetWithdrawalReports Query
+message GetWithdrawalReportsRequest
+{
+ google.protobuf.Timestamp start_date = 1; // Optional - default: 30 days ago
+ google.protobuf.Timestamp end_date = 2; // Optional - default: today
+ int32 period_type = 3; // ReportPeriodType: Daily=1, Weekly=2, Monthly=3
+ google.protobuf.Int32Value status = 4; // CommissionPayoutStatus enum (optional)
+ google.protobuf.Int64Value user_id = 5; // Optional user filter
+}
+
+message GetWithdrawalReportsResponse
+{
+ repeated PeriodReport period_reports = 1;
+ WithdrawalSummary summary = 2;
+}
+
+message PeriodReport
+{
+ string period_label = 1; // e.g., "2025-01-15", "هفته 3", "فروردین 1404"
+ google.protobuf.Timestamp start_date = 2;
+ google.protobuf.Timestamp end_date = 3;
+ int32 total_requests = 4;
+ int32 pending_count = 5;
+ int32 approved_count = 6;
+ int32 rejected_count = 7;
+ int32 completed_count = 8;
+ int32 failed_count = 9;
+ int64 total_amount = 10;
+ int64 paid_amount = 11;
+ int64 pending_amount = 12;
+}
+
+message WithdrawalSummary
+{
+ int32 total_requests = 1;
+ int64 total_amount = 2;
+ int64 total_paid = 3;
+ int64 total_pending = 4;
+ int64 total_rejected = 5;
+ int64 average_amount = 6;
+ int32 unique_users = 7;
+ float success_rate = 8; // Percentage (0-100)
+}