feat: Implement Discount Product and Shopping Cart functionalities

- Added UpdateDiscountProductCommandValidator for validating discount product updates.
- Created GetDiscountProductByIdQuery and its handler for retrieving discount product details by ID.
- Implemented GetDiscountProductsQuery and handler for fetching a list of discount products with filtering options.
- Developed AddToCartCommand and handler for adding products to the shopping cart.
- Implemented ClearCartCommand and handler for clearing the shopping cart.
- Created RemoveFromCartCommand and handler for removing items from the cart.
- Added UpdateCartItemCountCommand and handler for updating the quantity of items in the cart.
- Developed GetUserCartQuery and handler for retrieving the user's shopping cart details.
- Implemented Product Tag functionalities including assigning tags to products, creating, updating, and deleting tags.
- Added queries for fetching all tags and products by tag.
This commit is contained in:
masoodafar-web
2025-12-04 02:41:19 +03:30
parent c9dab944fa
commit 4f400eabc5
92 changed files with 2285 additions and 41 deletions

View File

@@ -0,0 +1,181 @@
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalReports;
/// <summary>
/// گزارش برداشت‌ها بر اساس بازه زمانی و دسته‌بندی
/// </summary>
public record GetWithdrawalReportsQuery : IRequest<GetWithdrawalReportsResponseDto>
{
/// <summary>
/// تاریخ شروع (اختیاری - پیش‌فرض: 30 روز قبل)
/// </summary>
public DateTime? StartDate { get; init; }
/// <summary>
/// تاریخ پایان (اختیاری - پیش‌فرض: امروز)
/// </summary>
public DateTime? EndDate { get; init; }
/// <summary>
/// نوع دوره گزارش: 1=روزانه, 2=هفتگی, 3=ماهانه
/// </summary>
public ReportPeriodType PeriodType { get; init; } = ReportPeriodType.Daily;
/// <summary>
/// فیلتر وضعیت (اختیاری)
/// </summary>
public int? Status { get; init; }
/// <summary>
/// فیلتر شناسه کاربر (اختیاری)
/// </summary>
public long? UserId { get; init; }
}
/// <summary>
/// نوع دسته‌بندی گزارش
/// </summary>
public enum ReportPeriodType
{
/// <summary>
/// روزانه
/// </summary>
Daily = 1,
/// <summary>
/// هفتگی
/// </summary>
Weekly = 2,
/// <summary>
/// ماهانه
/// </summary>
Monthly = 3
}
/// <summary>
/// پاسخ گزارش برداشت‌ها
/// </summary>
public class GetWithdrawalReportsResponseDto
{
/// <summary>
/// گزارش‌های هر دوره
/// </summary>
public List<PeriodReportDto> PeriodReports { get; set; } = new();
/// <summary>
/// خلاصه کل گزارش
/// </summary>
public WithdrawalSummaryDto Summary { get; set; } = new();
}
/// <summary>
/// گزارش یک دوره زمانی
/// </summary>
public class PeriodReportDto
{
/// <summary>
/// برچسب دوره (مثلاً "2025-01-15" یا "هفته 3" یا "فروردین 1404")
/// </summary>
public string PeriodLabel { get; set; } = string.Empty;
/// <summary>
/// تاریخ شروع دوره
/// </summary>
public DateTime StartDate { get; set; }
/// <summary>
/// تاریخ پایان دوره
/// </summary>
public DateTime EndDate { get; set; }
/// <summary>
/// تعداد کل درخواست‌ها
/// </summary>
public int TotalRequests { get; set; }
/// <summary>
/// تعداد در انتظار
/// </summary>
public int PendingCount { get; set; }
/// <summary>
/// تعداد تایید شده
/// </summary>
public int ApprovedCount { get; set; }
/// <summary>
/// تعداد رد شده
/// </summary>
public int RejectedCount { get; set; }
/// <summary>
/// تعداد تکمیل شده
/// </summary>
public int CompletedCount { get; set; }
/// <summary>
/// تعداد ناموفق
/// </summary>
public int FailedCount { get; set; }
/// <summary>
/// مبلغ کل (ریال)
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// مبلغ پرداخت شده (ریال)
/// </summary>
public long PaidAmount { get; set; }
/// <summary>
/// مبلغ در انتظار (ریال)
/// </summary>
public long PendingAmount { get; set; }
}
/// <summary>
/// خلاصه کل گزارش برداشت‌ها
/// </summary>
public class WithdrawalSummaryDto
{
/// <summary>
/// تعداد کل درخواست‌ها
/// </summary>
public int TotalRequests { get; set; }
/// <summary>
/// مبلغ کل درخواست‌ها (ریال)
/// </summary>
public long TotalAmount { get; set; }
/// <summary>
/// مبلغ کل پرداخت شده (ریال)
/// </summary>
public long TotalPaid { get; set; }
/// <summary>
/// مبلغ کل در انتظار (ریال)
/// </summary>
public long TotalPending { get; set; }
/// <summary>
/// مبلغ کل رد شده (ریال)
/// </summary>
public long TotalRejected { get; set; }
/// <summary>
/// میانگین مبلغ درخواست (ریال)
/// </summary>
public long AverageAmount { get; set; }
/// <summary>
/// تعداد کاربران منحصر به فرد
/// </summary>
public int UniqueUsers { get; set; }
/// <summary>
/// درصد موفقیت (0-100)
/// </summary>
public decimal SuccessRate { get; set; }
}

