feat: Implement inventory and warehouse management features
Build and Deploy to Kubernetes / build-and-deploy (push) Failing after 2m33s
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:
+26
@@ -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; }
|
||||
}
|
||||
+95
@@ -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 };
|
||||
}
|
||||
}
|
||||
+16
@@ -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("نوع حرکت معتبر نیست");
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
namespace CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
|
||||
|
||||
public class CreateStockMovementResponseDto
|
||||
{
|
||||
/// <summary>شناسه حرکت موجودی ایجاد شده</summary>
|
||||
public long Id { get; set; }
|
||||
}
|
||||
+18
@@ -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;
|
||||
}
|
||||
+92
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+26
@@ -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; }
|
||||
}
|
||||
+11
@@ -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;
|
||||
}
|
||||
+46
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
+24
@@ -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; }
|
||||
}
|
||||
Reference in New Issue
Block a user