feat: Implement Inventory Management Commands and Queries
Build and Deploy / build (push) Successful in 1m46s

- 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.
This commit is contained in:
masoodafar-web
2026-01-03 07:37:32 +03:30
parent 0c63b3c83c
commit f6f943947c
26 changed files with 1285 additions and 0 deletions
@@ -0,0 +1,38 @@
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems;
public record GetAllInventoryItemsQuery : IRequest<GetAllInventoryItemsResponseDto>
{
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<InventoryItemDto> 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;
}
@@ -0,0 +1,64 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllInventoryItems;
public class GetAllInventoryItemsQueryHandler : IRequestHandler<GetAllInventoryItemsQuery, GetAllInventoryItemsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllInventoryItemsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllInventoryItemsResponseDto> 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()
};
}
}
@@ -0,0 +1,22 @@
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses;
public record GetAllWarehousesQuery : IRequest<GetAllWarehousesResponseDto>
{
public bool? ActiveOnly { get; init; }
}
public class GetAllWarehousesResponseDto
{
public int TotalCount { get; set; }
public List<WarehouseDto> 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; }
}
@@ -0,0 +1,39 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetAllWarehouses;
public class GetAllWarehousesQueryHandler : IRequestHandler<GetAllWarehousesQuery, GetAllWarehousesResponseDto>
{
private readonly IApplicationContractContext _context;
public GetAllWarehousesQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetAllWarehousesResponseDto> 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()
};
}
}
@@ -0,0 +1,32 @@
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems;
public record GetLowStockItemsQuery : IRequest<GetLowStockItemsResponseDto>
{
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<LowStockItemDto> 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; // چقدر کم داریم
}
@@ -0,0 +1,50 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetLowStockItems;
public class GetLowStockItemsQueryHandler : IRequestHandler<GetLowStockItemsQuery, GetLowStockItemsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetLowStockItemsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetLowStockItemsResponseDto> 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()
};
}
}
@@ -0,0 +1,39 @@
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements;
public record GetStockMovementsQuery : IRequest<GetStockMovementsResponseDto>
{
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<StockMovementDto> 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; }
}
@@ -0,0 +1,91 @@
using CMSMicroservice.Protobuf.Protos.Inventory;
using Google.Protobuf.WellKnownTypes;
namespace BackOffice.BFF.Application.InventoryCQ.Queries.GetStockMovements;
public class GetStockMovementsQueryHandler : IRequestHandler<GetStockMovementsQuery, GetStockMovementsResponseDto>
{
private readonly IApplicationContractContext _context;
public GetStockMovementsQueryHandler(IApplicationContractContext context)
{
_context = context;
}
public async Task<GetStockMovementsResponseDto> 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 => "انتقال از انبار",
_ => "نامشخص"
};
}