View File

@@ -0,0 +1,78 @@
using BackOffice.BFF.Commission.Protobuf;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.CommissionCQ.Queries.GetWithdrawalReports;
public class GetWithdrawalReportsQueryHandler : IRequestHandler<GetWithdrawalReportsQuery, GetWithdrawalReportsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetWithdrawalReportsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetWithdrawalReportsResponseDto> 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
}
};
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,37 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
public record CreateDiscountCategoryCommand : IRequest<CreateDiscountCategoryResponseDto>
{
/// <summary>نام دسته‌بندی (انگلیسی)</summary>
public string Name { get; init; }
/// <summary>عنوان دسته‌بندی (فارسی)</summary>
public string Title { get; init; }
/// <summary>توضیحات دسته‌بندی</summary>
public string Description { get; init; }
/// <summary>شناسه دسته‌بندی والد (null برای دسته اصلی)</summary>
public long? ParentCategoryId { get; init; }
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; init; }
/// <summary>وضعیت فعال/غیرفعال</summary>
public bool IsActive { get; init; }
/// <summary>فایل تصویر دسته‌بندی</summary>
public DiscountCategoryFileModel ImageFile { get; init; }
}
public class DiscountCategoryFileModel
{
/// <summary>فایل</summary>
public byte[] File { get; set; }
/// <summary>نام فایل</summary>
public string FileName { get; set; }
/// <summary>نوع فایل</summary>
public string Mime { get; set; }
}

View File

@@ -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<CreateDiscountCategoryCommand, CreateDiscountCategoryResponseDto>
{
private readonly IApplicationContractContext _context;
public CreateDiscountCategoryCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<CreateDiscountCategoryResponseDto> 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
};
}
}

View File

