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,18 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
/// <summary>
/// Command برای ایجاد انبار جدید
/// </summary>
public record CreateWarehouseCommand : IRequest<CreateWarehouseResponseDto>
{
/// <summary>نام انبار</summary>
public string Name { get; init; } = string.Empty;
/// <summary>کد انبار</summary>
public string Code { get; init; } = string.Empty;
/// <summary>آدرس انبار</summary>
public string? Address { get; init; }
/// <summary>فعال؟</summary>
public bool IsActive { get; init; } = true;
/// <summary>پیش‌فرض؟</summary>
public bool IsDefault { get; init; } = false;
}
@@ -0,0 +1,39 @@
using CMSMicroservice.Domain.Entities;
namespace CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
public class CreateWarehouseCommandHandler : IRequestHandler<CreateWarehouseCommand, CreateWarehouseResponseDto>
{
private readonly IApplicationDbContext _context;
public CreateWarehouseCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<CreateWarehouseResponseDto> Handle(CreateWarehouseCommand request, CancellationToken cancellationToken)
{
// بررسی تکراری نبودن کد
var codeExists = await _context.Warehouses
.AnyAsync(w => w.Code == request.Code, cancellationToken);
if (codeExists)
{
throw new InvalidOperationException($"Warehouse with code '{request.Code}' already exists");
}
var entity = new Warehouse
{
Name = request.Name,
Code = request.Code,
Address = request.Address,
IsActive = request.IsActive,
IsDefault = request.IsDefault
};
await _context.Warehouses.AddAsync(entity, cancellationToken);
await _context.SaveChangesAsync(cancellationToken);
return new CreateWarehouseResponseDto { Id = entity.Id };
}
}
@@ -0,0 +1,18 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
public class CreateWarehouseCommandValidator : AbstractValidator<CreateWarehouseCommand>
{
public CreateWarehouseCommandValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("نام انبار الزامی است")
.MaximumLength(100).WithMessage("نام انبار نباید بیش از 100 کاراکتر باشد");
RuleFor(x => x.Code)
.NotEmpty().WithMessage("کد انبار الزامی است")
.MaximumLength(50).WithMessage("کد انبار نباید بیش از 50 کاراکتر باشد");
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("آدرس نباید بیش از 500 کاراکتر باشد");
}
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
public class CreateWarehouseResponseDto
{
/// <summary>شناسه انبار ایجاد شده</summary>
public long Id { get; set; }
}
@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
/// <summary>
/// Command برای حذف انبار
/// </summary>
public record DeleteWarehouseCommand(long Id) : IRequest<DeleteWarehouseResponseDto>;
@@ -0,0 +1,38 @@
using CMSMicroservice.Application.Common.Exceptions;
namespace CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
public class DeleteWarehouseCommandHandler : IRequestHandler<DeleteWarehouseCommand, DeleteWarehouseResponseDto>
{
private readonly IApplicationDbContext _context;
public DeleteWarehouseCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<DeleteWarehouseResponseDto> Handle(DeleteWarehouseCommand request, CancellationToken cancellationToken)
{
var warehouse = await _context.Warehouses
.FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken);
if (warehouse == null)
{
throw new NotFoundException(nameof(Domain.Entities.Warehouse), request.Id);
}
// بررسی اینکه انبار دارای موجودی نباشد
var hasInventory = await _context.InventoryItems
.AnyAsync(i => i.WarehouseId == request.Id, cancellationToken);
if (hasInventory)
{
throw new InvalidOperationException("Cannot delete warehouse with existing inventory items");
}
_context.Warehouses.Remove(warehouse);
await _context.SaveChangesAsync(cancellationToken);
return new DeleteWarehouseResponseDto { Success = true };
}
}
@@ -0,0 +1,10 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
public class DeleteWarehouseCommandValidator : AbstractValidator<DeleteWarehouseCommand>
{
public DeleteWarehouseCommandValidator()
{
RuleFor(x => x.Id)
.GreaterThan(0).WithMessage("شناسه انبار معتبر نیست");
}
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
public class DeleteWarehouseResponseDto
{
/// <summary>آیا عملیات موفق بود؟</summary>
public bool Success { get; set; }
}
@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
/// <summary>
/// Command برای تعیین انبار پیش‌فرض
/// </summary>
public record SetDefaultWarehouseCommand(long Id) : IRequest<SetDefaultWarehouseResponseDto>;
@@ -0,0 +1,41 @@
using CMSMicroservice.Application.Common.Exceptions;
namespace CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
public class SetDefaultWarehouseCommandHandler : IRequestHandler<SetDefaultWarehouseCommand, SetDefaultWarehouseResponseDto>
{
private readonly IApplicationDbContext _context;
public SetDefaultWarehouseCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<SetDefaultWarehouseResponseDto> Handle(SetDefaultWarehouseCommand request, CancellationToken cancellationToken)
{
var warehouse = await _context.Warehouses
.FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken);
if (warehouse == null)
{
throw new NotFoundException(nameof(Domain.Entities.Warehouse), request.Id);
}
// ریست کردن همه انبارهای دیگر
var otherWarehouses = await _context.Warehouses
.Where(w => w.IsDefault && w.Id != request.Id)
.ToListAsync(cancellationToken);
foreach (var other in otherWarehouses)
{
other.IsDefault = false;
}
// تنظیم این انبار به عنوان پیش‌فرض
warehouse.IsDefault = true;
await _context.SaveChangesAsync(cancellationToken);
return new SetDefaultWarehouseResponseDto { Success = true };
}
}
@@ -0,0 +1,10 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
public class SetDefaultWarehouseCommandValidator : AbstractValidator<SetDefaultWarehouseCommand>
{
public SetDefaultWarehouseCommandValidator()
{
RuleFor(x => x.Id)
.GreaterThan(0).WithMessage("شناسه انبار معتبر نیست");
}
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
public class SetDefaultWarehouseResponseDto
{
/// <summary>آیا عملیات موفق بود؟</summary>
public bool Success { get; set; }
}
@@ -0,0 +1,18 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
/// <summary>
/// Command برای آپدیت انبار
/// </summary>
public record UpdateWarehouseCommand : IRequest<UpdateWarehouseResponseDto>
{
/// <summary>شناسه انبار</summary>
public long Id { get; init; }
/// <summary>نام انبار</summary>
public string? Name { get; init; }
/// <summary>کد انبار</summary>
public string? Code { get; init; }
/// <summary>آدرس انبار</summary>
public string? Address { get; init; }
/// <summary>فعال؟</summary>
public bool? IsActive { get; init; }
}
@@ -0,0 +1,56 @@
using CMSMicroservice.Application.Common.Exceptions;
namespace CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
public class UpdateWarehouseCommandHandler : IRequestHandler<UpdateWarehouseCommand, UpdateWarehouseResponseDto>
{
private readonly IApplicationDbContext _context;
public UpdateWarehouseCommandHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<UpdateWarehouseResponseDto> Handle(UpdateWarehouseCommand request, CancellationToken cancellationToken)
{
var warehouse = await _context.Warehouses
.FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken);
if (warehouse == null)
{
throw new NotFoundException(nameof(Domain.Entities.Warehouse), request.Id);
}
// بررسی تکراری نبودن کد جدید
if (!string.IsNullOrEmpty(request.Code) && request.Code != warehouse.Code)
{
var codeExists = await _context.Warehouses
.AnyAsync(w => w.Code == request.Code && w.Id != request.Id, cancellationToken);
if (codeExists)
{
throw new InvalidOperationException($"Warehouse with code '{request.Code}' already exists");
}
warehouse.Code = request.Code;
}
if (!string.IsNullOrEmpty(request.Name))
{
warehouse.Name = request.Name;
}
if (request.Address != null)
{
warehouse.Address = request.Address;
}
if (request.IsActive.HasValue)
{
warehouse.IsActive = request.IsActive.Value;
}
await _context.SaveChangesAsync(cancellationToken);
return new UpdateWarehouseResponseDto { Success = true };
}
}
@@ -0,0 +1,19 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
public class UpdateWarehouseCommandValidator : AbstractValidator<UpdateWarehouseCommand>
{
public UpdateWarehouseCommandValidator()
{
RuleFor(x => x.Id)
.GreaterThan(0).WithMessage("شناسه انبار معتبر نیست");
RuleFor(x => x.Name)
.MaximumLength(100).WithMessage("نام انبار نباید بیش از 100 کاراکتر باشد");
RuleFor(x => x.Code)
.MaximumLength(50).WithMessage("کد انبار نباید بیش از 50 کاراکتر باشد");
RuleFor(x => x.Address)
.MaximumLength(500).WithMessage("آدرس نباید بیش از 500 کاراکتر باشد");
}
}
@@ -0,0 +1,7 @@
namespace CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
public class UpdateWarehouseResponseDto
{
/// <summary>آیا عملیات موفق بود؟</summary>
public bool Success { get; set; }
}
@@ -0,0 +1,6 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
/// <summary>
/// Query برای گرفتن لیست تمام انبارها
/// </summary>
public record GetAllWarehousesQuery : IRequest<GetAllWarehousesResponseDto>;
@@ -0,0 +1,34 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
public class GetAllWarehousesQueryHandler : IRequestHandler<GetAllWarehousesQuery, GetAllWarehousesResponseDto>
{
private readonly IApplicationDbContext _context;
public GetAllWarehousesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetAllWarehousesResponseDto> Handle(GetAllWarehousesQuery request, CancellationToken cancellationToken)
{
var warehouses = await _context.Warehouses
.AsNoTracking()
.OrderBy(w => w.Name)
.Select(w => new WarehouseItemDto
{
Id = w.Id,
Name = w.Name,
Code = w.Code,
Address = w.Address,
IsDefault = w.IsDefault,
IsActive = w.IsActive
})
.ToListAsync(cancellationToken);
return new GetAllWarehousesResponseDto
{
Warehouses = warehouses,
TotalCount = warehouses.Count
};
}
}
@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
public class GetAllWarehousesResponseDto
{
public List<WarehouseItemDto> Warehouses { get; set; } = new();
public int TotalCount { get; set; }
}
public class WarehouseItemDto
{
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,6 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
/// <summary>
/// Query برای گرفتن انبار با شناسه
/// </summary>
public record GetWarehouseQuery(long Id) : IRequest<GetWarehouseResponseDto?>;
@@ -0,0 +1,30 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
public class GetWarehouseQueryHandler : IRequestHandler<GetWarehouseQuery, GetWarehouseResponseDto?>
{
private readonly IApplicationDbContext _context;
public GetWarehouseQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<GetWarehouseResponseDto?> Handle(GetWarehouseQuery request, CancellationToken cancellationToken)
{
var warehouse = await _context.Warehouses
.AsNoTracking()
.Where(w => w.Id == request.Id)
.Select(w => new GetWarehouseResponseDto
{
Id = w.Id,
Name = w.Name,
Code = w.Code,
Address = w.Address,
IsDefault = w.IsDefault,
IsActive = w.IsActive
})
.FirstOrDefaultAsync(cancellationToken);
return warehouse;
}
}
@@ -0,0 +1,11 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
public class GetWarehouseResponseDto
{
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,12 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.SearchWarehouses;
/// <summary>
/// Query برای جستجوی انبارها
/// </summary>
public record SearchWarehousesQuery : IRequest<SearchWarehousesResponseDto>
{
public string? SearchTerm { get; init; }
public bool? IsActive { get; init; }
public int Skip { get; init; } = 0;
public int Take { get; init; } = 50;
}
@@ -0,0 +1,54 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.SearchWarehouses;
public class SearchWarehousesQueryHandler : IRequestHandler<SearchWarehousesQuery, SearchWarehousesResponseDto>
{
private readonly IApplicationDbContext _context;
public SearchWarehousesQueryHandler(IApplicationDbContext context)
{
_context = context;
}
public async Task<SearchWarehousesResponseDto> Handle(SearchWarehousesQuery request, CancellationToken cancellationToken)
{
var query = _context.Warehouses.AsNoTracking();
// فیلتر بر اساس جستجو
if (!string.IsNullOrEmpty(request.SearchTerm))
{
query = query.Where(w =>
w.Name.Contains(request.SearchTerm) ||
w.Code.Contains(request.SearchTerm) ||
(w.Address != null && w.Address.Contains(request.SearchTerm)));
}
// فیلتر بر اساس فعال بودن
if (request.IsActive.HasValue)
{
query = query.Where(w => w.IsActive == request.IsActive.Value);
}
var totalCount = await query.CountAsync(cancellationToken);
var warehouses = await query
.OrderBy(w => w.Name)
.Skip(request.Skip)
.Take(request.Take)
.Select(w => new WarehouseSearchItemDto
{
Id = w.Id,
Name = w.Name,
Code = w.Code,
Address = w.Address,
IsDefault = w.IsDefault,
IsActive = w.IsActive
})
.ToListAsync(cancellationToken);
return new SearchWarehousesResponseDto
{
Warehouses = warehouses,
TotalCount = totalCount
};
}
}
@@ -0,0 +1,17 @@
namespace CMSMicroservice.Application.WarehouseCQ.Queries.SearchWarehouses;
public class SearchWarehousesResponseDto
{
public List<WarehouseSearchItemDto> Warehouses { get; set; } = new();
public int TotalCount { get; set; }
}
public class WarehouseSearchItemDto
{
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; }
}