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
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 1m48s
This commit is contained in:
+430
@@ -0,0 +1,430 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user