@@ -0,0 +1,19 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
public class CreateDiscountCategoryCommandValidator : AbstractValidator<CreateDiscountCategoryCommand>
{
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 کاراکتر باشد");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.CreateDiscountCategory;
public class CreateDiscountCategoryResponseDto
{
/// <summary>شناسه دسته‌بندی ایجاد شده</summary>
public long CategoryId { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
public record DeleteDiscountCategoryCommand : IRequest
{
/// <summary>شناسه دسته‌بندی</summary>
public long CategoryId { get; init; }
}

View File

@@ -0,0 +1,22 @@
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
public class DeleteDiscountCategoryCommandHandler : IRequestHandler<DeleteDiscountCategoryCommand>
{
private readonly IApplicationContractContext _context;
public DeleteDiscountCategoryCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteDiscountCategoryCommand request, CancellationToken cancellationToken)
{
await _context.DiscountCategories.DeleteDiscountCategoryAsync(
new DeleteDiscountCategoryRequest { CategoryId = request.CategoryId },
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.DeleteDiscountCategory;
public class DeleteDiscountCategoryCommandValidator : AbstractValidator<DeleteDiscountCategoryCommand>
{
public DeleteDiscountCategoryCommandValidator()
{
RuleFor(x => x.CategoryId)
.GreaterThan(0).WithMessage("شناسه دسته‌بندی نامعتبر است");
}
}

View File

@@ -0,0 +1,40 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
public record UpdateDiscountCategoryCommand : IRequest
{
/// <summary>شناسه دسته‌بندی</summary>
public long CategoryId { get; init; }
/// <summary>نام دسته‌بندی (انگلیسی)</summary>
public string Name { get; init; }
/// <summary>عنوان دسته‌بندی (فارسی)</summary>
public string Title { get; init; }
/// <summary>توضیحات دسته‌بندی</summary>
public string Description { get; init; }
/// <summary>شناسه دسته‌بندی والد (null برای دسته اصلی)</summary>
public long? ParentCategoryId { get; init; }
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; init; }
/// <summary>وضعیت فعال/غیرفعال</summary>
public bool IsActive { get; init; }
/// <summary>فایل تصویر دسته‌بندی (اختیاری - برای تغییر تصویر)</summary>
public DiscountCategoryFileModel ImageFile { get; init; }
}
public class DiscountCategoryFileModel
{
/// <summary>فایل</summary>
public byte[] File { get; set; }
/// <summary>نام فایل</summary>
public string FileName { get; set; }
/// <summary>نوع فایل</summary>
public string Mime { get; set; }
}

View File

@@ -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<UpdateDiscountCategoryCommand>
{
private readonly IApplicationContractContext _context;
public UpdateDiscountCategoryCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,26 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Commands.UpdateDiscountCategory;
public class UpdateDiscountCategoryCommandValidator : AbstractValidator<UpdateDiscountCategoryCommand>
{
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("دسته‌بندی نمی‌تواند والد خودش باشد");
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
public record GetDiscountCategoriesQuery : IRequest<GetDiscountCategoriesResponseDto>
{
/// <summary>فیلتر براساس دسته والد (null برای ریشه)</summary>
public long? ParentCategoryId { get; init; }
/// <summary>فقط دسته‌های فعال</summary>
public bool? IsActive { get; init; }
}

View File

@@ -0,0 +1,50 @@
using CMSMicroservice.Protobuf.Protos.DiscountCategory;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
public class GetDiscountCategoriesQueryHandler : IRequestHandler<GetDiscountCategoriesQuery, GetDiscountCategoriesResponseDto>
{
private readonly IApplicationContractContext _context;
public GetDiscountCategoriesQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetDiscountCategoriesResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.DiscountCategoryCQ.Queries.GetDiscountCategories;
public class GetDiscountCategoriesResponseDto
{
/// <summary>لیست درختی دسته‌بندی‌ها</summary>
public List<DiscountCategoryTreeDto> 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; }
/// <summary>زیردسته‌های این دسته‌بندی</summary>
public List<DiscountCategoryTreeDto> ChildCategories { get; set; } = new();
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public record CompleteOrderPaymentCommand : IRequest
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
/// <summary>کد تراکنش پرداخت</summary>
public string PaymentTransactionCode { get; init; }
/// <summary>مبلغ پرداخت شده</summary>
public long PaidAmount { get; init; }
}

View File

@@ -0,0 +1,27 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public class CompleteOrderPaymentCommandHandler : IRequestHandler<CompleteOrderPaymentCommand>
{
private readonly IApplicationContractContext _context;
public CompleteOrderPaymentCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.CompleteOrderPayment;
public class CompleteOrderPaymentCommandValidator : AbstractValidator<CompleteOrderPaymentCommand>
{
public CompleteOrderPaymentCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
RuleFor(x => x.PaymentTransactionCode)
.NotEmpty().WithMessage("کد تراکنش پرداخت الزامی است");
RuleFor(x => x.PaidAmount)
.GreaterThan(0).WithMessage("مبلغ پرداخت شده باید بیشتر از صفر باشد");
}
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
public record PlaceOrderCommand : IRequest<PlaceOrderResponseDto>
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه آدرس ارسال</summary>
public long AddressId { get; init; }
/// <summary>مبلغ پرداخت از موجودی تخفیف</summary>
public long DiscountBalanceAmount { get; init; }
/// <summary>مبلغ پرداخت از درگاه</summary>
public long GatewayAmount { get; init; }
}

View File

@@ -0,0 +1,34 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
public class PlaceOrderCommandHandler : IRequestHandler<PlaceOrderCommand, PlaceOrderResponseDto>
{
private readonly IApplicationContractContext _context;
public PlaceOrderCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<PlaceOrderResponseDto> 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
};
}
}

View File

@@ -0,0 +1,23 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
public class PlaceOrderCommandValidator : AbstractValidator<PlaceOrderCommand>
{
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("مجموع مبالغ پرداخت باید بیشتر از صفر باشد");
}
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.PlaceOrder;
public class PlaceOrderResponseDto
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; set; }
/// <summary>کد پیگیری سفارش</summary>
public string TrackingCode { get; set; }
/// <summary>آیا نیاز به پرداخت از درگاه دارد</summary>
public bool RequiresGatewayPayment { get; set; }
/// <summary>مبلغ قابل پرداخت از درگاه</summary>
public long GatewayPayableAmount { get; set; }
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public record UpdateOrderStatusCommand : IRequest
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
/// <summary>وضعیت جدید</summary>
public int NewStatus { get; init; }
/// <summary>یادداشت مدیر (اختیاری)</summary>
public string AdminNote { get; init; }
}

View File

@@ -0,0 +1,27 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusCommandHandler : IRequestHandler<UpdateOrderStatusCommand>
{
private readonly IApplicationContractContext _context;
public UpdateOrderStatusCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Commands.UpdateOrderStatus;
public class UpdateOrderStatusCommandValidator : AbstractValidator<UpdateOrderStatusCommand>
{
public UpdateOrderStatusCommandValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
RuleFor(x => x.NewStatus)
.InclusiveBetween(0, 6).WithMessage("وضعیت سفارش نامعتبر است");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
public record GetOrderByIdQuery : IRequest<GetOrderByIdResponseDto>
{
/// <summary>شناسه سفارش</summary>
public long OrderId { get; init; }
}

View File

@@ -0,0 +1,58 @@
using CMSMicroservice.Protobuf.Protos.DiscountOrder;
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, GetOrderByIdResponseDto>
{
private readonly IApplicationContractContext _context;
public GetOrderByIdQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetOrderByIdResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetOrderById;
public class GetOrderByIdQueryValidator : AbstractValidator<GetOrderByIdQuery>
{
public GetOrderByIdQueryValidator()
{
RuleFor(x => x.OrderId)
.GreaterThan(0).WithMessage("شناسه سفارش نامعتبر است");
}
}

View File

@@ -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<OrderItemDto> 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; }
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
public record GetUserOrdersQuery : IRequest<GetUserOrdersResponseDto>
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>فیلتر براساس وضعیت سفارش</summary>
public int? Status { get; init; }
/// <summary>شماره صفحه</summary>
public int PageNumber { get; init; } = 1;
/// <summary>تعداد در هر صفحه</summary>
public int PageSize { get; init; } = 20;
}

View File

@@ -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<GetUserOrdersQuery, GetUserOrdersResponseDto>
{
private readonly IApplicationContractContext _context;
public GetUserOrdersQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetUserOrdersResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,21 @@
namespace BackOffice.BFF.Application.DiscountOrderCQ.Queries.GetUserOrders;
public class GetUserOrdersQueryValidator : AbstractValidator<GetUserOrdersQuery>
{
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("وضعیت سفارش نامعتبر است");
}
}

View File

@@ -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<UserOrderDto> 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; }
}

View File

@@ -0,0 +1,49 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
public record CreateDiscountProductCommand : IRequest<CreateDiscountProductResponseDto>
{
/// <summary>عنوان محصول</summary>
public string Title { get; init; }
/// <summary>اطلاعات کوتاه محصول</summary>
public string ShortInformation { get; init; }
/// <summary>اطلاعات کامل محصول</summary>
public string FullInformation { get; init; }
/// <summary>قیمت محصول</summary>
public long Price { get; init; }
/// <summary>حداکثر درصد تخفیف (0-100)</summary>
public int MaxDiscountPercent { get; init; }
/// <summary>تعداد اولیه موجودی</summary>
public int InitialCount { get; init; }
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; init; }
/// <summary>وضعیت فعال/غیرفعال</summary>
public bool IsActive { get; init; }
/// <summary>آیدی دسته‌بندی‌ها</summary>
public List<long> CategoryIds { get; init; } = new();
/// <summary>فایل تصویر اصلی</summary>
public DiscountProductFileModel ImageFile { get; init; }
/// <summary>فایل تصویر کوچک</summary>
public DiscountProductFileModel ThumbnailFile { get; init; }
}
public class DiscountProductFileModel
{
/// <summary>فایل</summary>
public byte[] File { get; set; }
/// <summary>نام فایل</summary>
public string FileName { get; set; }
/// <summary>نوع فایل</summary>
public string Mime { get; set; }
}

