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
@@ -1,9 +1,7 @@
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Application.DayaLoanCQ.Services;
using CMSMicroservice.Infrastructure.Persistence;
using CMSMicroservice.Infrastructure.Persistence.Interceptors;
using CMSMicroservice.Infrastructure.Persistence.Repositories;
using CMSMicroservice.Infrastructure.BackgroundJobs;
using CMSMicroservice.Infrastructure.Services.Monitoring;
using CMSMicroservice.Infrastructure.Configuration;
@@ -121,10 +119,7 @@ public static class ConfigureServices
builder => builder.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName)));
}
// Repository Pattern Registration
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
// Inventory Business Service
services.AddScoped<IInventoryService, InventoryService>();
#region AddAuthentication
@@ -2,9 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Infrastructure.Persistence;
using CMSMicroservice.Infrastructure.Persistence.Repositories;
using CMSMicroservice.Infrastructure.Services;
namespace CMSMicroservice.Infrastructure;
@@ -31,11 +29,6 @@ public static class DependencyInjection
// Application Context Interface
services.AddScoped<IApplicationDbContext>(provider => provider.GetRequiredService<ApplicationDbContext>());
// Repository Pattern Registration
services.AddScoped<IInventoryItemRepository, InventoryItemRepository>();
services.AddScoped<IStockMovementRepository, StockMovementRepository>();
services.AddScoped<IWarehouseRepository, WarehouseRepository>();
// Business Services
services.AddScoped<IInventoryService, InventoryService>();
@@ -1,454 +0,0 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت موجودی محصولات
/// </summary>
public class InventoryItemRepository : IInventoryItemRepository
{
private readonly IApplicationDbContext _context;
public InventoryItemRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<InventoryItem?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
}
public async Task<InventoryItem?> GetByProductIdAsync(long productId, long warehouseId = 1, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.ProductId == productId && i.WarehouseId == warehouseId, cancellationToken);
}
public async Task<InventoryItem?> GetByDiscountProductIdAsync(long discountProductId, long warehouseId = 1, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.FirstOrDefaultAsync(i => i.DiscountProductId == discountProductId && i.WarehouseId == warehouseId, cancellationToken);
}
public async Task<List<InventoryItem>> GetByWarehouseIdAsync(long warehouseId, CancellationToken cancellationToken = default)
{
return await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId)
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> GetLowStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId && i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
return await query
.OrderBy(i => i.Quantity)
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> GetOutOfStockItemsAsync(ProductType? productType = null, long warehouseId = 1, CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId && i.Quantity == 0);
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
return await query
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.ToListAsync(cancellationToken);
}
public async Task<List<InventoryItem>> SearchAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Include(i => i.Warehouse)
.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(i =>
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
}
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
if (warehouseId.HasValue)
{
query = query.Where(i => i.WarehouseId == warehouseId.Value);
}
if (minQuantity.HasValue)
{
query = query.Where(i => i.Quantity >= minQuantity.Value);
}
if (maxQuantity.HasValue)
{
query = query.Where(i => i.Quantity <= maxQuantity.Value);
}
return await query
.OrderBy(i => i.Product != null ? i.Product.Title : i.DiscountProduct != null ? i.DiscountProduct.Title : "")
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
string? searchTerm = null,
ProductType? productType = null,
long? warehouseId = null,
int? minQuantity = null,
int? maxQuantity = null,
CancellationToken cancellationToken = default)
{
var query = _context.InventoryItems.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(i =>
(i.Product != null && i.Product.Title.ToLower().Contains(term)) ||
(i.DiscountProduct != null && i.DiscountProduct.Title.ToLower().Contains(term)));
}
if (productType.HasValue)
{
query = query.Where(i => i.ProductType == productType.Value);
}
if (warehouseId.HasValue)
{
query = query.Where(i => i.WarehouseId == warehouseId.Value);
}
if (minQuantity.HasValue)
{
query = query.Where(i => i.Quantity >= minQuantity.Value);
}
if (maxQuantity.HasValue)
{
query = query.Where(i => i.Quantity <= maxQuantity.Value);
}
return await query.CountAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<InventoryItem> AddAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
{
_context.InventoryItems.Add(inventoryItem);
await _context.SaveChangesAsync(cancellationToken);
return inventoryItem;
}
public async Task UpdateAsync(InventoryItem inventoryItem, CancellationToken cancellationToken = default)
{
_context.InventoryItems.Update(inventoryItem);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { id }, cancellationToken);
if (item != null)
{
_context.InventoryItems.Remove(item);
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task<bool> UpdateQuantityAsync(
long inventoryItemId,
int quantityChange,
StockMovementType movementType,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی اینکه موجودی کافی برای کاهش موجود باشد
if (quantityChange < 0 && item.Quantity + quantityChange < 0)
{
return false;
}
// بروزرسانی موجودی
item.Quantity += quantityChange;
// بروزرسانی تاریخ آخرین فعالیت
if (movementType == StockMovementType.Sale)
{
item.LastSoldAt = DateTime.UtcNow;
}
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
{
item.LastRestockedAt = DateTime.UtcNow;
}
// ثبت حرکت موجودی
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = movementType,
Quantity = Math.Abs(quantityChange),
Note = note,
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> ReserveQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی موجودی قابل دسترس
if (item.AvailableQuantity < quantity)
{
return false;
}
// رزرو موجودی
item.ReservedQuantity += quantity;
// ثبت حرکت رزرو
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = StockMovementType.Reserved,
Quantity = quantity,
Note = note ?? "Quantity reserved",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> ReleaseReservedQuantityAsync(
long inventoryItemId,
int quantity,
string? note = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var item = await _context.InventoryItems.FindAsync(new object[] { inventoryItemId }, cancellationToken);
if (item == null) return false;
// بررسی اینکه مقدار رزرو شده کافی باشد
if (item.ReservedQuantity < quantity)
{
return false;
}
// آزاد کردن رزرو
item.ReservedQuantity -= quantity;
// ثبت حرکت آزادسازی
var stockMovement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = StockMovementType.Released,
Quantity = quantity,
Note = note ?? "Reserved quantity released",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
#endregion
#region Bulk Operations
public async Task<bool> BulkUpdateQuantityAsync(
List<(long InventoryItemId, int QuantityChange, string? Note)> updates,
StockMovementType movementType,
string? referenceNumber = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var inventoryItemIds = updates.Select(u => u.InventoryItemId).ToList();
var items = await _context.InventoryItems
.Where(i => inventoryItemIds.Contains(i.Id))
.ToListAsync(cancellationToken);
if (items.Count != updates.Count)
{
return false; // برخی آیتم‌ها پیدا نشدند
}
var stockMovements = new List<StockMovement>();
foreach (var update in updates)
{
var item = items.First(i => i.Id == update.InventoryItemId);
// بررسی موجودی کافی
if (update.QuantityChange < 0 && item.Quantity + update.QuantityChange < 0)
{
return false;
}
item.Quantity += update.QuantityChange;
if (movementType == StockMovementType.Sale)
{
item.LastSoldAt = DateTime.UtcNow;
}
else if (movementType == StockMovementType.Restock || movementType == StockMovementType.InitialStock)
{
item.LastRestockedAt = DateTime.UtcNow;
}
stockMovements.Add(new StockMovement
{
InventoryItemId = update.InventoryItemId,
MovementType = movementType,
Quantity = Math.Abs(update.QuantityChange),
Note = update.Note,
ReferenceNumber = referenceNumber,
PerformedByUserId = performedByUserId
});
}
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
public async Task<bool> BulkReserveQuantityAsync(
List<(long InventoryItemId, int Quantity, string? Note)> reservations,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var inventoryItemIds = reservations.Select(r => r.InventoryItemId).ToList();
var items = await _context.InventoryItems
.Where(i => inventoryItemIds.Contains(i.Id))
.ToListAsync(cancellationToken);
if (items.Count != reservations.Count)
{
return false;
}
var stockMovements = new List<StockMovement>();
foreach (var reservation in reservations)
{
var item = items.First(i => i.Id == reservation.InventoryItemId);
// بررسی موجودی قابل دسترس
if (item.AvailableQuantity < reservation.Quantity)
{
return false;
}
item.ReservedQuantity += reservation.Quantity;
stockMovements.Add(new StockMovement
{
InventoryItemId = reservation.InventoryItemId,
MovementType = StockMovementType.Reserved,
Quantity = reservation.Quantity,
Note = reservation.Note ?? "Bulk reservation",
ReferenceNumber = referenceNumber,
OrderId = orderId,
DiscountOrderId = discountOrderId,
PerformedByUserId = performedByUserId
});
}
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return true;
}
#endregion
}
@@ -1,430 +0,0 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت حرکات موجودی
/// </summary>
public class StockMovementRepository : IStockMovementRepository
{
private readonly IApplicationDbContext _context;
public StockMovementRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<StockMovement?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(m => m.Id == id, cancellationToken);
}
public async Task<List<StockMovement>> GetByInventoryItemIdAsync(
long inventoryItemId,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.Where(m => m.InventoryItemId == inventoryItemId);
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByOrderIdAsync(long orderId, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.OrderId == orderId)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByDiscountOrderIdAsync(long discountOrderId, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.DiscountOrderId == discountOrderId)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByReferenceNumberAsync(string referenceNumber, CancellationToken cancellationToken = default)
{
return await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.ReferenceNumber == referenceNumber)
.OrderByDescending(m => m.Created)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetByMovementTypeAsync(
StockMovementType movementType,
DateTime? fromDate = null,
DateTime? toDate = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.MovementType == movementType);
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> GetRecentMovementsAsync(
int count = 50,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.AsQueryable();
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Take(count)
.ToListAsync(cancellationToken);
}
public async Task<List<StockMovement>> SearchAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
int skip = 0,
int take = 100,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.AsQueryable();
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
if (!string.IsNullOrWhiteSpace(referenceNumber))
{
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
}
if (orderId.HasValue)
{
query = query.Where(m => m.OrderId == orderId.Value);
}
if (discountOrderId.HasValue)
{
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
}
if (performedByUserId.HasValue)
{
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
}
return await query
.OrderByDescending(m => m.Created)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
long? inventoryItemId = null,
StockMovementType? movementType = null,
DateTime? fromDate = null,
DateTime? toDate = null,
string? referenceNumber = null,
long? orderId = null,
long? discountOrderId = null,
long? performedByUserId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements.AsQueryable();
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
if (fromDate.HasValue)
{
query = query.Where(m => m.Created >= fromDate.Value);
}
if (toDate.HasValue)
{
query = query.Where(m => m.Created <= toDate.Value);
}
if (!string.IsNullOrWhiteSpace(referenceNumber))
{
query = query.Where(m => m.ReferenceNumber != null && m.ReferenceNumber.Contains(referenceNumber));
}
if (orderId.HasValue)
{
query = query.Where(m => m.OrderId == orderId.Value);
}
if (discountOrderId.HasValue)
{
query = query.Where(m => m.DiscountOrderId == discountOrderId.Value);
}
if (performedByUserId.HasValue)
{
query = query.Where(m => m.PerformedByUserId == performedByUserId.Value);
}
return await query.CountAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<StockMovement> AddAsync(StockMovement stockMovement, CancellationToken cancellationToken = default)
{
_context.StockMovements.Add(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
return stockMovement;
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var stockMovement = await _context.StockMovements.FindAsync(new object[] { id }, cancellationToken);
if (stockMovement != null)
{
_context.StockMovements.Remove(stockMovement);
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task<List<StockMovement>> BulkAddAsync(List<StockMovement> stockMovements, CancellationToken cancellationToken = default)
{
_context.StockMovements.AddRange(stockMovements);
await _context.SaveChangesAsync(cancellationToken);
return stockMovements;
}
#endregion
#region Analytics & Reports
public async Task<Dictionary<StockMovementType, int>> GetMovementSummaryAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
var movements = await query
.GroupBy(m => m.MovementType)
.Select(g => new { MovementType = g.Key, TotalQuantity = g.Sum(m => m.Quantity) })
.ToListAsync(cancellationToken);
return movements.ToDictionary(x => x.MovementType, x => x.TotalQuantity);
}
public async Task<List<(DateTime Date, int InboundQuantity, int OutboundQuantity)>> GetDailyMovementVolumeAsync(
DateTime fromDate,
DateTime toDate,
long? inventoryItemId = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (inventoryItemId.HasValue)
{
query = query.Where(m => m.InventoryItemId == inventoryItemId.Value);
}
var movements = await query.ToListAsync(cancellationToken);
// نوع‌های ورودی (افزایش موجودی)
var inboundTypes = new[]
{
StockMovementType.InitialStock,
StockMovementType.Restock,
StockMovementType.Return,
StockMovementType.TransferIn,
StockMovementType.AdjustmentPlus,
StockMovementType.Released
};
// نوع‌های خروجی (کاهش موجودی)
var outboundTypes = new[]
{
StockMovementType.Sale,
StockMovementType.Damaged,
StockMovementType.Lost,
StockMovementType.TransferOut,
StockMovementType.AdjustmentMinus,
StockMovementType.Reserved
};
var dailyVolumes = movements
.GroupBy(m => m.Created.Date)
.Select(g => (
Date: g.Key,
InboundQuantity: g.Where(m => inboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity),
OutboundQuantity: g.Where(m => outboundTypes.Contains(m.MovementType)).Sum(m => m.Quantity)
))
.OrderBy(x => x.Date)
.ToList();
return dailyVolumes;
}
public async Task<List<(long InventoryItemId, string ProductName, int MovementCount, int TotalQuantityChange)>> GetTopMovingProductsAsync(
DateTime fromDate,
DateTime toDate,
int count = 10,
StockMovementType? movementType = null,
CancellationToken cancellationToken = default)
{
var query = _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.Created >= fromDate && m.Created <= toDate);
if (movementType.HasValue)
{
query = query.Where(m => m.MovementType == movementType.Value);
}
var movements = await query.ToListAsync(cancellationToken);
var topProducts = movements
.GroupBy(m => m.InventoryItemId)
.Select(g =>
{
var firstItem = g.First().InventoryItem;
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
return (
InventoryItemId: g.Key,
ProductName: productName,
MovementCount: g.Count(),
TotalQuantityChange: g.Sum(m => m.Quantity)
);
})
.OrderByDescending(x => x.MovementCount)
.Take(count)
.ToList();
return topProducts;
}
#endregion
}
@@ -1,309 +0,0 @@
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Application.Common.Interfaces.Repositories;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Infrastructure.Persistence.Repositories;
/// <summary>
/// Repository implementation برای مدیریت انبارها
/// </summary>
public class WarehouseRepository : IWarehouseRepository
{
private readonly IApplicationDbContext _context;
public WarehouseRepository(IApplicationDbContext context)
{
_context = context;
}
#region Read Operations
public async Task<Warehouse?> GetByIdAsync(long id, CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.Id == id, cancellationToken);
}
public async Task<Warehouse?> GetByCodeAsync(string code, CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.Code == code, cancellationToken);
}
public async Task<Warehouse?> GetDefaultWarehouseAsync(CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Include(w => w.InventoryItems)
.ThenInclude(i => i.Product)
.Include(w => w.InventoryItems)
.ThenInclude(i => i.DiscountProduct)
.FirstOrDefaultAsync(w => w.IsDefault, cancellationToken);
}
public async Task<List<Warehouse>> GetActiveWarehousesAsync(CancellationToken cancellationToken = default)
{
return await _context.Warehouses
.Where(w => w.IsActive)
.OrderBy(w => w.Name)
.ToListAsync(cancellationToken);
}
public async Task<List<Warehouse>> GetAllAsync(
bool includeInactive = false,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!includeInactive)
{
query = query.Where(w => w.IsActive);
}
return await query
.OrderBy(w => w.Name)
.ToListAsync(cancellationToken);
}
public async Task<List<Warehouse>> SearchAsync(
string? searchTerm = null,
bool? isActive = null,
int skip = 0,
int take = 50,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(w =>
w.Name.ToLower().Contains(term) ||
w.Code.ToLower().Contains(term) ||
(w.Address != null && w.Address.ToLower().Contains(term)));
}
if (isActive.HasValue)
{
query = query.Where(w => w.IsActive == isActive.Value);
}
return await query
.OrderBy(w => w.Name)
.Skip(skip)
.Take(take)
.ToListAsync(cancellationToken);
}
public async Task<int> CountAsync(
string? searchTerm = null,
bool? isActive = null,
CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.AsQueryable();
if (!string.IsNullOrWhiteSpace(searchTerm))
{
var term = searchTerm.ToLower();
query = query.Where(w =>
w.Name.ToLower().Contains(term) ||
w.Code.ToLower().Contains(term) ||
(w.Address != null && w.Address.ToLower().Contains(term)));
}
if (isActive.HasValue)
{
query = query.Where(w => w.IsActive == isActive.Value);
}
return await query.CountAsync(cancellationToken);
}
public async Task<bool> ExistsByCodeAsync(string code, long? excludeId = null, CancellationToken cancellationToken = default)
{
var query = _context.Warehouses.Where(w => w.Code == code);
if (excludeId.HasValue)
{
query = query.Where(w => w.Id != excludeId.Value);
}
return await query.AnyAsync(cancellationToken);
}
#endregion
#region Write Operations
public async Task<Warehouse> AddAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
{
// اگر این انبار پیش‌فرض است، سایر انبارها را غیرپیش‌فرض کن
if (warehouse.IsDefault)
{
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
}
_context.Warehouses.Add(warehouse);
await _context.SaveChangesAsync(cancellationToken);
return warehouse;
}
public async Task UpdateAsync(Warehouse warehouse, CancellationToken cancellationToken = default)
{
// اگر این انبار پیش‌فرض شده، سایر انبارها را غیرپیش‌فرض کن
if (warehouse.IsDefault)
{
await RemoveDefaultFromAllWarehousesAsync(warehouse.Id, cancellationToken);
}
_context.Warehouses.Update(warehouse);
await _context.SaveChangesAsync(cancellationToken);
}
public async Task DeleteAsync(long id, CancellationToken cancellationToken = default)
{
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsActive = false;
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task SetActiveStatusAsync(long id, bool isActive, CancellationToken cancellationToken = default)
{
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsActive = isActive;
await _context.SaveChangesAsync(cancellationToken);
}
}
public async Task SetAsDefaultAsync(long id, CancellationToken cancellationToken = default)
{
// ابتدا همه انبارها را غیرپیش‌فرض کن
await RemoveDefaultFromAllWarehousesAsync(cancellationToken);
// سپس انبار مورد نظر را پیش‌فرض کن
var warehouse = await _context.Warehouses.FindAsync(new object[] { id }, cancellationToken);
if (warehouse != null)
{
warehouse.IsDefault = true;
await _context.SaveChangesAsync(cancellationToken);
}
}
#endregion
#region Analytics
public async Task<(int TotalProducts, int LowStockProducts, int OutOfStockProducts, decimal TotalValue)> GetWarehouseStatisticsAsync(
long warehouseId,
CancellationToken cancellationToken = default)
{
var inventoryItems = await _context.InventoryItems
.Include(i => i.Product)
.Include(i => i.DiscountProduct)
.Where(i => i.WarehouseId == warehouseId)
.ToListAsync(cancellationToken);
var totalProducts = inventoryItems.Count;
var lowStockProducts = inventoryItems.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0);
var outOfStockProducts = inventoryItems.Count(i => i.Quantity == 0);
// محاسبه ارزش کل بر اساس قیمت محصولات
decimal totalValue = 0;
foreach (var item in inventoryItems)
{
if (item.Product != null)
{
totalValue += item.Quantity * item.Product.Price;
}
else if (item.DiscountProduct != null)
{
totalValue += item.Quantity * item.DiscountProduct.Price;
}
}
return (totalProducts, lowStockProducts, outOfStockProducts, totalValue);
}
public async Task<List<(long ProductId, string ProductName, int TotalSold, int CurrentStock)>> GetTopSellingProductsAsync(
long warehouseId,
DateTime fromDate,
DateTime toDate,
int count = 10,
CancellationToken cancellationToken = default)
{
// دریافت حرکات فروش برای این انبار در بازه زمانی مشخص
var salesMovements = await _context.StockMovements
.Include(m => m.InventoryItem)
.ThenInclude(i => i.Product)
.Include(m => m.InventoryItem)
.ThenInclude(i => i.DiscountProduct)
.Where(m => m.InventoryItem.WarehouseId == warehouseId &&
m.MovementType == StockMovementType.Sale &&
m.Created >= fromDate &&
m.Created <= toDate)
.ToListAsync(cancellationToken);
// گروه‌بندی بر اساس محصول و محاسبه تعداد فروش
var topProducts = salesMovements
.GroupBy(m => m.InventoryItemId)
.Select(g =>
{
var firstItem = g.First().InventoryItem;
var productId = firstItem.ProductId ?? firstItem.DiscountProductId ?? 0;
var productName = firstItem.Product?.Title ?? firstItem.DiscountProduct?.Title ?? "Unknown";
var totalSold = g.Sum(m => m.Quantity);
var currentStock = firstItem.Quantity;
return (ProductId: productId, ProductName: productName, TotalSold: totalSold, CurrentStock: currentStock);
})
.OrderByDescending(x => x.TotalSold)
.Take(count)
.ToList();
return topProducts;
}
#endregion
#region Private Methods
private async Task RemoveDefaultFromAllWarehousesAsync(CancellationToken cancellationToken = default)
{
var defaultWarehouses = await _context.Warehouses
.Where(w => w.IsDefault)
.ToListAsync(cancellationToken);
foreach (var warehouse in defaultWarehouses)
{
warehouse.IsDefault = false;
}
}
private async Task RemoveDefaultFromAllWarehousesAsync(long excludeId, CancellationToken cancellationToken = default)
{
var defaultWarehouses = await _context.Warehouses
.Where(w => w.IsDefault && w.Id != excludeId)
.ToListAsync(cancellationToken);
foreach (var warehouse in defaultWarehouses)
{
warehouse.IsDefault = false;
}
}
#endregion
}