feat: Implement inventory and warehouse management features
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s

- Add GetLowStockItemsResponseDto and LowStockItemDto for low stock item queries.
- Create CreateStockMovementCommand and its handler for managing stock movements.
- Implement CreateStockMovementCommandValidator for validating stock movement commands.
- Add GetStockMovementsQuery and its handler to retrieve stock movement records.
- Create GetStockMovementsResponseDto and StockMovementListDto for stock movement responses.
- Implement GetStockMovementsByInventoryItemQuery and its handler for fetching movements by inventory item.
- Add CreateWarehouseCommand and its handler for creating new warehouses.
- Implement CreateWarehouseCommandValidator for warehouse creation validation.
- Add DeleteWarehouseCommand and its handler for removing warehouses.
- Implement SetDefaultWarehouseCommand and its handler for setting a default warehouse.
- Create UpdateWarehouseCommand and its handler for updating warehouse details.
- Implement GetAllWarehousesQuery and its handler to retrieve all warehouses.
- Add GetWarehouseQuery and its handler for fetching a specific warehouse by ID.
- Implement SearchWarehousesQuery and its handler for searching warehouses based on criteria.
This commit is contained in:
masoodafar-web
2026-01-03 11:59:08 +03:30
parent 8c3d710253
commit dde4c68b2f
102 changed files with 2715 additions and 3721 deletions
@@ -0,0 +1,26 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
/// <summary>
/// Command برای ایجاد حرکت موجودی جدید
/// </summary>
public record CreateStockMovementCommand : IRequest<CreateStockMovementResponseDto>
{
/// <summary>شناسه آیتم موجودی</summary>
public long InventoryItemId { get; init; }
/// <summary>نوع حرکت</summary>
public StockMovementType MovementType { get; init; }
/// <summary>تعداد</summary>
public int Quantity { get; init; }
/// <summary>یادداشت</summary>
public string? Note { get; init; }
/// <summary>شماره مرجع</summary>
public string? ReferenceNumber { get; init; }
/// <summary>شناسه سفارش عادی</summary>
public long? OrderId { get; init; }
/// <summary>شناسه سفارش تخفیفی</summary>
public long? DiscountOrderId { get; init; }
/// <summary>شناسه کاربر انجام‌دهنده</summary>
public long? PerformedByUserId { get; init; }
}
@@ -0,0 +1,95 @@
using CMSMicroservice.Application.Common.Exceptions;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
public class CreateStockMovementCommandHandler : IRequestHandler<CreateStockMovementCommand, CreateStockMovementResponseDto>
{
private readonly IApplicationDbContext _context;
public CreateStockMovementCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<CreateStockMovementResponseDto> Handle(CreateStockMovementCommand request, CancellationToken cancellationToken)
{
var inventoryItem = await _context.InventoryItems
.FirstOrDefaultAsync(i => i.Id == request.InventoryItemId, cancellationToken);
if (inventoryItem == null)
{
throw new NotFoundException(nameof(InventoryItem), request.InventoryItemId);
}
var quantityBefore = inventoryItem.Quantity;
var quantityAfter = quantityBefore;
// اعمال تغییرات موجودی بر اساس نوع حرکت
switch (request.MovementType)
{
case StockMovementType.InitialStock:
case StockMovementType.Restock:
case StockMovementType.Return:
case StockMovementType.AdjustmentPlus:
case StockMovementType.TransferIn:
quantityAfter = quantityBefore + request.Quantity;
inventoryItem.Quantity = quantityAfter;
inventoryItem.LastRestockedAt = DateTime.UtcNow;
break;
case StockMovementType.Sale:
case StockMovementType.AdjustmentMinus:
case StockMovementType.Lost:
case StockMovementType.Damaged:
case StockMovementType.TransferOut:
if (inventoryItem.Quantity < request.Quantity)
{
throw new InvalidOperationException($"Insufficient stock. Available: {inventoryItem.Quantity}, Requested: {request.Quantity}");
}
quantityAfter = quantityBefore - request.Quantity;
inventoryItem.Quantity = quantityAfter;
if (request.MovementType == StockMovementType.Sale)
{
inventoryItem.LastSoldAt = DateTime.UtcNow;
}
break;
case StockMovementType.Reserved:
if (inventoryItem.AvailableQuantity < request.Quantity)
{
throw new InvalidOperationException($"Insufficient available stock. Available: {inventoryItem.AvailableQuantity}, Requested: {request.Quantity}");
}
inventoryItem.ReservedQuantity += request.Quantity;
break;
case StockMovementType.Released:
if (inventoryItem.ReservedQuantity < request.Quantity)
{
throw new InvalidOperationException($"Cannot release more than reserved. Reserved: {inventoryItem.ReservedQuantity}, Requested: {request.Quantity}");
}
inventoryItem.ReservedQuantity -= request.Quantity;
break;
}
var entity = new StockMovement
{
InventoryItemId = request.InventoryItemId,
MovementType = request.MovementType,
Quantity = request.Quantity,
QuantityBefore = quantityBefore,
QuantityAfter = quantityAfter,
Note = request.Note,
ReferenceNumber = request.ReferenceNumber ?? $"MVT-{DateTime.UtcNow:yyyyMMddHHmmss}",
OrderId = request.OrderId,
DiscountOrderId = request.DiscountOrderId,
PerformedByUserId = request.PerformedByUserId
};
await _context.StockMovements.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new CreateStockMovementResponseDto { Id = entity.Id };
}
}
@@ -0,0 +1,16 @@
namespace CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
public class CreateStockMovementCommandValidator : AbstractValidator<CreateStockMovementCommand>
{
public CreateStockMovementCommandValidator()
{
RuleFor(x => x.InventoryItemId)
.GreaterThan(0).WithMessage("شناسه آیتم موجودی معتبر نیست");
RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("تعداد باید بیشتر از صفر باشد");
RuleFor(x => x.MovementType)
.IsInEnum().WithMessage("نوع حرکت معتبر نیست");
}
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
public class CreateStockMovementResponseDto
{
/// <summary>شناسه حرکت موجودی ایجاد شده</summary>
public long Id { get; set; }
}
@@ -0,0 +1,18 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
/// <summary>
/// Query برای جستجوی حرکات موجودی
/// </summary>
public record GetStockMovementsQuery : IRequest<GetStockMovementsResponseDto>
{
public long? InventoryItemId { get; init; }
public long? ProductId { get; init; }
public ProductType? ProductType { get; init; }
public StockMovementType? MovementType { get; init; }
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 50;
}
@@ -0,0 +1,92 @@
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
public class GetStockMovementsQueryHandler : IRequestHandler<GetStockMovementsQuery, GetStockMovementsResponseDto>
{
private readonly IApplicationDbContext _context;
public GetStockMovementsQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetStockMovementsResponseDto> Handle(GetStockMovementsQuery request, CancellationToken cancellationToken)
{
var query = _context.StockMovements
.AsNoTracking()
.Include(s => s.InventoryItem)
.ThenInclude(i => i!.Product)
.Include(s => s.InventoryItem)
.ThenInclude(i => i!.DiscountProduct)
.AsQueryable();
// فیلترها
if (request.InventoryItemId.HasValue)
{
query = query.Where(s => s.InventoryItemId == request.InventoryItemId.Value);
}
if (request.ProductId.HasValue)
{
query = query.Where(s =>
s.InventoryItem != null &&
(s.InventoryItem.ProductId == request.ProductId.Value ||
s.InventoryItem.DiscountProductId == request.ProductId.Value));
}
if (request.ProductType.HasValue)
{
query = query.Where(s =>
s.InventoryItem != null &&
s.InventoryItem.ProductType == request.ProductType.Value);
}
if (request.MovementType.HasValue)
{
query = query.Where(s => s.MovementType == request.MovementType.Value);
}
if (request.StartDate.HasValue)
{
query = query.Where(s => s.Created >= request.StartDate.Value);
}
if (request.EndDate.HasValue)
{
query = query.Where(s => s.Created <= request.EndDate.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
var movements = await query
.OrderByDescending(s => s.Created)
.Skip(request.Skip)
.Take(request.Take)
.Select(s => new StockMovementListDto
{
Id = s.Id,
InventoryItemId = s.InventoryItemId,
MovementType = s.MovementType,
Quantity = s.Quantity,
QuantityBefore = s.QuantityBefore,
QuantityAfter = s.QuantityAfter,
Note = s.Note,
ReferenceNumber = s.ReferenceNumber,
OrderId = s.OrderId,
DiscountOrderId = s.DiscountOrderId,
PerformedByUserId = s.PerformedByUserId,
ProductTitle = s.InventoryItem != null && s.InventoryItem.Product != null
? s.InventoryItem.Product.Title
: (s.InventoryItem != null && s.InventoryItem.DiscountProduct != null
? s.InventoryItem.DiscountProduct.Title
: null),
Created = s.Created
})
.ToListAsync(cancellationToken);
return new GetStockMovementsResponseDto
{
Movements = movements,
TotalCount = totalCount
};
}
}
@@ -0,0 +1,26 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
public class GetStockMovementsResponseDto
{
public List<StockMovementListDto> Movements { get; set; } = new();
public int TotalCount { get; set; }
}
public class StockMovementListDto
{
public long Id { get; set; }
public long InventoryItemId { get; set; }
public StockMovementType MovementType { get; set; }
public int Quantity { get; set; }
public int QuantityBefore { get; set; }
public int QuantityAfter { get; set; }
public string? Note { get; set; }
public string? ReferenceNumber { get; set; }
public long? OrderId { get; set; }
public long? DiscountOrderId { get; set; }
public long? PerformedByUserId { get; set; }
public string? ProductTitle { get; set; }
public DateTime Created { get; set; }
}
@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
/// <summary>
/// Query برای دریافت تاریخچه حرکات موجودی یک آیتم
/// </summary>
public record GetStockMovementsByInventoryItemQuery : IRequest<GetStockMovementsByInventoryItemResponseDto>
{
public long InventoryItemId { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 50;
}
@@ -0,0 +1,46 @@
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
public class GetStockMovementsByInventoryItemQueryHandler : IRequestHandler<GetStockMovementsByInventoryItemQuery, GetStockMovementsByInventoryItemResponseDto>
{
private readonly IApplicationDbContext _context;
public GetStockMovementsByInventoryItemQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetStockMovementsByInventoryItemResponseDto> Handle(GetStockMovementsByInventoryItemQuery request, CancellationToken cancellationToken)
{
var query = _context.StockMovements
.AsNoTracking()
.Where(s => s.InventoryItemId == request.InventoryItemId);
var totalCount = await query.CountAsync(cancellationToken);
var movements = await query
.OrderByDescending(s => s.Created)
.Skip(request.Skip)
.Take(request.Take)
.Select(s => new StockMovementHistoryDto
{
Id = s.Id,
MovementType = s.MovementType,
Quantity = s.Quantity,
QuantityBefore = s.QuantityBefore,
QuantityAfter = s.QuantityAfter,
Note = s.Note,
ReferenceNumber = s.ReferenceNumber,
OrderId = s.OrderId,
DiscountOrderId = s.DiscountOrderId,
PerformedByUserId = s.PerformedByUserId,
Created = s.Created
})
.ToListAsync(cancellationToken);
return new GetStockMovementsByInventoryItemResponseDto
{
Movements = movements,
TotalCount = totalCount
};
}
}
@@ -0,0 +1,24 @@
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
public class GetStockMovementsByInventoryItemResponseDto
{
public List<StockMovementHistoryDto> Movements { get; set; } = new();
public int TotalCount { get; set; }
}
public class StockMovementHistoryDto
{
public long Id { get; set; }
public StockMovementType MovementType { get; set; }
public int Quantity { get; set; }
public int QuantityBefore { get; set; }
public int QuantityAfter { get; set; }
public string? Note { get; set; }
public string? ReferenceNumber { get; set; }
public long? OrderId { get; set; }
public long? DiscountOrderId { get; set; }
public long? PerformedByUserId { get; set; }
public DateTime Created { get; set; }
}