feat: Add bulk update functionalities for product prices and stock, along with low stock query support

This commit is contained in:
masoodafar-web
2025-12-04 02:56:07 +03:30
parent 4f400eabc5
commit 256b41fcb5
10 changed files with 431 additions and 225 deletions

View File

@@ -0,0 +1,28 @@
namespace BackOffice.BFF.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
public record BulkUpdateProductPricesCommand : IRequest<BulkUpdateProductPricesResponseDto>
{
public List<ProductPriceUpdateDto> Products { get; init; } = new();
}
public class ProductPriceUpdateDto
{
public long ProductId { get; set; }
public long NewPrice { get; set; }
public int? NewDiscount { get; set; }
public int? NewClubDiscountPercent { get; set; }
}
public class BulkUpdateProductPricesResponseDto
{
public int Total { get; set; }
public int Succeeded { get; set; }
public int Failed { get; set; }
public List<BulkOperationError> Errors { get; set; } = new();
}
public class BulkOperationError
{
public long ProductId { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,34 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.Products;
namespace BackOffice.BFF.Application.ProductsCQ.Commands.BulkUpdateProductPrices;
public class BulkUpdateProductPricesCommandHandler : IRequestHandler<BulkUpdateProductPricesCommand, BulkUpdateProductPricesResponseDto>
{
private readonly IApplicationContractContext _context;
public BulkUpdateProductPricesCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<BulkUpdateProductPricesResponseDto> Handle(BulkUpdateProductPricesCommand request, CancellationToken cancellationToken)
{
var protoRequest = new BulkUpdateProductPricesRequest();
foreach (var product in request.Products)
{
protoRequest.Products.Add(new ProductPriceUpdate
{
ProductId = product.ProductId,
NewPrice = product.NewPrice,
NewDiscount = product.NewDiscount,
NewClubDiscountPercent = product.NewClubDiscountPercent
});
}
var response = await _context.Products.BulkUpdateProductPricesAsync(protoRequest, cancellationToken: cancellationToken);
return response.Adapt<BulkUpdateProductPricesResponseDto>();
}
}

View File

@@ -0,0 +1,34 @@
namespace BackOffice.BFF.Application.ProductsCQ.Commands.BulkUpdateProductStock;
public record BulkUpdateProductStockCommand : IRequest<BulkUpdateProductStockResponseDto>
{
public List<ProductStockUpdateDto> Products { get; init; } = new();
public StockUpdateType UpdateType { get; init; }
}
public class ProductStockUpdateDto
{
public long ProductId { get; set; }
public int Quantity { get; set; }
}
public enum StockUpdateType
{
Set = 0,
Add = 1,
Subtract = 2
}
public class BulkUpdateProductStockResponseDto
{
public int Total { get; set; }
public int Succeeded { get; set; }
public int Failed { get; set; }
public List<BulkOperationError> Errors { get; set; } = new();
}
public class BulkOperationError
{
public long ProductId { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,35 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.Products;
namespace BackOffice.BFF.Application.ProductsCQ.Commands.BulkUpdateProductStock;
public class BulkUpdateProductStockCommandHandler : IRequestHandler<BulkUpdateProductStockCommand, BulkUpdateProductStockResponseDto>
{
private readonly IApplicationContractContext _context;
public BulkUpdateProductStockCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<BulkUpdateProductStockResponseDto> Handle(BulkUpdateProductStockCommand request, CancellationToken cancellationToken)
{
var protoRequest = new BulkUpdateProductStockRequest
{
UpdateType = (CMSMicroservice.Protobuf.Protos.Products.StockUpdateType)request.UpdateType
};
foreach (var product in request.Products)
{
protoRequest.Products.Add(new ProductStockUpdate
{
ProductId = product.ProductId,
Quantity = product.Quantity
});
}
var response = await _context.Products.BulkUpdateProductStockAsync(protoRequest, cancellationToken: cancellationToken);
return response.Adapt<BulkUpdateProductStockResponseDto>();
}
}

View File

@@ -0,0 +1,22 @@
namespace BackOffice.BFF.Application.ProductsCQ.Commands.ToggleProductStatus;
public record ToggleProductStatusCommand : IRequest<ToggleProductStatusResponseDto>
{
public List<long> ProductIds { get; init; } = new();
public bool Enable { get; init; }
public int DefaultStock { get; init; } = 1;
}
public class ToggleProductStatusResponseDto
{
public int Total { get; set; }
public int Succeeded { get; set; }
public int Failed { get; set; }
public List<BulkOperationError> Errors { get; set; } = new();
}
public class BulkOperationError
{
public long ProductId { get; set; }
public string ErrorMessage { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,29 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.Products;
namespace BackOffice.BFF.Application.ProductsCQ.Commands.ToggleProductStatus;
public class ToggleProductStatusCommandHandler : IRequestHandler<ToggleProductStatusCommand, ToggleProductStatusResponseDto>
{
private readonly IApplicationContractContext _context;
public ToggleProductStatusCommandHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<ToggleProductStatusResponseDto> Handle(ToggleProductStatusCommand request, CancellationToken cancellationToken)
{
var protoRequest = new ToggleProductStatusRequest
{
Enable = request.Enable,
DefaultStock = request.DefaultStock
};
protoRequest.ProductIds.AddRange(request.ProductIds);
var response = await _context.Products.ToggleProductStatusAsync(protoRequest, cancellationToken: cancellationToken);
return response.Adapt<ToggleProductStatusResponseDto>();
}
}

View File

@@ -0,0 +1,26 @@
using BackOffice.BFF.Application.Common.Models;
namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetLowStockProducts;
public record GetLowStockProductsQuery : IRequest<GetLowStockProductsResponseDto>
{
public int Threshold { get; init; } = 10;
public int PageIndex { get; init; } = 1;
public int PageSize { get; init; } = 20;
public bool? IsClubExclusive { get; init; }
}
public class GetLowStockProductsResponseDto
{
public MetaData MetaData { get; set; } = new();
public List<LowStockProductDto> Products { get; set; } = new();
}
public class LowStockProductDto
{
public long Id { get; set; }
public string Title { get; set; } = string.Empty;
public int RemainingCount { get; set; }
public long Price { get; set; }
public bool IsClubExclusive { get; set; }
}

View File

@@ -0,0 +1,33 @@
using BackOffice.BFF.Application.Common.Interfaces;
using CMSMicroservice.Protobuf.Protos.Products;
namespace BackOffice.BFF.Application.ProductsCQ.Queries.GetLowStockProducts;
public class GetLowStockProductsQueryHandler : IRequestHandler<GetLowStockProductsQuery, GetLowStockProductsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetLowStockProductsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetLowStockProductsResponseDto> Handle(GetLowStockProductsQuery request, CancellationToken cancellationToken)
{
var protoRequest = new GetLowStockProductsRequest
{
Threshold = request.Threshold,
PageIndex = request.PageIndex,
PageSize = request.PageSize
};
if (request.IsClubExclusive.HasValue)
{
protoRequest.IsClubExclusive = request.IsClubExclusive.Value;
}
var response = await _context.Products.GetLowStockProductsAsync(protoRequest, cancellationToken: cancellationToken);
return response.Adapt<GetLowStockProductsResponseDto>();
}
}

View File

@@ -24,7 +24,11 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Protobuf Include="Protos\products.proto" ProtoRoot="Protos\" GrpcServices="Both" /> <Protobuf Include="Protos\products.proto" ProtoRoot="Protos\" GrpcServices="Both" AdditionalImportDirs="..\BackOffice.BFF.Common.Protobuf\Protos"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BackOffice.BFF.Common.Protobuf\BackOffice.BFF.Common.Protobuf.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="PushToFourSat" AfterTargets="Pack"> <Target Name="PushToFourSat" AfterTargets="Pack">

View File

@@ -2,314 +2,275 @@ syntax = "proto3";
package products; package products;
import "public_messages.proto";
import "google/protobuf/empty.proto"; import "google/protobuf/empty.proto";
import "google/protobuf/wrappers.proto"; import "google/protobuf/wrappers.proto";
import "google/protobuf/duration.proto"; import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto"; import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto"; import "google/api/annotations.proto";
option csharp_namespace = "BackOffice.BFF.Products.Protobuf.Protos.Products"; option csharp_namespace = "CMSMicroservice.Protobuf.Protos.Products";
service ProductsContract service ProductsContract
{ {
rpc CreateNewProducts(CreateNewProductsRequest) returns (CreateNewProductsResponse){ rpc CreateNewProducts(CreateNewProductsRequest) returns (CreateNewProductsResponse){
option (google.api.http) = { option (google.api.http) = {
post: "/CreateNewProducts" post: "/CreateNewProducts"
body: "*" body: "*"
};
}; };
}; rpc UpdateProducts(UpdateProductsRequest) returns (google.protobuf.Empty){
rpc UpdateProducts(UpdateProductsRequest) returns (google.protobuf.Empty){ option (google.api.http) = {
option (google.api.http) = { put: "/UpdateProducts"
put: "/UpdateProducts" body: "*"
body: "*" };
}; };
}; rpc DeleteProducts(DeleteProductsRequest) returns (google.protobuf.Empty){
rpc DeleteProducts(DeleteProductsRequest) returns (google.protobuf.Empty){ option (google.api.http) = {
option (google.api.http) = { delete: "/DeleteProducts"
delete: "/DeleteProducts" body: "*"
body: "*" };
}; };
}; rpc GetProducts(GetProductsRequest) returns (GetProductsResponse){
rpc GetProducts(GetProductsRequest) returns (GetProductsResponse){ option (google.api.http) = {
option (google.api.http) = { get: "/GetProducts"
get: "/GetProducts"
};
}; };
}; rpc GetAllProductsByFilter(GetAllProductsByFilterRequest) returns (GetAllProductsByFilterResponse){
rpc GetAllProductsByFilter(GetAllProductsByFilterRequest) returns (GetAllProductsByFilterResponse){ option (google.api.http) = {
option (google.api.http) = { get: "/GetAllProductsByFilter"
get: "/GetAllProductsByFilter"
};
}; };
}; rpc BulkUpdateProductPrices(BulkUpdateProductPricesRequest) returns (BulkUpdateProductPricesResponse){
option (google.api.http) = {
rpc AddProductImage(AddProductImageRequest) returns (AddProductImageResponse){ post: "/BulkUpdateProductPrices"
option (google.api.http) = { body: "*"
post: "/AddProductImage" };
body: "*"
}; };
}; rpc BulkUpdateProductStock(BulkUpdateProductStockRequest) returns (BulkUpdateProductStockResponse){
option (google.api.http) = {
rpc GetProductGallery(GetProductGalleryRequest) returns (GetProductGalleryResponse){ post: "/BulkUpdateProductStock"
option (google.api.http) = { body: "*"
get: "/GetProductGallery" };
}; };
}; rpc GetLowStockProducts(GetLowStockProductsRequest) returns (GetLowStockProductsResponse){
option (google.api.http) = {
rpc RemoveProductImage(RemoveProductImageRequest) returns (google.protobuf.Empty){ get: "/GetLowStockProducts"
option (google.api.http) = { };
delete: "/RemoveProductImage"
body: "*"
}; };
}; rpc ToggleProductStatus(ToggleProductStatusRequest) returns (ToggleProductStatusResponse){
option (google.api.http) = {
rpc GetCategories(GetCategoriesRequest) returns (GetCategoriesResponse){ post: "/ToggleProductStatus"
option (google.api.http) = { body: "*"
get: "/GetCategories" };
}; };
};
rpc UpdateProductCategories(UpdateProductCategoriesRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
post: "/UpdateProductCategories"
body: "*"
};
};
rpc GetProductsForCategory(GetProductsForCategoryRequest) returns (GetProductsForCategoryResponse){
option (google.api.http) = {
get: "/GetProductsForCategory"
};
};
rpc UpdateCategoryProducts(UpdateCategoryProductsRequest) returns (google.protobuf.Empty){
option (google.api.http) = {
post: "/UpdateCategoryProducts"
body: "*"
};
};
} }
message ImageFileModel
{
bytes file = 1;
string file_name = 2;
string mime = 3;
}
message CreateNewProductsRequest message CreateNewProductsRequest
{ {
string title = 1; string title = 1;
string description = 2; string description = 2;
string short_infomation = 3; string short_infomation = 3;
string full_information = 4; string full_information = 4;
int64 price = 5; int64 price = 5;
int32 discount = 6; int32 discount = 6;
int32 rate = 7; int32 rate = 7;
ImageFileModel image_file = 8; string image_path = 8;
ImageFileModel thumbnail_file = 9; string thumbnail_path = 9;
// لیست شناسه دسته‌بندی‌های محصول int32 sale_count = 10;
repeated int64 category_ids = 10; int32 view_count = 11;
int32 remaining_count = 12;
// لیست شناسه دسته‌بندی‌های محصول
repeated int64 category_ids = 13;
} }
message CreateNewProductsResponse message CreateNewProductsResponse
{ {
int64 id = 1; int64 id = 1;
} }
message UpdateProductsRequest message UpdateProductsRequest
{ {
int64 id = 1; int64 id = 1;
string title = 2; string title = 2;
string description = 3; string description = 3;
string short_infomation = 4; string short_infomation = 4;
string full_information = 5; string full_information = 5;
int64 price = 6; int64 price = 6;
int32 discount = 7; int32 discount = 7;
int32 rate = 8; int32 rate = 8;
string image_path = 9; string image_path = 9;
string thumbnail_path = 10; string thumbnail_path = 10;
ImageFileModel image_file = 11; int32 sale_count = 11;
ImageFileModel thumbnail_file = 12; int32 view_count = 12;
// لیست شناسه دسته‌بندی‌های محصول int32 remaining_count = 13;
repeated int64 category_ids = 13; // لیست شناسه دسته‌بندی‌های محصول
repeated int64 category_ids = 14;
} }
message DeleteProductsRequest message DeleteProductsRequest
{ {
int64 id = 1; int64 id = 1;
} }
message GetProductsRequest message GetProductsRequest
{ {
int64 id = 1; int64 id = 1;
} }
message GetProductsResponse message GetProductsResponse
{ {
int64 id = 1; int64 id = 1;
string title = 2; string title = 2;
string description = 3; string description = 3;
string short_infomation = 4; string short_infomation = 4;
string full_information = 5; string full_information = 5;
int64 price = 6; int64 price = 6;
int32 discount = 7; int32 discount = 7;
int32 rate = 8; int32 rate = 8;
string image_path = 9; string image_path = 9;
string thumbnail_path = 10; string thumbnail_path = 10;
int32 sale_count = 11; int32 sale_count = 11;
int32 view_count = 12; int32 view_count = 12;
int32 remaining_count = 13; int32 remaining_count = 13;
// لیست شناسه دسته‌بندی‌های محصول // لیست شناسه دسته‌بندی‌های محصول
repeated int64 category_ids = 14; repeated int64 category_ids = 14;
} }
message GetAllProductsByFilterRequest message GetAllProductsByFilterRequest
{ {
PaginationState pagination_state = 1; messages.PaginationState pagination_state = 1;
google.protobuf.StringValue sort_by = 2; google.protobuf.StringValue sort_by = 2;
GetAllProductsByFilterFilter filter = 3; GetAllProductsByFilterFilter filter = 3;
} }
message GetAllProductsByFilterFilter message GetAllProductsByFilterFilter
{ {
google.protobuf.Int64Value id = 1; google.protobuf.Int64Value id = 1;
google.protobuf.StringValue title = 2; google.protobuf.StringValue title = 2;
google.protobuf.StringValue description = 3; google.protobuf.StringValue description = 3;
google.protobuf.StringValue short_infomation = 4; google.protobuf.StringValue short_infomation = 4;
google.protobuf.StringValue full_information = 5; google.protobuf.StringValue full_information = 5;
google.protobuf.Int64Value price = 6; google.protobuf.Int64Value price = 6;
google.protobuf.Int32Value discount = 7; google.protobuf.Int32Value discount = 7;
google.protobuf.Int32Value rate = 8; google.protobuf.Int32Value rate = 8;
google.protobuf.StringValue image_path = 9;
google.protobuf.StringValue thumbnail_path = 10;
google.protobuf.Int32Value sale_count = 11;
google.protobuf.Int32Value view_count = 12;
google.protobuf.Int32Value remaining_count = 13;
google.protobuf.Int64Value category_id = 14;
} }
message GetAllProductsByFilterResponse message GetAllProductsByFilterResponse
{ {
MetaData meta_data = 1; messages.MetaData meta_data = 1;
repeated GetAllProductsByFilterResponseModel models = 2; repeated GetAllProductsByFilterResponseModel models = 2;
} }
message GetAllProductsByFilterResponseModel message GetAllProductsByFilterResponseModel
{ {
int64 id = 1; int64 id = 1;
string title = 2; string title = 2;
string description = 3; string description = 3;
string short_infomation = 4; string short_infomation = 4;
string full_information = 5; string full_information = 5;
int64 price = 6; int64 price = 6;
int32 discount = 7; int32 discount = 7;
int32 rate = 8; int32 rate = 8;
string image_path = 9; string image_path = 9;
string thumbnail_path = 10; string thumbnail_path = 10;
int32 sale_count = 11; int32 sale_count = 11;
int32 view_count = 12; int32 view_count = 12;
int32 remaining_count = 13; int32 remaining_count = 13;
// لیست شناسه دسته‌بندی‌های محصول // لیست شناسه دسته‌بندی‌های محصول
repeated int64 category_ids = 14; repeated int64 category_ids = 14;
} }
message PaginationState // Bulk Update Product Prices
message BulkUpdateProductPricesRequest
{ {
int32 page_number = 1; repeated ProductPriceUpdate products = 1;
int32 page_size = 2;
} }
message MetaData message ProductPriceUpdate
{ {
int64 current_page = 1; int64 product_id = 1;
int64 total_page = 2; int64 new_price = 2;
int64 page_size = 3; google.protobuf.Int32Value new_discount = 3;
int64 total_count = 4; google.protobuf.Int32Value new_club_discount_percent = 4;
bool has_previous = 5;
bool has_next = 6;
} }
message DecimalValue message BulkUpdateProductPricesResponse
{ {
int64 units = 1; int32 total = 1;
sfixed32 nanos = 2; int32 succeeded = 2;
int32 failed = 3;
repeated BulkOperationError errors = 4;
} }
message AddProductImageRequest message BulkOperationError
{ {
int64 product_id = 1; int64 product_id = 1;
string title = 2; string error_message = 2;
ImageFileModel image_file = 3;
} }
message AddProductImageResponse // Bulk Update Product Stock
message BulkUpdateProductStockRequest
{ {
int64 product_gallery_id = 1; repeated ProductStockUpdate products = 1;
int64 product_image_id = 2; StockUpdateType update_type = 2;
string title = 3;
string image_path = 4;
string image_thumbnail_path = 5;
} }
message GetProductGalleryRequest message ProductStockUpdate
{ {
int64 product_id = 1; int64 product_id = 1;
int32 quantity = 2;
} }
message ProductGalleryItem enum StockUpdateType
{ {
int64 product_gallery_id = 1; SET = 0;
int64 product_image_id = 2; ADD = 1;
string title = 3; SUBTRACT = 2;
string image_path = 4;
string image_thumbnail_path = 5;
} }
message GetProductGalleryResponse message BulkUpdateProductStockResponse
{ {
repeated ProductGalleryItem items = 1; int32 total = 1;
int32 succeeded = 2;
int32 failed = 3;
repeated BulkOperationError errors = 4;
} }
message RemoveProductImageRequest // Get Low Stock Products
message GetLowStockProductsRequest
{ {
int64 product_gallery_id = 1; int32 threshold = 1;
int32 page_index = 2;
int32 page_size = 3;
google.protobuf.BoolValue is_club_exclusive = 4;
} }
message CategoryItem message GetLowStockProductsResponse
{ {
int64 id = 1; messages.MetaData meta_data = 1;
string title = 2; repeated LowStockProduct products = 2;
bool selected = 3;
} }
message GetCategoriesRequest message LowStockProduct
{ {
int64 product_id = 1; int64 id = 1;
string title = 2;
int32 remaining_count = 3;
int64 price = 4;
bool is_club_exclusive = 5;
} }
message GetCategoriesResponse // Toggle Product Status
message ToggleProductStatusRequest
{ {
repeated CategoryItem items = 1; repeated int64 product_ids = 1;
bool enable = 2;
int32 default_stock = 3;
} }
message UpdateProductCategoriesRequest message ToggleProductStatusResponse
{ {
int64 product_id = 1; int32 total = 1;
repeated int64 category_ids = 2; int32 succeeded = 2;
} int32 failed = 3;
repeated BulkOperationError errors = 4;
message CategoryProductItem
{
int64 id = 1;
string title = 2;
bool selected = 3;
}
message GetProductsForCategoryRequest
{
int64 category_id = 1;
}
message GetProductsForCategoryResponse
{
repeated CategoryProductItem items = 1;
}
message UpdateCategoryProductsRequest
{
int64 category_id = 1;
repeated int64 product_ids = 2;
} }