View File

@@ -0,0 +1,70 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Google.Protobuf;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
public class CreateDiscountProductCommandHandler : IRequestHandler<CreateDiscountProductCommand, CreateDiscountProductResponseDto>
{
private readonly IApplicationContractContext _context;
public CreateDiscountProductCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<CreateDiscountProductResponseDto> 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
};
}
}

View File

@@ -0,0 +1,30 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
public class CreateDiscountProductCommandValidator : AbstractValidator<CreateDiscountProductCommand>
{
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("حداقل یک دسته‌بندی باید انتخاب شود");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.CreateDiscountProduct;
public class CreateDiscountProductResponseDto
{
/// <summary>شناسه محصول ایجاد شده</summary>
public long ProductId { get; set; }
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
public record DeleteDiscountProductCommand : IRequest
{
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
}

View File

@@ -0,0 +1,22 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
public class DeleteDiscountProductCommandHandler : IRequestHandler<DeleteDiscountProductCommand>
{
private readonly IApplicationContractContext _context;
public DeleteDiscountProductCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteDiscountProductCommand request, CancellationToken cancellationToken)
{
await _context.DiscountProducts.DeleteDiscountProductAsync(
new DeleteDiscountProductRequest { ProductId = request.ProductId },
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.DeleteDiscountProduct;
public class DeleteDiscountProductCommandValidator : AbstractValidator<DeleteDiscountProductCommand>
{
public DeleteDiscountProductCommandValidator()
{
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
}
}

View File

@@ -0,0 +1,49 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
public record UpdateDiscountProductCommand : IRequest
{
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
/// <summary>عنوان محصول</summary>
public string Title { get; init; }
/// <summary>اطلاعات کوتاه محصول</summary>
public string ShortInformation { get; init; }
/// <summary>اطلاعات کامل محصول</summary>
public string FullInformation { get; init; }
/// <summary>قیمت محصول</summary>
public long Price { get; init; }
/// <summary>حداکثر درصد تخفیف (0-100)</summary>
public int MaxDiscountPercent { get; init; }
/// <summary>ترتیب نمایش</summary>
public int SortOrder { get; init; }
/// <summary>وضعیت فعال/غیرفعال</summary>
public bool IsActive { get; init; }
/// <summary>آیدی دسته‌بندی‌ها</summary>
public List<long> CategoryIds { get; init; } = new();
/// <summary>فایل تصویر اصلی (اختیاری - برای تغییر تصویر)</summary>
public DiscountProductFileModel ImageFile { get; init; }
/// <summary>فایل تصویر کوچک (اختیاری - برای تغییر تصویر)</summary>
public DiscountProductFileModel ThumbnailFile { get; init; }
}
public class DiscountProductFileModel
{
/// <summary>فایل</summary>
public byte[] File { get; set; }
/// <summary>نام فایل</summary>
public string FileName { get; set; }
/// <summary>نوع فایل</summary>
public string Mime { get; set; }
}

View File

@@ -0,0 +1,67 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
using Google.Protobuf;
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
public class UpdateDiscountProductCommandHandler : IRequestHandler<UpdateDiscountProductCommand>
{
private readonly IApplicationContractContext _context;
public UpdateDiscountProductCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,30 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Commands.UpdateDiscountProduct;
public class UpdateDiscountProductCommandValidator : AbstractValidator<UpdateDiscountProductCommand>
{
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("حداقل یک دسته‌بندی باید انتخاب شود");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
public record GetDiscountProductByIdQuery : IRequest<GetDiscountProductByIdResponseDto>
{
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
}

View File

@@ -0,0 +1,43 @@
using CMSMicroservice.Protobuf.Protos.DiscountProduct;
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
public class GetDiscountProductByIdQueryHandler : IRequestHandler<GetDiscountProductByIdQuery, GetDiscountProductByIdResponseDto>
{
private readonly IApplicationContractContext _context;
public GetDiscountProductByIdQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetDiscountProductByIdResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProductById;
public class GetDiscountProductByIdQueryValidator : AbstractValidator<GetDiscountProductByIdQuery>
{
public GetDiscountProductByIdQueryValidator()
{
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
}
}

View File

@@ -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<CategoryInfoDto> 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; }
}

View File

@@ -0,0 +1,28 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
public record GetDiscountProductsQuery : IRequest<GetDiscountProductsResponseDto>
{
/// <summary>فیلتر براساس دسته‌بندی</summary>
public long? CategoryId { get; init; }
/// <summary>جستجو در عنوان و توضیحات</summary>
public string SearchQuery { get; init; }
/// <summary>حداقل قیمت</summary>
public long? MinPrice { get; init; }
/// <summary>حداکثر قیمت</summary>
public long? MaxPrice { get; init; }
/// <summary>فقط محصولات فعال</summary>
public bool? IsActive { get; init; }
/// <summary>فقط محصولات موجود در انبار</summary>
public bool? InStock { get; init; }
/// <summary>شماره صفحه</summary>
public int PageNumber { get; init; } = 1;
/// <summary>تعداد در هر صفحه</summary>
public int PageSize { get; init; } = 20;
}

View File

@@ -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<GetDiscountProductsQuery, GetDiscountProductsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetDiscountProductsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetDiscountProductsResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.DiscountProductCQ.Queries.GetDiscountProducts;
public class GetDiscountProductsQueryValidator : AbstractValidator<GetDiscountProductsQuery>
{
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("حداکثر قیمت باید بیشتر از حداقل قیمت باشد");
}
}

View File

@@ -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<DiscountProductDto> 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; }
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
public record AddToCartCommand : IRequest
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه محصول</summary>
public long ProductId { get; init; }
/// <summary>تعداد</summary>
public int Count { get; init; }
}

View File

@@ -0,0 +1,27 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
public class AddToCartCommandHandler : IRequestHandler<AddToCartCommand>
{
private readonly IApplicationContractContext _context;
public AddToCartCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.AddToCart;
public class AddToCartCommandValidator : AbstractValidator<AddToCartCommand>
{
public AddToCartCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("شناسه محصول نامعتبر است");
RuleFor(x => x.Count)
.GreaterThan(0).WithMessage("تعداد باید بیشتر از صفر باشد");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
public record ClearCartCommand : IRequest
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
}

View File

@@ -0,0 +1,22 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
public class ClearCartCommandHandler : IRequestHandler<ClearCartCommand>
{
private readonly IApplicationContractContext _context;
public ClearCartCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(ClearCartCommand request, CancellationToken cancellationToken)
{
await _context.DiscountShoppingCarts.ClearCartAsync(
new ClearCartRequest { UserId = request.UserId },
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.ClearCart;
public class ClearCartCommandValidator : AbstractValidator<ClearCartCommand>
{
public ClearCartCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
public record RemoveFromCartCommand : IRequest
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه آیتم سبد خرید</summary>
public long CartItemId { get; init; }
}

View File

@@ -0,0 +1,26 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
public class RemoveFromCartCommandHandler : IRequestHandler<RemoveFromCartCommand>
{
private readonly IApplicationContractContext _context;
public RemoveFromCartCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(RemoveFromCartCommand request, CancellationToken cancellationToken)
{
await _context.DiscountShoppingCarts.RemoveFromCartAsync(
new RemoveFromCartRequest
{
UserId = request.UserId,
CartItemId = request.CartItemId
},
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.RemoveFromCart;
public class RemoveFromCartCommandValidator : AbstractValidator<RemoveFromCartCommand>
{
public RemoveFromCartCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
RuleFor(x => x.CartItemId)
.GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
}
}

View File

@@ -0,0 +1,13 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
public record UpdateCartItemCountCommand : IRequest
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
/// <summary>شناسه آیتم سبد خرید</summary>
public long CartItemId { get; init; }
/// <summary>تعداد جدید</summary>
public int NewCount { get; init; }
}

View File

@@ -0,0 +1,27 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
public class UpdateCartItemCountCommandHandler : IRequestHandler<UpdateCartItemCountCommand>
{
private readonly IApplicationContractContext _context;
public UpdateCartItemCountCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,16 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Commands.UpdateCartItemCount;
public class UpdateCartItemCountCommandValidator : AbstractValidator<UpdateCartItemCountCommand>
{
public UpdateCartItemCountCommandValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
RuleFor(x => x.CartItemId)
.GreaterThan(0).WithMessage("شناسه آیتم سبد خرید نامعتبر است");
RuleFor(x => x.NewCount)
.GreaterThan(0).WithMessage("تعداد جدید باید بیشتر از صفر باشد");
}
}

View File

@@ -0,0 +1,7 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
public record GetUserCartQuery : IRequest<GetUserCartResponseDto>
{
/// <summary>شناسه کاربر</summary>
public long UserId { get; init; }
}

View File

@@ -0,0 +1,41 @@
using CMSMicroservice.Protobuf.Protos.DiscountShoppingCart;
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
public class GetUserCartQueryHandler : IRequestHandler<GetUserCartQuery, GetUserCartResponseDto>
{
private readonly IApplicationContractContext _context;
public GetUserCartQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetUserCartResponseDto> 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()
};
}
}

View File

@@ -0,0 +1,10 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
public class GetUserCartQueryValidator : AbstractValidator<GetUserCartQuery>
{
public GetUserCartQueryValidator()
{
RuleFor(x => x.UserId)
.GreaterThan(0).WithMessage("شناسه کاربر نامعتبر است");
}
}

View File

@@ -0,0 +1,24 @@
namespace BackOffice.BFF.Application.DiscountShoppingCartCQ.Queries.GetUserCart;
public class GetUserCartResponseDto
{
public long UserId { get; set; }
public List<CartItemDto> 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; }
}

View File

@@ -0,0 +1,9 @@
using CMSMicroservice.Protobuf.Protos.ProductTag;
namespace BackOffice.BFF.Application.ProductTagCQ.Commands.AssignTagToProduct;
public record AssignTagToProductCommand : IRequest<long>
{
public long ProductId { get; init; }
public long TagId { get; init; }
}

View File

@@ -0,0 +1,26 @@
using CMSMicroservice.Protobuf.Protos.ProductTag;
namespace BackOffice.BFF.Application.ProductTagCQ.Commands.AssignTagToProduct;
public class AssignTagToProductCommandHandler : IRequestHandler<AssignTagToProductCommand, long>
{
private readonly IApplicationContractContext _context;
public AssignTagToProductCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<long> Handle(AssignTagToProductCommand request, CancellationToken cancellationToken)
{
var response = await _context.ProductTags.CreateNewProductTagAsync(
new CreateNewProductTagRequest
{
ProductId = request.ProductId,
TagId = request.TagId
},
cancellationToken: cancellationToken);
return response.Id;
}
}

View File

@@ -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<AddProductImageComm
ImageThumbnailPath = thumbFileInfo.File
}, cancellationToken: cancellationToken);
var createGalleryResponse = await _context.ProductGallerys.CreateNewProductGallerysAsync(new CreateNewProductGallerysRequest
var createGalleryResponse = await _context.ProductGalleries.CreateNewProductGalleriesAsync(new CreateNewProductGalleriesRequest
{
ProductId = request.ProductId,
ProductImageId = createImageResponse.Id

View File

@@ -1,5 +1,5 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
namespace BackOffice.BFF.Application.ProductsCQ.Commands.RemoveProductImage;
@@ -14,7 +14,7 @@ public class RemoveProductImageCommandHandler : IRequestHandler<RemoveProductIma
public async Task<Unit> Handle(RemoveProductImageCommand request, CancellationToken cancellationToken)
{
await _context.ProductGallerys.DeleteProductGallerysAsync(new DeleteProductGallerysRequest
await _context.ProductGalleries.DeleteProductGalleriesAsync(new DeleteProductGalleriesRequest
{
Id = request.ProductGalleryId
}, cancellationToken: cancellationToken);

View File

@@ -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<UpdateCatego
var targetIds = request.ProductIds?.ToHashSet() ?? new HashSet<long>();
// 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<UpdateCatego
}
};
var existingResponse = await _context.ProductCategories.GetAllPruductCategoryByFilterAsync(existingRequest, cancellationToken: cancellationToken);
var existingResponse = await _context.ProductCategories.GetAllProductCategoryByFilterAsync(existingRequest, cancellationToken: cancellationToken);
var existing = existingResponse.Models ?? new();
var existingIds = existing.Select(x => x.ProductId).ToHashSet();
@@ -40,19 +40,19 @@ public class UpdateCategoryProductsCommandHandler : IRequestHandler<UpdateCatego
foreach (var productId in toAdd)
{
var createRequest = new CMSPruductCategory.CreateNewPruductCategoryRequest
var createRequest = new CMSProductCategory.CreateNewProductCategoryRequest
{
ProductId = productId,
CategoryId = request.CategoryId
};
await _context.ProductCategories.CreateNewPruductCategoryAsync(createRequest, cancellationToken: cancellationToken);
await _context.ProductCategories.CreateNewProductCategoryAsync(createRequest, cancellationToken: cancellationToken);
}
foreach (var rel in toRemove)
{
await _context.ProductCategories.DeletePruductCategoryAsync(
new CMSPruductCategory.DeletePruductCategoryRequest { Id = rel.Id },
await _context.ProductCategories.DeleteProductCategoryAsync(
new CMSProductCategory.DeleteProductCategoryRequest { Id = rel.Id },
cancellationToken: cancellationToken);
}

View File

@@ -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.UpdateProductCategories;
@@ -17,9 +17,9 @@ public class UpdateProductCategoriesCommandHandler : IRequestHandler<UpdateProdu
var targetIds = request.CategoryIds?.ToHashSet() ?? new HashSet<long>();
// 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<UpdateProdu
}
};
var existingResponse = await _context.ProductCategories.GetAllPruductCategoryByFilterAsync(existingRequest, cancellationToken: cancellationToken);
var existingResponse = await _context.ProductCategories.GetAllProductCategoryByFilterAsync(existingRequest, cancellationToken: cancellationToken);
var existing = existingResponse.Models ?? new();
var existingIds = existing.Select(x => x.CategoryId).ToHashSet();
@@ -41,19 +41,19 @@ public class UpdateProductCategoriesCommandHandler : IRequestHandler<UpdateProdu
foreach (var categoryId in toAdd)
{
var createRequest = new CMSPruductCategory.CreateNewPruductCategoryRequest
var createRequest = new CMSProductCategory.CreateNewProductCategoryRequest
{
ProductId = request.ProductId,
CategoryId = categoryId
};
await _context.ProductCategories.CreateNewPruductCategoryAsync(createRequest, cancellationToken: cancellationToken);
await _context.ProductCategories.CreateNewProductCategoryAsync(createRequest, cancellationToken: cancellationToken);
}
foreach (var rel in toRemove)
{
await _context.ProductCategories.DeletePruductCategoryAsync(
new CMSPruductCategory.DeletePruductCategoryRequest { Id = rel.Id },
await _context.ProductCategories.DeleteProductCategoryAsync(
new CMSProductCategory.DeleteProductCategoryRequest { Id = rel.Id },
cancellationToken: cancellationToken);
}

View File

@@ -1,6 +1,6 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSCategory = CMSMicroservice.Protobuf.Protos.Category;
using CMSPruductCategory = CMSMicroservice.Protobuf.Protos.PruductCategory;
using CMSProductCategory = CMSMicroservice.Protobuf.Protos.ProductCategory;
namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetCategories;
@@ -30,9 +30,9 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Get
var categories = categoriesResponse.Models ?? new();
// Load existing product-category relations for this product
var productCategoryRequest = new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
var productCategoryRequest = new CMSProductCategory.GetAllProductCategoryByFilterRequest
{
Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
Filter = new CMSProductCategory.GetAllProductCategoryByFilterFilter
{
ProductId = request.ProductId
},
@@ -43,7 +43,7 @@ public class GetCategoriesQueryHandler : IRequestHandler<GetCategoriesQuery, Get
}
};
var productCategoriesResponse = await _context.ProductCategories.GetAllPruductCategoryByFilterAsync(productCategoryRequest, cancellationToken: cancellationToken);
var productCategoriesResponse = await _context.ProductCategories.GetAllProductCategoryByFilterAsync(productCategoryRequest, cancellationToken: cancellationToken);
var productCategories = productCategoriesResponse.Models ?? new();
var selectedIds = productCategories

View File

@@ -1,5 +1,5 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.ProductGallerys;
using CMSMicroservice.Protobuf.Protos.ProductGalleries;
using CMSMicroservice.Protobuf.Protos.ProductImages;
namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetProductGallery;
@@ -15,12 +15,12 @@ public class GetProductGalleryQueryHandler : IRequestHandler<GetProductGalleryQu
public async Task<GetProductGalleryResponseDto> 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();

View File

@@ -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<GetProductsFor
var products = productsResponse.Models ?? new();
// Load links for this category
var linksRequest = new CMSPruductCategory.GetAllPruductCategoryByFilterRequest
var linksRequest = new CMSProductCategory.GetAllProductCategoryByFilterRequest
{
Filter = new CMSPruductCategory.GetAllPruductCategoryByFilterFilter
Filter = new CMSProductCategory.GetAllProductCategoryByFilterFilter
{
CategoryId = request.CategoryId
},
@@ -43,7 +43,7 @@ public class GetProductsForCategoryQueryHandler : IRequestHandler<GetProductsFor
}
};
var linksResponse = await _context.ProductCategories.GetAllPruductCategoryByFilterAsync(linksRequest, cancellationToken: cancellationToken);
var linksResponse = await _context.ProductCategories.GetAllProductCategoryByFilterAsync(linksRequest, cancellationToken: cancellationToken);
var links = linksResponse.Models ?? new();
var selectedProductIds = links.Select(l => l.ProductId).ToHashSet();

View File

@@ -0,0 +1,12 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Commands.CreateTag;
public record CreateTagCommand : IRequest<long>
{
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; }
}

View File

@@ -0,0 +1,29 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Commands.CreateTag;
public class CreateTagCommandHandler : IRequestHandler<CreateTagCommand, long>
{
private readonly IApplicationContractContext _context;
public CreateTagCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<long> 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;
}
}

View File

@@ -0,0 +1,6 @@
namespace BackOffice.BFF.Application.TagCQ.Commands.DeleteTag;
public record DeleteTagCommand : IRequest
{
public long Id { get; init; }
}

View File

@@ -0,0 +1,25 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Commands.DeleteTag;
public class DeleteTagCommandHandler : IRequestHandler<DeleteTagCommand>
{
private readonly IApplicationContractContext _context;
public DeleteTagCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> Handle(DeleteTagCommand request, CancellationToken cancellationToken)
{
await _context.Tags.DeleteTagAsync(
new DeleteTagRequest
{
Id = request.Id
},
cancellationToken: cancellationToken);
return Unit.Value;
}
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,30 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Commands.UpdateTag;
public class UpdateTagCommandHandler : IRequestHandler<UpdateTagCommand>
{
private readonly IApplicationContractContext _context;
public UpdateTagCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<Unit> 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;
}
}

View File

@@ -0,0 +1,11 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Queries.GetAllTags;
public record GetAllTagsQuery : IRequest<GetAllTagByFilterResponse>
{
public int PageNumber { get; init; } = 1;
public int PageSize { get; init; } = 10;
public string? SearchTerm { get; init; }
public bool? IsActive { get; init; }
}

View File

@@ -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<GetAllTagsQuery, GetAllTagByFilterResponse>
{
private readonly IApplicationContractContext _context;
public GetAllTagsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllTagByFilterResponse> 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;
}
}

