Implement Inventory Management Service with CRUD operations for warehouses and inventory items, stock operations, and bulk processing capabilities.
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m48s

This commit is contained in:
masoodafar-web
2026-01-02 00:45:37 +03:30
parent f0117eb1d5
commit 1cf501711f
50 changed files with 10699 additions and 101 deletions
@@ -0,0 +1,662 @@
using System.Collections.Generic;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Entities;
using CMSMicroservice.Domain.Enums;
using CMSMicroservice.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Infrastructure.Services;
/// <summary>
/// پیاده‌سازی سرویس مدیریت موجودی
/// این سرویس Source of Truth برای موجودی است و مسئول همگام‌سازی با Product.RemainingCount
/// </summary>
public class InventoryService : IInventoryService
{
private readonly ApplicationDbContext _context;
private readonly ILogger<InventoryService> _logger;
private const long DefaultWarehouseId = 1; // انبار پیش‌فرض
public InventoryService(ApplicationDbContext context, ILogger<InventoryService> logger)
{
_context = context;
_logger = logger;
}
#region Initialization
public async Task<long> InitializeInventoryAsync(
long productId,
ProductType productType,
int initialQuantity,
long? warehouseId = null,
int lowStockThreshold = 10,
CancellationToken ct = default)
{
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
// چک کردن اینکه آیا قبلاً InventoryItem برای این محصول وجود دارد
var existingItem = productType == ProductType.RegularProduct
? await _context.InventoryItems.FirstOrDefaultAsync(
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
: await _context.InventoryItems.FirstOrDefaultAsync(
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
if (existingItem != null)
{
_logger.LogWarning("InventoryItem already exists for {ProductType} with Id {ProductId}",
productType, productId);
return existingItem.Id;
}
// ایجاد InventoryItem جدید
var inventoryItem = new InventoryItem
{
ProductId = productType == ProductType.RegularProduct ? productId : null,
DiscountProductId = productType == ProductType.DiscountProduct ? productId : null,
ProductType = productType,
Quantity = initialQuantity,
ReservedQuantity = 0,
LowStockThreshold = lowStockThreshold,
WarehouseId = effectiveWarehouseId
};
_context.InventoryItems.Add(inventoryItem);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement اولیه
if (initialQuantity > 0)
{
await LogMovementAsync(
inventoryItem.Id,
StockMovementType.InitialStock,
initialQuantity,
0,
initialQuantity,
null,
null,
null,
"موجودی اولیه",
null,
ct);
}
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(inventoryItem, ct);
_logger.LogInformation(
"Initialized inventory for {ProductType} Id={ProductId}, Quantity={Quantity}",
productType, productId, initialQuantity);
return inventoryItem.Id;
}
#endregion
#region Query Operations
public async Task<InventoryItem?> GetInventoryAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default)
{
var effectiveWarehouseId = warehouseId ?? DefaultWarehouseId;
return productType == ProductType.RegularProduct
? await _context.InventoryItems.FirstOrDefaultAsync(
x => x.ProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct)
: await _context.InventoryItems.FirstOrDefaultAsync(
x => x.DiscountProductId == productId && x.WarehouseId == effectiveWarehouseId && !x.IsDeleted, ct);
}
public async Task<int> GetAvailableQuantityAsync(
long productId,
ProductType productType,
long? warehouseId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, warehouseId, ct);
return item?.AvailableQuantity ?? 0;
}
public async Task<bool> CheckAvailabilityAsync(
long productId,
ProductType productType,
int requiredQuantity,
long? warehouseId = null,
CancellationToken ct = default)
{
var available = await GetAvailableQuantityAsync(productId, productType, warehouseId, ct);
return available >= requiredQuantity;
}
public async Task<List<InventoryItem>> GetLowStockItemsAsync(
ProductType? productType = null,
long? warehouseId = null,
int count = 50,
CancellationToken ct = default)
{
var query = _context.InventoryItems
.Where(x => !x.IsDeleted)
.Where(x => x.Quantity <= x.LowStockThreshold);
if (productType.HasValue)
query = query.Where(x => x.ProductType == productType.Value);
if (warehouseId.HasValue)
query = query.Where(x => x.WarehouseId == warehouseId.Value);
return await query
.OrderBy(x => x.Quantity)
.Take(count)
.ToListAsync(ct);
}
public async Task<List<StockMovement>> GetStockMovementsAsync(
long productId,
ProductType productType,
DateTime? fromDate = null,
DateTime? toDate = null,
CancellationToken ct = default)
{
var inventoryItem = await GetInventoryAsync(productId, productType, null, ct);
if (inventoryItem == null)
return new List<StockMovement>();
var query = _context.StockMovements
.Where(x => x.InventoryItemId == inventoryItem.Id && !x.IsDeleted);
if (fromDate.HasValue)
query = query.Where(x => x.Created >= fromDate.Value);
if (toDate.HasValue)
query = query.Where(x => x.Created <= toDate.Value);
return await query
.OrderByDescending(x => x.Created)
.ToListAsync(ct);
}
#endregion
#region Order Flow Operations
public async Task<bool> ReserveStockAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot reserve: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
if (item.AvailableQuantity < quantity)
{
_logger.LogWarning(
"Cannot reserve: Insufficient stock. Available={Available}, Requested={Requested}",
item.AvailableQuantity, quantity);
return false;
}
var quantityBefore = item.ReservedQuantity;
item.ReservedQuantity += quantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Reserved,
quantity,
quantityBefore,
item.ReservedQuantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"رزرو برای سفارش",
null,
ct);
_logger.LogInformation(
"Reserved {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}",
quantity, productType, productId, orderId);
return true;
}
public async Task<bool> ReleaseReservationAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot release: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.ReservedQuantity;
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Released,
quantity,
quantityBefore,
item.ReservedQuantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"آزادسازی رزرو",
null,
ct);
_logger.LogInformation(
"Released {Quantity} reserved units for {ProductType} Id={ProductId}, OrderId={OrderId}",
quantity, productType, productId, orderId);
return true;
}
public async Task<bool> ConfirmSaleAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot confirm sale: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
// کاهش موجودی واقعی
item.Quantity -= quantity;
// کاهش رزرو (اگر رزرو شده بود)
item.ReservedQuantity = Math.Max(0, item.ReservedQuantity - quantity);
// آپدیت آخرین فروش
item.LastSoldAt = DateTime.UtcNow;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Sale,
-quantity, // منفی برای خروج
quantityBefore,
item.Quantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
"فروش",
null,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Confirmed sale of {Quantity} units for {ProductType} Id={ProductId}, OrderId={OrderId}. New Quantity={NewQuantity}",
quantity, productType, productId, orderId, item.Quantity);
return true;
}
#endregion
#region Stock Management Operations
public async Task<bool> AddStockAsync(
long productId,
ProductType productType,
int quantity,
string? referenceNumber = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot add stock: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity += quantity;
item.LastRestockedAt = DateTime.UtcNow;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Restock,
quantity,
quantityBefore,
item.Quantity,
null,
null,
referenceNumber,
note ?? "ورود کالا",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Added {Quantity} units to {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
quantity, productType, productId, item.Quantity);
return true;
}
public async Task<bool> AdjustStockAsync(
long productId,
ProductType productType,
int newQuantity,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot adjust stock: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
var difference = newQuantity - quantityBefore;
item.Quantity = newQuantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
var movementType = difference >= 0
? StockMovementType.AdjustmentPlus
: StockMovementType.AdjustmentMinus;
await LogMovementAsync(
item.Id,
movementType,
difference,
quantityBefore,
newQuantity,
null,
null,
null,
note ?? "تعدیل موجودی",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Adjusted stock for {ProductType} Id={ProductId}. Before={Before}, After={After}",
productType, productId, quantityBefore, newQuantity);
return true;
}
public async Task<bool> ProcessReturnAsync(
long productId,
ProductType productType,
int quantity,
long? orderId = null,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot process return: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity += quantity;
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
StockMovementType.Return,
quantity,
quantityBefore,
item.Quantity,
orderId,
productType == ProductType.DiscountProduct ? orderId : null,
null,
note ?? "برگشت از مشتری",
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Processed return of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
quantity, productType, productId, item.Quantity);
return true;
}
public async Task<bool> RecordLossAsync(
long productId,
ProductType productType,
int quantity,
StockMovementType lossType,
string? note = null,
long? performedByUserId = null,
CancellationToken ct = default)
{
if (lossType != StockMovementType.Damaged && lossType != StockMovementType.Lost)
{
_logger.LogWarning("Invalid loss type: {LossType}. Must be Damaged or Lost.", lossType);
return false;
}
var item = await GetInventoryAsync(productId, productType, null, ct);
if (item == null)
{
_logger.LogWarning("Cannot record loss: InventoryItem not found for {ProductType} Id={ProductId}",
productType, productId);
return false;
}
var quantityBefore = item.Quantity;
item.Quantity = Math.Max(0, item.Quantity - quantity);
await _context.SaveChangesAsync(ct);
// ثبت StockMovement
await LogMovementAsync(
item.Id,
lossType,
-quantity,
quantityBefore,
item.Quantity,
null,
null,
null,
note ?? (lossType == StockMovementType.Damaged ? "ضایعات" : "مفقودی"),
performedByUserId,
ct);
// همگام‌سازی با Product.RemainingCount
await SyncRemainingCountAsync(item, ct);
_logger.LogInformation(
"Recorded {LossType} of {Quantity} units for {ProductType} Id={ProductId}. New Quantity={NewQuantity}",
lossType, quantity, productType, productId, item.Quantity);
return true;
}
#endregion
#region Bulk Operations
public async Task<bool> BulkReserveStockAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
var result = await ReserveStockAsync(productId, productType, quantity, orderId, ct);
if (!result)
{
// در صورت خطا، رزروهای قبلی را آزاد کنید
_logger.LogError(
"Bulk reserve failed at {ProductType} Id={ProductId}. Rolling back...",
productType, productId);
// TODO: Implement rollback logic
return false;
}
}
return true;
}
public async Task<bool> BulkReleaseReservationAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
await ReleaseReservationAsync(productId, productType, quantity, orderId, ct);
}
return true;
}
public async Task<bool> BulkConfirmSaleAsync(
IEnumerable<(long ProductId, ProductType ProductType, int Quantity)> items,
long? orderId = null,
CancellationToken ct = default)
{
foreach (var (productId, productType, quantity) in items)
{
var result = await ConfirmSaleAsync(productId, productType, quantity, orderId, ct);
if (!result)
{
_logger.LogError(
"Bulk confirm sale failed at {ProductType} Id={ProductId}",
productType, productId);
return false;
}
}
return true;
}
#endregion
#region Private Helper Methods
/// <summary>
/// همگام‌سازی موجودی InventoryItem با Product.RemainingCount
/// این متد اطمینان می‌دهد که داده‌های قدیمی (RemainingCount) همیشه با سیستم جدید sync است
/// </summary>
private async Task SyncRemainingCountAsync(InventoryItem item, CancellationToken ct)
{
try
{
if (item.ProductType == ProductType.RegularProduct && item.ProductId.HasValue)
{
var product = await _context.Products.FindAsync(new object[] { item.ProductId.Value }, ct);
if (product != null)
{
product.RemainingCount = item.Quantity;
await _context.SaveChangesAsync(ct);
_logger.LogDebug("Synced RemainingCount for Product Id={ProductId} to {Quantity}",
item.ProductId, item.Quantity);
}
}
else if (item.ProductType == ProductType.DiscountProduct && item.DiscountProductId.HasValue)
{
var discountProduct = await _context.DiscountProducts.FindAsync(
new object[] { item.DiscountProductId.Value }, ct);
if (discountProduct != null)
{
discountProduct.RemainingCount = item.Quantity;
await _context.SaveChangesAsync(ct);
_logger.LogDebug("Synced RemainingCount for DiscountProduct Id={ProductId} to {Quantity}",
item.DiscountProductId, item.Quantity);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to sync RemainingCount for InventoryItem Id={ItemId}", item.Id);
// Don't throw - sync failure shouldn't break the main operation
}
}
/// <summary>
/// ثبت حرکت موجودی در StockMovements
/// </summary>
private async Task LogMovementAsync(
long inventoryItemId,
StockMovementType movementType,
int quantity,
int quantityBefore,
int quantityAfter,
long? orderId,
long? discountOrderId,
string? referenceNumber,
string? note,
long? performedByUserId,
CancellationToken ct)
{
var movement = new StockMovement
{
InventoryItemId = inventoryItemId,
MovementType = movementType,
Quantity = quantity,
QuantityBefore = quantityBefore,
QuantityAfter = quantityAfter,
OrderId = orderId,
DiscountOrderId = discountOrderId,
ReferenceNumber = referenceNumber,
Note = note,
PerformedByUserId = performedByUserId
};
_context.StockMovements.Add(movement);
await _context.SaveChangesAsync(ct);
}
#endregion
}