From f6f943947ca38fc7016e8dc0aec33c82dc3b16ad Mon Sep 17 00:00:00 2001 From: masoodafar-web Date: Sat, 3 Jan 2026 07:37:32 +0330 Subject: [PATCH] feat: Implement Inventory Management Commands and Queries - Added CreateWarehouseCommand and CreateWarehouseCommandHandler for warehouse creation. - Introduced RecordLossCommand and RecordLossCommandHandler to handle stock loss recording. - Created UpdateInventorySettingsCommand and its handler for updating inventory settings. - Implemented GetAllInventoryItemsQuery and its handler to retrieve inventory items with pagination. - Added GetAllWarehousesQuery and handler for fetching all warehouses. - Developed GetLowStockItemsQuery and handler to get items below a specified stock threshold. - Implemented GetStockMovementsQuery and handler for retrieving stock movement records. - Created mappings for inventory-related requests and responses in InventoryProfile. - Developed InventoryService to handle gRPC requests for inventory operations. - Added Protobuf definitions for inventory management services and messages. --- .../Interfaces/IApplicationContractContext.cs | 4 + .../Commands/AddStock/AddStockCommand.cs | 19 ++ .../AddStock/AddStockCommandHandler.cs | 42 +++ .../AdjustStock/AdjustStockCommand.cs | 19 ++ .../AdjustStock/AdjustStockCommandHandler.cs | 37 +++ .../CreateWarehouse/CreateWarehouseCommand.cs | 10 + .../CreateWarehouseCommandHandler.cs | 29 ++ .../Commands/RecordLoss/RecordLossCommand.cs | 11 + .../RecordLoss/RecordLossCommandHandler.cs | 30 ++ .../UpdateInventorySettingsCommand.cs | 9 + .../UpdateInventorySettingsCommandHandler.cs | 27 ++ .../GetAllInventoryItemsQuery.cs | 38 +++ .../GetAllInventoryItemsQueryHandler.cs | 64 +++++ .../GetAllWarehouses/GetAllWarehousesQuery.cs | 22 ++ .../GetAllWarehousesQueryHandler.cs | 39 +++ .../GetLowStockItems/GetLowStockItemsQuery.cs | 32 +++ .../GetLowStockItemsQueryHandler.cs | 50 ++++ .../GetStockMovementsQuery.cs | 39 +++ .../GetStockMovementsQueryHandler.cs | 91 ++++++ .../Services/ApplicationContractContext.cs | 4 + .../BackOffice.BFF.WebApi.csproj | 1 + .../Common/Mappings/InventoryProfile.cs | 265 ++++++++++++++++++ .../Services/InventoryService.cs | 104 +++++++ src/BackOffice.BFF.sln | 15 + .../BackOffice.BFF.Inventory.Protobuf.csproj | 43 +++ .../Protos/inventory.proto | 241 ++++++++++++++++ 26 files changed, 1285 insertions(+) create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommand.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommandHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommand.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommandHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommand.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommandHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommand.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommandHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommand.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommandHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQuery.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQueryHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQuery.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQueryHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQuery.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQueryHandler.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQuery.cs create mode 100644 src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQueryHandler.cs create mode 100644 src/BackOffice.BFF.WebApi/Common/Mappings/InventoryProfile.cs create mode 100644 src/BackOffice.BFF.WebApi/Services/InventoryService.cs create mode 100644 src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj create mode 100644 src/Protobufs/BackOffice.BFF.Inventory.Protobuf/Protos/inventory.proto diff --git a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs index 0d3b29c..573b693 100644 --- a/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs +++ b/src/BackOffice.BFF.Application/Common/Interfaces/IApplicationContractContext.cs @@ -24,6 +24,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountOrder; using CMSMicroservice.Protobuf.Protos.ManualPayment; using CMSMicroservice.Protobuf.Protos.NetworkMembership; using CMSMicroservice.Protobuf.Protos.AppVersion; +using CMSMicroservice.Protobuf.Protos.Inventory; namespace BackOffice.BFF.Application.Common.Interfaces; @@ -69,6 +70,9 @@ public interface IApplicationContractContext // App Version Management AppVersionContract.AppVersionContractClient AppVersions { get; } + + // Inventory Management System + InventoryContract.InventoryContractClient Inventory { get; } #endregion } diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommand.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommand.cs new file mode 100644 index 0000000..072db44 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommand.cs @@ -0,0 +1,19 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Commands.AddStock; + +public record AddStockCommand : IRequest +{ + public long ProductId { get; init; } + public int ProductType { get; init; } // 1=Regular, 2=Discount + public int Quantity { get; init; } + public string? ReferenceNumber { get; init; } + public string? Note { get; init; } + public long? WarehouseId { get; init; } +} + +public class AddStockResponseDto +{ + public bool Success { get; set; } + public long InventoryItemId { get; set; } + public int NewQuantity { get; set; } + public string? Message { get; set; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommandHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommandHandler.cs new file mode 100644 index 0000000..b5cad31 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AddStock/AddStockCommandHandler.cs @@ -0,0 +1,42 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Commands.AddStock; + +public class AddStockCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public AddStockCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(AddStockCommand request, CancellationToken cancellationToken) + { + var protoRequest = new AddStockRequest + { + ProductId = request.ProductId, + ProductType = (ProductType)request.ProductType, + Quantity = request.Quantity + }; + + if (!string.IsNullOrWhiteSpace(request.ReferenceNumber)) + protoRequest.ReferenceNumber = request.ReferenceNumber; + + if (!string.IsNullOrWhiteSpace(request.Note)) + protoRequest.Note = request.Note; + + if (request.WarehouseId.HasValue) + protoRequest.WarehouseId = request.WarehouseId.Value; + + var response = await _context.Inventory.AddStockAsync(protoRequest, cancellationToken: cancellationToken); + + return new AddStockResponseDto + { + Success = true, + InventoryItemId = response.InventoryItemId, + NewQuantity = response.NewQuantity, + Message = "ورود کالا با موفقیت ثبت شد" + }; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommand.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommand.cs new file mode 100644 index 0000000..6a91790 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommand.cs @@ -0,0 +1,19 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Commands.AdjustStock; + +public record AdjustStockCommand : IRequest +{ + public long ProductId { get; init; } + public int ProductType { get; init; } // 1=Regular, 2=Discount + public int NewQuantity { get; init; } + public string? Note { get; init; } + public long? WarehouseId { get; init; } +} + +public class AdjustStockResponseDto +{ + public bool Success { get; set; } + public int OldQuantity { get; set; } + public int NewQuantity { get; set; } + public int Difference { get; set; } + public string? Message { get; set; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommandHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommandHandler.cs new file mode 100644 index 0000000..7b03907 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/AdjustStock/AdjustStockCommandHandler.cs @@ -0,0 +1,37 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Commands.AdjustStock; + +public class AdjustStockCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public AdjustStockCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(AdjustStockCommand request, CancellationToken cancellationToken) + { + var protoRequest = new AdjustStockRequest + { + ProductId = request.ProductId, + ProductType = (ProductType)request.ProductType, + NewQuantity = request.NewQuantity + }; + + if (!string.IsNullOrWhiteSpace(request.Note)) + protoRequest.Reason = request.Note; + + var response = await _context.Inventory.AdjustStockAsync(protoRequest, cancellationToken: cancellationToken); + + return new AdjustStockResponseDto + { + Success = true, + OldQuantity = response.PreviousQuantity, + NewQuantity = response.NewQuantity, + Difference = response.Difference, + Message = "تعدیل موجودی با موفقیت انجام شد" + }; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommand.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommand.cs new file mode 100644 index 0000000..17ad73f --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommand.cs @@ -0,0 +1,10 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Commands.CreateWarehouse; + +public record CreateWarehouseCommand : IRequest +{ + public string Name { get; init; } = string.Empty; + public string Code { get; init; } = string.Empty; + public string? Address { get; init; } + public bool IsDefault { get; init; } + public bool IsActive { get; init; } = true; +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommandHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommandHandler.cs new file mode 100644 index 0000000..52afa91 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/CreateWarehouse/CreateWarehouseCommandHandler.cs @@ -0,0 +1,29 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Commands.CreateWarehouse; + +public class CreateWarehouseCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public CreateWarehouseCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(CreateWarehouseCommand request, CancellationToken cancellationToken) + { + var protoRequest = new CreateWarehouseRequest + { + Name = request.Name, + Code = request.Code, + IsDefault = request.IsDefault + }; + + if (!string.IsNullOrWhiteSpace(request.Address)) + protoRequest.Address = request.Address; + + var response = await _context.Inventory.CreateWarehouseAsync(protoRequest, cancellationToken: cancellationToken); + return response.Id; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommand.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommand.cs new file mode 100644 index 0000000..c68b28b --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommand.cs @@ -0,0 +1,11 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Commands.RecordLoss; + +public record RecordLossCommand : IRequest +{ + public long ProductId { get; init; } + public int ProductType { get; init; } // 1=Regular, 2=Discount + public int Quantity { get; init; } + public int LossType { get; init; } // 40=Loss, 41=Damaged, 42=Expired + public string? Note { get; init; } + public long? WarehouseId { get; init; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommandHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommandHandler.cs new file mode 100644 index 0000000..3de3229 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/RecordLoss/RecordLossCommandHandler.cs @@ -0,0 +1,30 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Commands.RecordLoss; + +public class RecordLossCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public RecordLossCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(RecordLossCommand request, CancellationToken cancellationToken) + { + var protoRequest = new RecordLossRequest + { + ProductId = request.ProductId, + ProductType = (ProductType)request.ProductType, + Quantity = request.Quantity, + LossType = (StockMovementType)request.LossType + }; + + if (!string.IsNullOrWhiteSpace(request.Note)) + protoRequest.Reason = request.Note; + + await _context.Inventory.RecordLossAsync(protoRequest, cancellationToken: cancellationToken); + return true; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommand.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommand.cs new file mode 100644 index 0000000..6805988 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommand.cs @@ -0,0 +1,9 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Commands.UpdateInventorySettings; + +public record UpdateInventorySettingsCommand : IRequest +{ + public long InventoryItemId { get; init; } + public int LowStockThreshold { get; init; } + public int ReorderPoint { get; init; } + public int MaxStockLevel { get; init; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommandHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommandHandler.cs new file mode 100644 index 0000000..c4b21d6 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Commands/UpdateInventorySettings/UpdateInventorySettingsCommandHandler.cs @@ -0,0 +1,27 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Commands.UpdateInventorySettings; + +public class UpdateInventorySettingsCommandHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public UpdateInventorySettingsCommandHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(UpdateInventorySettingsCommand request, CancellationToken cancellationToken) + { + var protoRequest = new UpdateInventorySettingsRequest + { + Id = request.InventoryItemId, + LowStockThreshold = request.LowStockThreshold, + ReorderPoint = request.ReorderPoint, + MaxStockLevel = request.MaxStockLevel + }; + + await _context.Inventory.UpdateInventorySettingsAsync(protoRequest, cancellationToken: cancellationToken); + return true; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQuery.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQuery.cs new file mode 100644 index 0000000..0f6e14c --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQuery.cs @@ -0,0 +1,38 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems; + +public record GetAllInventoryItemsQuery : IRequest +{ + public int PageIndex { get; init; } = 1; + public int PageSize { get; init; } = 20; + public long? WarehouseId { get; init; } + public int? ProductType { get; init; } // 1=Regular, 2=Discount + public bool? LowStockOnly { get; init; } + public string? SearchTerm { get; init; } +} + +public class GetAllInventoryItemsResponseDto +{ + public int TotalCount { get; set; } + public MetaData MetaData { get; set; } = new(); + public List Items { get; set; } = new(); +} + +public class InventoryItemDto +{ + public long Id { get; set; } + public long? ProductId { get; set; } + public long? DiscountProductId { get; set; } + public int ProductType { get; set; } + public string ProductName { get; set; } = string.Empty; + public int Quantity { get; set; } + public int ReservedQuantity { get; set; } + public int AvailableQuantity { get; set; } + public int LowStockThreshold { get; set; } + public int ReorderPoint { get; set; } + public int MaxStockLevel { get; set; } + public DateTime? LastRestockedAt { get; set; } + public DateTime? LastSoldAt { get; set; } + public long WarehouseId { get; set; } + public string WarehouseName { get; set; } = string.Empty; + public bool IsLowStock => Quantity <= LowStockThreshold; +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQueryHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQueryHandler.cs new file mode 100644 index 0000000..2a02396 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllInventoryItems/GetAllInventoryItemsQueryHandler.cs @@ -0,0 +1,64 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; +using Google.Protobuf.WellKnownTypes; + +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems; + +public class GetAllInventoryItemsQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetAllInventoryItemsQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetAllInventoryItemsQuery request, CancellationToken cancellationToken) + { + var protoRequest = new GetAllInventoryItemsRequest + { + Page = request.PageIndex, + PageSize = request.PageSize + }; + + if (request.WarehouseId.HasValue) + protoRequest.WarehouseId = request.WarehouseId.Value; + + if (request.ProductType.HasValue) + protoRequest.ProductType = (ProductType)request.ProductType.Value; + + if (!string.IsNullOrWhiteSpace(request.SearchTerm)) + protoRequest.Search = request.SearchTerm; + + var response = await _context.Inventory.GetAllInventoryItemsAsync(protoRequest, cancellationToken: cancellationToken); + + return new GetAllInventoryItemsResponseDto + { + TotalCount = response.TotalCount, + MetaData = new MetaData + { + TotalCount = response.TotalCount, + PageSize = request.PageSize, + CurrentPage = request.PageIndex, + TotalPage = (int)Math.Ceiling((double)response.TotalCount / request.PageSize) + }, + Items = response.Items.Select(i => new InventoryItemDto + { + Id = i.Id, + ProductId = i.ProductId, + DiscountProductId = i.DiscountProductId, + ProductType = (int)i.ProductType, + ProductName = i.ProductTitle, + Quantity = i.Quantity, + ReservedQuantity = i.ReservedQuantity, + AvailableQuantity = i.AvailableQuantity, + LowStockThreshold = i.LowStockThreshold, + ReorderPoint = i.ReorderPoint, + MaxStockLevel = i.MaxStockLevel, + LastRestockedAt = i.LastRestockedAt?.ToDateTime(), + LastSoldAt = i.LastSoldAt?.ToDateTime(), + WarehouseId = i.WarehouseId, + WarehouseName = i.WarehouseName + }).ToList() + }; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQuery.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQuery.cs new file mode 100644 index 0000000..053d26e --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQuery.cs @@ -0,0 +1,22 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses; + +public record GetAllWarehousesQuery : IRequest +{ + public bool? ActiveOnly { get; init; } +} + +public class GetAllWarehousesResponseDto +{ + public int TotalCount { get; set; } + public List Warehouses { get; set; } = new(); +} + +public class WarehouseDto +{ + public long Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Code { get; set; } = string.Empty; + public string? Address { get; set; } + public bool IsDefault { get; set; } + public bool IsActive { get; set; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQueryHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQueryHandler.cs new file mode 100644 index 0000000..4600a3f --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetAllWarehouses/GetAllWarehousesQueryHandler.cs @@ -0,0 +1,39 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses; + +public class GetAllWarehousesQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetAllWarehousesQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetAllWarehousesQuery request, CancellationToken cancellationToken) + { + var protoRequest = new GetAllWarehousesRequest(); + + if (request.ActiveOnly.HasValue) + { + protoRequest.IsActive = request.ActiveOnly.Value; + } + + var response = await _context.Inventory.GetAllWarehousesAsync(protoRequest, cancellationToken: cancellationToken); + + return new GetAllWarehousesResponseDto + { + TotalCount = response.TotalCount, + Warehouses = response.Warehouses.Select(w => new WarehouseDto + { + Id = w.Id, + Name = w.Name, + Code = w.Code, + Address = w.Address, + IsDefault = w.IsDefault, + IsActive = w.IsActive + }).ToList() + }; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQuery.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQuery.cs new file mode 100644 index 0000000..d3ca3fd --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQuery.cs @@ -0,0 +1,32 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems; + +public record GetLowStockItemsQuery : IRequest +{ + public int? Threshold { get; init; } + public long? WarehouseId { get; init; } + public int? ProductType { get; init; } // 1=Regular, 2=Discount + public int Count { get; init; } = 50; +} + +public class GetLowStockItemsResponseDto +{ + public int TotalCount { get; set; } + public List Items { get; set; } = new(); +} + +public class LowStockItemDto +{ + public long Id { get; set; } + public long? ProductId { get; set; } + public long? DiscountProductId { get; set; } + public int ProductType { get; set; } + public string ProductName { get; set; } = string.Empty; + public int Quantity { get; set; } + public int ReservedQuantity { get; set; } + public int AvailableQuantity { get; set; } + public int LowStockThreshold { get; set; } + public int ReorderPoint { get; set; } + public long WarehouseId { get; set; } + public string WarehouseName { get; set; } = string.Empty; + public int DeficitAmount => LowStockThreshold - Quantity; // چقدر کم داریم +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQueryHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQueryHandler.cs new file mode 100644 index 0000000..787fb7b --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetLowStockItems/GetLowStockItemsQueryHandler.cs @@ -0,0 +1,50 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; + +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems; + +public class GetLowStockItemsQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetLowStockItemsQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetLowStockItemsQuery request, CancellationToken cancellationToken) + { + var protoRequest = new GetLowStockItemsRequest + { + Page = 1, + PageSize = request.Count + }; + + if (request.WarehouseId.HasValue) + protoRequest.WarehouseId = request.WarehouseId.Value; + + if (request.ProductType.HasValue) + protoRequest.ProductType = (ProductType)request.ProductType.Value; + + var response = await _context.Inventory.GetLowStockItemsAsync(protoRequest, cancellationToken: cancellationToken); + + return new GetLowStockItemsResponseDto + { + TotalCount = response.TotalCount, + Items = response.Items.Select(i => new LowStockItemDto + { + Id = i.Id, + ProductId = i.ProductId, + DiscountProductId = i.DiscountProductId, + ProductType = (int)i.ProductType, + ProductName = i.ProductTitle, + Quantity = i.Quantity, + ReservedQuantity = i.ReservedQuantity, + AvailableQuantity = i.AvailableQuantity, + LowStockThreshold = i.LowStockThreshold, + ReorderPoint = i.ReorderPoint, + WarehouseId = i.WarehouseId, + WarehouseName = i.WarehouseName + }).ToList() + }; + } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQuery.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQuery.cs new file mode 100644 index 0000000..211e26c --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQuery.cs @@ -0,0 +1,39 @@ +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements; + +public record GetStockMovementsQuery : IRequest +{ + public long? InventoryItemId { get; init; } + public long? ProductId { get; init; } + public int? ProductType { get; init; } + public int? MovementType { get; init; } + public DateTime? FromDate { get; init; } + public DateTime? ToDate { get; init; } + public int PageIndex { get; init; } = 1; + public int PageSize { get; init; } = 20; +} + +public class GetStockMovementsResponseDto +{ + public int TotalCount { get; set; } + public MetaData MetaData { get; set; } = new(); + public List Movements { get; set; } = new(); +} + +public class StockMovementDto +{ + public long Id { get; set; } + public long InventoryItemId { get; set; } + public string ProductName { get; set; } = string.Empty; + public int MovementType { get; set; } + public string MovementTypeName { get; set; } = string.Empty; + public int Quantity { get; set; } + public int QuantityBefore { get; set; } + public int QuantityAfter { get; set; } + public long? OrderId { get; set; } + public long? DiscountOrderId { get; set; } + public string? ReferenceNumber { get; set; } + public string? Note { get; set; } + public long? PerformedByUserId { get; set; } + public string? PerformedByUserName { get; set; } + public DateTime CreatedAt { get; set; } +} diff --git a/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQueryHandler.cs b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQueryHandler.cs new file mode 100644 index 0000000..794bfa6 --- /dev/null +++ b/src/BackOffice.BFF.Application/InventoryCQ/Queries/GetStockMovements/GetStockMovementsQueryHandler.cs @@ -0,0 +1,91 @@ +using CMSMicroservice.Protobuf.Protos.Inventory; +using Google.Protobuf.WellKnownTypes; + +namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements; + +public class GetStockMovementsQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + + public GetStockMovementsQueryHandler(IApplicationContractContext context) + { + _context = context; + } + + public async Task Handle(GetStockMovementsQuery request, CancellationToken cancellationToken) + { + var protoRequest = new GetStockMovementsRequest + { + Page = request.PageIndex, + PageSize = request.PageSize + }; + + if (request.InventoryItemId.HasValue) + protoRequest.InventoryItemId = request.InventoryItemId.Value; + + if (request.ProductId.HasValue) + protoRequest.ProductId = request.ProductId.Value; + + if (request.ProductType.HasValue) + protoRequest.ProductType = (ProductType)request.ProductType.Value; + + if (request.MovementType.HasValue) + protoRequest.MovementType = (StockMovementType)request.MovementType.Value; + + if (request.FromDate.HasValue) + protoRequest.FromDate = Timestamp.FromDateTime(request.FromDate.Value.ToUniversalTime()); + + if (request.ToDate.HasValue) + protoRequest.ToDate = Timestamp.FromDateTime(request.ToDate.Value.ToUniversalTime()); + + var response = await _context.Inventory.GetStockMovementsAsync(protoRequest, cancellationToken: cancellationToken); + + return new GetStockMovementsResponseDto + { + TotalCount = response.TotalCount, + MetaData = new MetaData + { + TotalCount = response.TotalCount, + PageSize = request.PageSize, + CurrentPage = request.PageIndex, + TotalPage = (int)Math.Ceiling((double)response.TotalCount / request.PageSize) + }, + Movements = response.Movements.Select(m => new StockMovementDto + { + Id = m.Id, + InventoryItemId = m.InventoryItemId, + ProductName = m.ProductTitle, + MovementType = (int)m.MovementType, + MovementTypeName = GetMovementTypeName(m.MovementType), + Quantity = m.Quantity, + QuantityBefore = m.QuantityBefore, + QuantityAfter = m.QuantityAfter, + OrderId = m.OrderId, + DiscountOrderId = m.DiscountOrderId, + ReferenceNumber = string.IsNullOrEmpty(m.ReferenceNumber) ? null : m.ReferenceNumber, + Note = string.IsNullOrEmpty(m.Note) ? null : m.Note, + PerformedByUserId = m.PerformedByUserId, + PerformedByUserName = null, + CreatedAt = m.Created?.ToDateTime() ?? DateTime.MinValue + }).ToList() + }; + } + + private static string GetMovementTypeName(StockMovementType type) => type switch + { + StockMovementType.InitialStock => "موجودی اولیه", + StockMovementType.Restock => "ورود کالا", + StockMovementType.Return => "برگشت از مشتری", + StockMovementType.Sale => "فروش", + StockMovementType.AdjustmentIncrease => "تعدیل افزایشی", + StockMovementType.AdjustmentDecrease => "تعدیل کاهشی", + StockMovementType.Reserved => "رزرو", + StockMovementType.Released => "آزادسازی رزرو", + StockMovementType.Loss => "مفقودی", + StockMovementType.Damaged => "ضایعات", + StockMovementType.Expired => "منقضی", + StockMovementType.TransferOut => "انتقال به انبار", + StockMovementType.TransferIn => "انتقال از انبار", + _ => "نامشخص" + }; +} diff --git a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs index 0d5c085..0351d1a 100644 --- a/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs +++ b/src/BackOffice.BFF.Infrastructure/Services/ApplicationContractContext.cs @@ -25,6 +25,7 @@ using CMSMicroservice.Protobuf.Protos.DiscountOrder; using CMSMicroservice.Protobuf.Protos.ManualPayment; using CMSMicroservice.Protobuf.Protos.NetworkMembership; using CMSMicroservice.Protobuf.Protos.AppVersion; +using CMSMicroservice.Protobuf.Protos.Inventory; // BFF Protobuf contracts @@ -97,5 +98,8 @@ public class ApplicationContractContext : IApplicationContractContext // App Version Management public AppVersionContract.AppVersionContractClient AppVersions => GetService(); + + // Inventory Management System + public InventoryContract.InventoryContractClient Inventory => GetService(); #endregion } diff --git a/src/BackOffice.BFF.WebApi/BackOffice.BFF.WebApi.csproj b/src/BackOffice.BFF.WebApi/BackOffice.BFF.WebApi.csproj index 6ac4fec..56927dd 100644 --- a/src/BackOffice.BFF.WebApi/BackOffice.BFF.WebApi.csproj +++ b/src/BackOffice.BFF.WebApi/BackOffice.BFF.WebApi.csproj @@ -48,6 +48,7 @@ + diff --git a/src/BackOffice.BFF.WebApi/Common/Mappings/InventoryProfile.cs b/src/BackOffice.BFF.WebApi/Common/Mappings/InventoryProfile.cs new file mode 100644 index 0000000..9236459 --- /dev/null +++ b/src/BackOffice.BFF.WebApi/Common/Mappings/InventoryProfile.cs @@ -0,0 +1,265 @@ +using BackOffice.BFF.Application.Common.Models; +using BackOffice.BFF.Application.InventoryCQ.Commands.AddStock; +using BackOffice.BFF.Application.InventoryCQ.Commands.AdjustStock; +using BackOffice.BFF.Application.InventoryCQ.Commands.CreateWarehouse; +using BackOffice.BFF.Application.InventoryCQ.Commands.RecordLoss; +using BackOffice.BFF.Application.InventoryCQ.Commands.UpdateInventorySettings; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements; +using Google.Protobuf.WellKnownTypes; +using BffProtos = Foursat.BackOffice.BFF.Inventory.Protos; + +namespace BackOffice.BFF.WebApi.Common.Mappings; + +public class InventoryProfile : IRegister +{ + void IRegister.Register(TypeAdapterConfig config) + { + // ============================================= + // GetAllWarehouses + // ============================================= + config.NewConfig() + .MapWith(src => new GetAllWarehousesQuery + { + ActiveOnly = src.ActiveOnly != null ? src.ActiveOnly.Value : null + }); + + config.NewConfig() + .MapWith(src => new BffProtos.GetAllWarehousesResponse + { + Warehouses = { src.Warehouses.Select(w => new BffProtos.WarehouseDto + { + Id = w.Id, + Name = w.Name ?? string.Empty, + Code = w.Code ?? string.Empty, + Address = w.Address ?? string.Empty, + IsDefault = w.IsDefault, + IsActive = w.IsActive + }) } + }); + + // ============================================= + // CreateWarehouse + // ============================================= + config.NewConfig() + .MapWith(src => new CreateWarehouseCommand + { + Name = src.Name, + Code = src.Code, + Address = src.Address, + IsDefault = src.IsDefault, + IsActive = true + }); + + config.NewConfig() + .MapWith(src => new BffProtos.CreateWarehouseResponse { Id = src }); + + // ============================================= + // GetAllInventoryItems + // ============================================= + config.NewConfig() + .MapWith(src => new GetAllInventoryItemsQuery + { + PageIndex = src.PageIndex > 0 ? src.PageIndex : 1, + PageSize = src.PageSize > 0 ? src.PageSize : 20, + WarehouseId = src.WarehouseId, + ProductType = src.ProductType != BffProtos.ProductType.Unspecified + ? (int)src.ProductType + : null, + SearchTerm = string.IsNullOrWhiteSpace(src.SearchTerm) ? null : src.SearchTerm + }); + + config.NewConfig() + .MapWith(src => new BffProtos.GetAllInventoryItemsResponse + { + TotalCount = src.TotalCount, + MetaData = new BackOffice.BFF.Protobuf.Common.MetaData + { + CurrentPage = src.MetaData.CurrentPage, + PageSize = src.MetaData.PageSize, + TotalCount = src.MetaData.TotalCount, + TotalPage = src.MetaData.TotalPage + }, + Items = { src.Items.Select(i => new BffProtos.InventoryItemDto + { + Id = i.Id, + ProductId = i.ProductId.HasValue ? i.ProductId.Value : null, + DiscountProductId = i.DiscountProductId.HasValue ? i.DiscountProductId.Value : null, + ProductType = (BffProtos.ProductType)i.ProductType, + ProductName = i.ProductName ?? string.Empty, + Quantity = i.Quantity, + ReservedQuantity = i.ReservedQuantity, + AvailableQuantity = i.AvailableQuantity, + LowStockThreshold = i.LowStockThreshold, + ReorderPoint = i.ReorderPoint, + MaxStockLevel = i.MaxStockLevel, + LastRestockedAt = i.LastRestockedAt.HasValue + ? Timestamp.FromDateTime(DateTime.SpecifyKind(i.LastRestockedAt.Value, DateTimeKind.Utc)) + : null, + LastSoldAt = i.LastSoldAt.HasValue + ? Timestamp.FromDateTime(DateTime.SpecifyKind(i.LastSoldAt.Value, DateTimeKind.Utc)) + : null, + WarehouseId = i.WarehouseId, + WarehouseName = i.WarehouseName ?? string.Empty, + IsLowStock = i.IsLowStock + }) } + }); + + // ============================================= + // GetLowStockItems + // ============================================= + config.NewConfig() + .MapWith(src => new GetLowStockItemsQuery + { + Count = src.Count > 0 ? src.Count : 10, + WarehouseId = src.WarehouseId, + ProductType = src.ProductType != BffProtos.ProductType.Unspecified + ? (int)src.ProductType + : null + }); + + config.NewConfig() + .MapWith(src => new BffProtos.GetLowStockItemsResponse + { + TotalCount = src.TotalCount, + Items = { src.Items.Select(i => new BffProtos.LowStockItemDto + { + Id = i.Id, + ProductId = i.ProductId.HasValue ? i.ProductId.Value : null, + DiscountProductId = i.DiscountProductId.HasValue ? i.DiscountProductId.Value : null, + ProductType = (BffProtos.ProductType)i.ProductType, + ProductName = i.ProductName ?? string.Empty, + Quantity = i.Quantity, + ReservedQuantity = i.ReservedQuantity, + AvailableQuantity = i.AvailableQuantity, + LowStockThreshold = i.LowStockThreshold, + ReorderPoint = i.ReorderPoint, + WarehouseId = i.WarehouseId, + WarehouseName = i.WarehouseName ?? string.Empty + }) } + }); + + // ============================================= + // GetStockMovements + // ============================================= + config.NewConfig() + .MapWith(src => new GetStockMovementsQuery + { + PageIndex = src.PageIndex > 0 ? src.PageIndex : 1, + PageSize = src.PageSize > 0 ? src.PageSize : 20, + InventoryItemId = src.InventoryItemId, + ProductId = src.ProductId, + ProductType = src.ProductType != BffProtos.ProductType.Unspecified + ? (int)src.ProductType + : null, + MovementType = src.MovementType != BffProtos.StockMovementType.Unspecified + ? (int)src.MovementType + : null, + FromDate = src.FromDate != null ? src.FromDate.ToDateTime() : null, + ToDate = src.ToDate != null ? src.ToDate.ToDateTime() : null + }); + + config.NewConfig() + .MapWith(src => new BffProtos.GetStockMovementsResponse + { + TotalCount = src.TotalCount, + MetaData = new BackOffice.BFF.Protobuf.Common.MetaData + { + CurrentPage = src.MetaData.CurrentPage, + PageSize = src.MetaData.PageSize, + TotalCount = src.MetaData.TotalCount, + TotalPage = src.MetaData.TotalPage + }, + Movements = { src.Movements.Select(m => new BffProtos.StockMovementDto + { + Id = m.Id, + InventoryItemId = m.InventoryItemId, + ProductName = m.ProductName ?? string.Empty, + MovementType = (BffProtos.StockMovementType)m.MovementType, + MovementTypeName = m.MovementTypeName ?? string.Empty, + Quantity = m.Quantity, + QuantityBefore = m.QuantityBefore, + QuantityAfter = m.QuantityAfter, + OrderId = m.OrderId.HasValue ? m.OrderId.Value : null, + DiscountOrderId = m.DiscountOrderId.HasValue ? m.DiscountOrderId.Value : null, + ReferenceNumber = m.ReferenceNumber ?? string.Empty, + Note = m.Note ?? string.Empty, + PerformedByUserId = m.PerformedByUserId.HasValue ? m.PerformedByUserId.Value : null, + PerformedByUserName = m.PerformedByUserName ?? string.Empty, + CreatedAt = Timestamp.FromDateTime(DateTime.SpecifyKind(m.CreatedAt, DateTimeKind.Utc)) + }) } + }); + + // ============================================= + // AddStock + // ============================================= + config.NewConfig() + .MapWith(src => new AddStockCommand + { + ProductId = src.ProductId, + ProductType = (int)src.ProductType, + Quantity = src.Quantity, + ReferenceNumber = src.ReferenceNumber, + Note = src.Note, + WarehouseId = src.WarehouseId + }); + + config.NewConfig() + .MapWith(src => new BffProtos.AddStockResponse + { + Success = src.Success, + InventoryItemId = src.InventoryItemId, + NewQuantity = src.NewQuantity, + Message = src.Message ?? string.Empty + }); + + // ============================================= + // AdjustStock + // ============================================= + config.NewConfig() + .MapWith(src => new AdjustStockCommand + { + ProductId = src.ProductId, + ProductType = (int)src.ProductType, + NewQuantity = src.NewQuantity, + Note = src.Note + }); + + config.NewConfig() + .MapWith(src => new BffProtos.AdjustStockResponse + { + Success = src.Success, + OldQuantity = src.OldQuantity, + NewQuantity = src.NewQuantity, + Difference = src.Difference, + Message = src.Message ?? string.Empty + }); + + // ============================================= + // RecordLoss + // ============================================= + config.NewConfig() + .MapWith(src => new RecordLossCommand + { + ProductId = src.ProductId, + ProductType = (int)src.ProductType, + Quantity = src.Quantity, + LossType = 40, // Loss type + Note = src.Reason + }); + + // ============================================= + // UpdateInventorySettings + // ============================================= + config.NewConfig() + .MapWith(src => new UpdateInventorySettingsCommand + { + InventoryItemId = src.InventoryItemId, + LowStockThreshold = src.LowStockThreshold, + ReorderPoint = src.ReorderPoint, + MaxStockLevel = src.MaxStockLevel + }); + } +} diff --git a/src/BackOffice.BFF.WebApi/Services/InventoryService.cs b/src/BackOffice.BFF.WebApi/Services/InventoryService.cs new file mode 100644 index 0000000..3cec667 --- /dev/null +++ b/src/BackOffice.BFF.WebApi/Services/InventoryService.cs @@ -0,0 +1,104 @@ +using BackOffice.BFF.WebApi.Common.Services; +using BackOffice.BFF.Application.InventoryCQ.Commands.AddStock; +using BackOffice.BFF.Application.InventoryCQ.Commands.AdjustStock; +using BackOffice.BFF.Application.InventoryCQ.Commands.CreateWarehouse; +using BackOffice.BFF.Application.InventoryCQ.Commands.RecordLoss; +using BackOffice.BFF.Application.InventoryCQ.Commands.UpdateInventorySettings; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems; +using BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements; +using Foursat.BackOffice.BFF.Inventory.Protos; + +namespace BackOffice.BFF.WebApi.Services; + +public class InventoryService : InventoryBFFContract.InventoryBFFContractBase +{ + private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS; + + public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS) + { + _dispatchRequestToCQRS = dispatchRequestToCQRS; + } + + public override async Task GetAllWarehouses( + GetAllWarehousesRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task CreateWarehouse( + CreateWarehouseRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task GetAllInventoryItems( + GetAllInventoryItemsRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task GetLowStockItems( + GetLowStockItemsRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task GetStockMovements( + GetStockMovementsRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task AddStock( + AddStockRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task AdjustStock( + AdjustStockRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task RecordLoss( + RecordLossRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } + + public override async Task UpdateInventorySettings( + UpdateInventorySettingsRequest request, + ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle( + request, + context); + } +} diff --git a/src/BackOffice.BFF.sln b/src/BackOffice.BFF.sln index 23780b7..1306dd1 100644 --- a/src/BackOffice.BFF.sln +++ b/src/BackOffice.BFF.sln @@ -59,6 +59,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.Configuratio EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.PublicMessage.Protobuf", "Protobufs\BackOffice.BFF.PublicMessage.Protobuf\BackOffice.BFF.PublicMessage.Protobuf.csproj", "{3454F4C0-A6C8-44BC-9389-6248518E3EA6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BackOffice.BFF.Inventory.Protobuf", "Protobufs\BackOffice.BFF.Inventory.Protobuf\BackOffice.BFF.Inventory.Protobuf.csproj", "{0FFE3E2F-4F78-4376-B373-51830C447EEE}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -393,6 +395,18 @@ Global {3454F4C0-A6C8-44BC-9389-6248518E3EA6}.Release|x64.Build.0 = Release|Any CPU {3454F4C0-A6C8-44BC-9389-6248518E3EA6}.Release|x86.ActiveCfg = Release|Any CPU {3454F4C0-A6C8-44BC-9389-6248518E3EA6}.Release|x86.Build.0 = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|x64.ActiveCfg = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|x64.Build.0 = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|x86.ActiveCfg = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Debug|x86.Build.0 = Debug|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|Any CPU.Build.0 = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|x64.ActiveCfg = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|x64.Build.0 = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|x86.ActiveCfg = Release|Any CPU + {0FFE3E2F-4F78-4376-B373-51830C447EEE}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -421,6 +435,7 @@ Global {1F95197B-9118-4C19-9C1B-C0872AA8F412} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {FB2AAF65-F9DC-4315-979E-A77EC44C5FB1} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {3454F4C0-A6C8-44BC-9389-6248518E3EA6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {0FFE3E2F-4F78-4376-B373-51830C447EEE} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0AE1AB4A-3C91-4853-93C2-C2476E79F845} diff --git a/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj b/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj new file mode 100644 index 0000000..21777bf --- /dev/null +++ b/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/BackOffice.BFF.Inventory.Protobuf.csproj @@ -0,0 +1,43 @@ + + + + net9.0 + enable + enable + true + Foursat.BackOffice.BFF.Inventory.Protobuf + 0.0.1 + FourSat + FourSat + Foursat.BackOffice.BFF.Inventory.Protobuf + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + $(PackageOutputPath)$(PackageId).$(Version).nupkg + + dotnet nuget push **/*.nupkg --source https://git.afrino.co/api/packages/FourSat/nuget/index.json --api-key 061a5cb15517c6da39c16cfce8556c55ae104d0d --skip-duplicate + + + + + + diff --git a/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/Protos/inventory.proto b/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/Protos/inventory.proto new file mode 100644 index 0000000..9c08a0c --- /dev/null +++ b/src/Protobufs/BackOffice.BFF.Inventory.Protobuf/Protos/inventory.proto @@ -0,0 +1,241 @@ +syntax = "proto3"; + +package inventory; + +import "public_messages.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/timestamp.proto"; + +option csharp_namespace = "Foursat.BackOffice.BFF.Inventory.Protos"; + +// ============================================= +// Service Definition +// ============================================= +service InventoryBFFContract +{ + // Warehouse Management + rpc GetAllWarehouses(GetAllWarehousesRequest) returns (GetAllWarehousesResponse); + rpc CreateWarehouse(CreateWarehouseRequest) returns (CreateWarehouseResponse); + + // Inventory Items + rpc GetAllInventoryItems(GetAllInventoryItemsRequest) returns (GetAllInventoryItemsResponse); + rpc GetLowStockItems(GetLowStockItemsRequest) returns (GetLowStockItemsResponse); + rpc GetStockMovements(GetStockMovementsRequest) returns (GetStockMovementsResponse); + + // Stock Operations + rpc AddStock(AddStockRequest) returns (AddStockResponse); + rpc AdjustStock(AdjustStockRequest) returns (AdjustStockResponse); + rpc RecordLoss(RecordLossRequest) returns (google.protobuf.Empty); + rpc UpdateInventorySettings(UpdateInventorySettingsRequest) returns (google.protobuf.Empty); +} + +// ============================================= +// Enums +// ============================================= +enum ProductType { + PRODUCT_TYPE_UNSPECIFIED = 0; + REGULAR = 1; + DISCOUNT = 2; +} + +enum StockMovementType { + STOCK_MOVEMENT_TYPE_UNSPECIFIED = 0; + INITIAL_STOCK = 1; + RESTOCK = 2; + RETURN = 3; + SALE = 4; + ADJUSTMENT_INCREASE = 5; + ADJUSTMENT_DECREASE = 6; + RESERVED = 7; + RELEASED = 8; + LOSS = 9; + DAMAGED = 10; + EXPIRED = 11; + TRANSFER_OUT = 12; + TRANSFER_IN = 13; +} + +// ============================================= +// Warehouse Messages +// ============================================= +message GetAllWarehousesRequest { + google.protobuf.BoolValue active_only = 1; +} + +message GetAllWarehousesResponse { + repeated WarehouseDto warehouses = 1; +} + +message WarehouseDto { + int64 id = 1; + string name = 2; + string code = 3; + string address = 4; + bool is_default = 5; + bool is_active = 6; + google.protobuf.Timestamp created = 7; +} + +message CreateWarehouseRequest { + string name = 1; + string code = 2; + string address = 3; + bool is_default = 4; +} + +message CreateWarehouseResponse { + int64 id = 1; +} + +// ============================================= +// Inventory Item Messages +// ============================================= +message GetAllInventoryItemsRequest { + int32 page_index = 1; + int32 page_size = 2; + google.protobuf.Int64Value warehouse_id = 3; + ProductType product_type = 4; + string search_term = 5; +} + +message GetAllInventoryItemsResponse { + int32 total_count = 1; + messages.MetaData meta_data = 2; + repeated InventoryItemDto items = 3; +} + +message InventoryItemDto { + int64 id = 1; + google.protobuf.Int64Value product_id = 2; + google.protobuf.Int64Value discount_product_id = 3; + ProductType product_type = 4; + string product_name = 5; + int32 quantity = 6; + int32 reserved_quantity = 7; + int32 available_quantity = 8; + int32 low_stock_threshold = 9; + int32 reorder_point = 10; + int32 max_stock_level = 11; + google.protobuf.Timestamp last_restocked_at = 12; + google.protobuf.Timestamp last_sold_at = 13; + int64 warehouse_id = 14; + string warehouse_name = 15; + bool is_low_stock = 16; +} + +// ============================================= +// Low Stock Messages +// ============================================= +message GetLowStockItemsRequest { + int32 count = 1; + google.protobuf.Int64Value warehouse_id = 2; + ProductType product_type = 3; +} + +message GetLowStockItemsResponse { + int32 total_count = 1; + repeated LowStockItemDto items = 2; +} + +message LowStockItemDto { + int64 id = 1; + google.protobuf.Int64Value product_id = 2; + google.protobuf.Int64Value discount_product_id = 3; + ProductType product_type = 4; + string product_name = 5; + int32 quantity = 6; + int32 reserved_quantity = 7; + int32 available_quantity = 8; + int32 low_stock_threshold = 9; + int32 reorder_point = 10; + int64 warehouse_id = 11; + string warehouse_name = 12; +} + +// ============================================= +// Stock Movement Messages +// ============================================= +message GetStockMovementsRequest { + int32 page_index = 1; + int32 page_size = 2; + google.protobuf.Int64Value inventory_item_id = 3; + google.protobuf.Int64Value product_id = 4; + ProductType product_type = 5; + StockMovementType movement_type = 6; + google.protobuf.Timestamp from_date = 7; + google.protobuf.Timestamp to_date = 8; +} + +message GetStockMovementsResponse { + int32 total_count = 1; + messages.MetaData meta_data = 2; + repeated StockMovementDto movements = 3; +} + +message StockMovementDto { + int64 id = 1; + int64 inventory_item_id = 2; + string product_name = 3; + StockMovementType movement_type = 4; + string movement_type_name = 5; + int32 quantity = 6; + int32 quantity_before = 7; + int32 quantity_after = 8; + google.protobuf.Int64Value order_id = 9; + google.protobuf.Int64Value discount_order_id = 10; + string reference_number = 11; + string note = 12; + google.protobuf.Int64Value performed_by_user_id = 13; + string performed_by_user_name = 14; + google.protobuf.Timestamp created_at = 15; +} + +// ============================================= +// Stock Operation Messages +// ============================================= +message AddStockRequest { + int64 product_id = 1; + ProductType product_type = 2; + int32 quantity = 3; + string reference_number = 4; + string note = 5; + google.protobuf.Int64Value warehouse_id = 6; +} + +message AddStockResponse { + bool success = 1; + int64 inventory_item_id = 2; + int32 new_quantity = 3; + string message = 4; +} + +message AdjustStockRequest { + int64 product_id = 1; + ProductType product_type = 2; + int32 new_quantity = 3; + string note = 4; +} + +message AdjustStockResponse { + bool success = 1; + int32 old_quantity = 2; + int32 new_quantity = 3; + int32 difference = 4; + string message = 5; +} + +message RecordLossRequest { + int64 product_id = 1; + ProductType product_type = 2; + int32 quantity = 3; + string reason = 4; + string reference_number = 5; +} + +message UpdateInventorySettingsRequest { + int64 inventory_item_id = 1; + int32 low_stock_threshold = 2; + int32 reorder_point = 3; + int32 max_stock_level = 4; +}