View File

@@ -0,0 +1,8 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Queries.GetProductsByTag;
public record GetProductsByTagQuery : IRequest<GetProductsByTagResponse>
{
public long TagId { get; init; }
}

View File

@@ -0,0 +1,22 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Queries.GetProductsByTag;
public class GetProductsByTagQueryHandler : IRequestHandler<GetProductsByTagQuery, GetProductsByTagResponse>
{
private readonly IApplicationContractContext _context;
public GetProductsByTagQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetProductsByTagResponse> Handle(GetProductsByTagQuery request, CancellationToken cancellationToken)
{
var response = await _context.Tags.GetProductsByTagAsync(
new GetProductsByTagRequest { TagId = request.TagId },
cancellationToken: cancellationToken);
return response;
}
}

View File

@@ -0,0 +1,8 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Queries.GetTag;
public record GetTagQuery : IRequest<GetTagResponse>
{
public long Id { get; init; }
}

View File

@@ -0,0 +1,22 @@
using CMSMicroservice.Protobuf.Protos.Tag;
namespace BackOffice.BFF.Application.TagCQ.Queries.GetTag;
public class GetTagQueryHandler : IRequestHandler<GetTagQuery, GetTagResponse>
{
private readonly IApplicationContractContext _context;
public GetTagQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetTagResponse> Handle(GetTagQuery request, CancellationToken cancellationToken)
{
var response = await _context.Tags.GetTagAsync(
new GetTagRequest { Id = request.Id },
cancellationToken: cancellationToken);
return response;
}
}

View File

@@ -7,11 +7,15 @@
<ItemGroup>
<PackageReference Include="Afrino.FMSMicroservice.Protobuf" Version="0.0.122" />
<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.141" />
<!--<PackageReference Include="Foursat.CMSMicroservice.Protobuf" Version="0.0.141" />-->
<PackageReference Include="Google.Protobuf" Version="3.23.3" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.54.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\..\FourSat\CMS\src\CMSMicroservice.Protobuf\CMSMicroservice.Protobuf.csproj" />
</ItemGroup>
</Project>

View File

@@ -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<PackageContract.PackageContractClient>();
public ProductsContract.ProductsContractClient Products => GetService<ProductsContract.ProductsContractClient>();
public ProductImagesContract.ProductImagesContractClient ProductImages => GetService<ProductImagesContract.ProductImagesContractClient>();
public ProductGallerysContract.ProductGallerysContractClient ProductGallerys => GetService<ProductGallerysContract.ProductGallerysContractClient>();
public ProductGalleriesContract.ProductGalleriesContractClient ProductGalleries => GetService<ProductGalleriesContract.ProductGalleriesContractClient>();
public RoleContract.RoleContractClient Roles => GetService<RoleContract.RoleContractClient>();
public CategoryContract.CategoryContractClient Categories => GetService<CategoryContract.CategoryContractClient>();
public PruductCategoryContract.PruductCategoryContractClient ProductCategories => GetService<PruductCategoryContract.PruductCategoryContractClient>();
public ProductCategoryContract.ProductCategoryContractClient ProductCategories => GetService<ProductCategoryContract.ProductCategoryContractClient>();
public UserAddressContract.UserAddressContractClient UserAddress => GetService<UserAddressContract.UserAddressContractClient>();
public UserContract.UserContractClient Users => GetService<UserContract.UserContractClient>();
public UserOrderContract.UserOrderContractClient UserOrders => GetService<UserOrderContract.UserOrderContractClient>();
@@ -64,5 +70,15 @@ public class ApplicationContractContext : IApplicationContractContext
public NetworkMembershipContract.NetworkMembershipContractClient NetworkMemberships => GetService<NetworkMembershipContract.NetworkMembershipContractClient>();
public ClubMembershipContract.ClubMembershipContractClient ClubMemberships => GetService<ClubMembershipContract.ClubMembershipContractClient>();
public ConfigurationContract.ConfigurationContractClient Configurations => GetService<ConfigurationContract.ConfigurationContractClient>();
// Discount Shop System (Phase 9)
public DiscountProductContract.DiscountProductContractClient DiscountProducts => GetService<DiscountProductContract.DiscountProductContractClient>();
public DiscountCategoryContract.DiscountCategoryContractClient DiscountCategories => GetService<DiscountCategoryContract.DiscountCategoryContractClient>();
public DiscountShoppingCartContract.DiscountShoppingCartContractClient DiscountShoppingCarts => GetService<DiscountShoppingCartContract.DiscountShoppingCartContractClient>();
public DiscountOrderContract.DiscountOrderContractClient DiscountOrders => GetService<DiscountOrderContract.DiscountOrderContractClient>();
// Tag Management System
public TagContract.TagContractClient Tags => GetService<TagContract.TagContractClient>();
public ProductTagContract.ProductTagContractClient ProductTags => GetService<ProductTagContract.ProductTagContractClient>();
#endregion
}

View File

@@ -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)
}