477 lines
20 KiB
C#
477 lines
20 KiB
C#
using CMSMicroservice.Protobuf.Protos.Inventory;
|
|
using CMSMicroservice.WebApi.Common.Services;
|
|
using CMSMicroservice.Application.WarehouseCQ.Commands.CreateWarehouse;
|
|
using CMSMicroservice.Application.WarehouseCQ.Commands.UpdateWarehouse;
|
|
using CMSMicroservice.Application.WarehouseCQ.Commands.DeleteWarehouse;
|
|
using CMSMicroservice.Application.WarehouseCQ.Commands.SetDefaultWarehouse;
|
|
using CMSMicroservice.Application.WarehouseCQ.Queries.GetWarehouse;
|
|
using CMSMicroservice.Application.WarehouseCQ.Queries.GetAllWarehouses;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Commands.UpdateInventoryItem;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Commands.IncreaseInventory;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReduceInventory;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReserveInventory;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Commands.ReleaseReservedInventory;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryItem;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetInventoryByProduct;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetAllInventoryItems;
|
|
using CMSMicroservice.Application.InventoryItemCQ.Queries.GetLowStockItems;
|
|
using CMSMicroservice.Application.StockMovementCQ.Commands.CreateStockMovement;
|
|
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovements;
|
|
using CMSMicroservice.Application.StockMovementCQ.Queries.GetStockMovementsByInventoryItem;
|
|
using CMSMicroservice.Application.Common.Interfaces;
|
|
using Google.Protobuf.WellKnownTypes;
|
|
using Grpc.Core;
|
|
using MediatR;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using System.Linq;
|
|
|
|
namespace CMSMicroservice.WebApi.Services;
|
|
|
|
public class InventoryService : InventoryContract.InventoryContractBase
|
|
{
|
|
private readonly IDispatchRequestToCQRS _dispatchRequestToCQRS;
|
|
private readonly IMediator _mediator;
|
|
private readonly IApplicationDbContext _context;
|
|
|
|
public InventoryService(IDispatchRequestToCQRS dispatchRequestToCQRS, IMediator mediator, IApplicationDbContext context)
|
|
{
|
|
_dispatchRequestToCQRS = dispatchRequestToCQRS;
|
|
_mediator = mediator;
|
|
_context = context;
|
|
}
|
|
|
|
// ========== Warehouse Management ==========
|
|
|
|
public override async Task<CreateWarehouseResponse> CreateWarehouse(CreateWarehouseRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<CreateWarehouseRequest, CreateWarehouseCommand, CreateWarehouseResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<Empty> UpdateWarehouse(UpdateWarehouseRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<UpdateWarehouseRequest, UpdateWarehouseCommand>(request, context);
|
|
}
|
|
|
|
public override async Task<Empty> DeleteWarehouse(DeleteWarehouseRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<DeleteWarehouseRequest, DeleteWarehouseCommand>(request, context);
|
|
}
|
|
|
|
public override async Task<GetWarehouseResponse> GetWarehouse(GetWarehouseRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetWarehouseRequest, GetWarehouseQuery, GetWarehouseResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<GetAllWarehousesResponse> GetAllWarehouses(GetAllWarehousesRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetAllWarehousesRequest, GetAllWarehousesQuery, GetAllWarehousesResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<Empty> SetDefaultWarehouse(SetDefaultWarehouseRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<SetDefaultWarehouseRequest, SetDefaultWarehouseCommand>(request, context);
|
|
}
|
|
|
|
// ========== Inventory Item Management ==========
|
|
|
|
public override async Task<GetInventoryItemResponse> GetInventoryItem(GetInventoryItemRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetInventoryItemRequest, GetInventoryItemQuery, GetInventoryItemResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<GetInventoryByProductResponse> GetInventoryByProduct(GetInventoryByProductRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetInventoryByProductRequest, GetInventoryByProductQuery, GetInventoryByProductResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<GetAllInventoryItemsResponse> GetAllInventoryItems(GetAllInventoryItemsRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetAllInventoryItemsRequest, GetAllInventoryItemsQuery, GetAllInventoryItemsResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<GetLowStockItemsResponse> GetLowStockItems(GetLowStockItemsRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetLowStockItemsRequest, GetLowStockItemsQuery, GetLowStockItemsResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<Empty> UpdateInventorySettings(UpdateInventorySettingsRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<UpdateInventorySettingsRequest, UpdateInventoryItemCommand>(request, context);
|
|
}
|
|
|
|
// ========== Stock Operations ==========
|
|
|
|
public override async Task<AddStockResponse> AddStock(AddStockRequest request, ServerCallContext context)
|
|
{
|
|
// Lookup InventoryItem by ProductId + ProductType
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
{
|
|
throw new RpcException(new Status(StatusCode.NotFound,
|
|
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
|
|
}
|
|
|
|
// Execute IncreaseInventoryCommand
|
|
var response = await _mediator.Send(
|
|
new IncreaseInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
ReferenceNumber = request.ReferenceNumber
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new AddStockResponse
|
|
{
|
|
InventoryItemId = inventoryItem.Id,
|
|
NewQuantity = response.NewQuantity
|
|
};
|
|
}
|
|
|
|
public override async Task<AdjustStockResponse> AdjustStock(AdjustStockRequest request, ServerCallContext context)
|
|
{
|
|
// Lookup InventoryItem
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
{
|
|
throw new RpcException(new Status(StatusCode.NotFound,
|
|
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
|
|
}
|
|
|
|
// Determine if increase or decrease
|
|
var difference = request.NewQuantity - inventoryItem.Quantity;
|
|
|
|
if (difference > 0)
|
|
{
|
|
await _mediator.Send(
|
|
new IncreaseInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = difference,
|
|
ReferenceNumber = request.ReferenceNumber
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new AdjustStockResponse
|
|
{
|
|
PreviousQuantity = inventoryItem.Quantity,
|
|
NewQuantity = request.NewQuantity,
|
|
Difference = difference
|
|
};
|
|
}
|
|
else if (difference < 0)
|
|
{
|
|
await _mediator.Send(
|
|
new ReduceInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = Math.Abs(difference),
|
|
FromReserved = false,
|
|
ReferenceNumber = request.ReferenceNumber,
|
|
MovementType = Domain.Enums.StockMovementType.AdjustmentMinus,
|
|
Note = "Stock adjustment (decrease)"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new AdjustStockResponse
|
|
{
|
|
PreviousQuantity = inventoryItem.Quantity,
|
|
NewQuantity = request.NewQuantity,
|
|
Difference = difference
|
|
};
|
|
}
|
|
else
|
|
{
|
|
return new AdjustStockResponse
|
|
{
|
|
PreviousQuantity = inventoryItem.Quantity,
|
|
NewQuantity = inventoryItem.Quantity,
|
|
Difference = 0
|
|
};
|
|
}
|
|
}
|
|
|
|
public override async Task<ReserveStockResponse> ReserveStock(ReserveStockRequest request, ServerCallContext context)
|
|
{
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
return new ReserveStockResponse { Success = false, Message = "آیتم موجودی یافت نشد", AvailableQuantity = 0 };
|
|
|
|
var available = inventoryItem.Quantity - inventoryItem.ReservedQuantity;
|
|
if (available < request.Quantity)
|
|
return new ReserveStockResponse { Success = false, Message = $"موجودی کافی نیست. موجود: {available}", AvailableQuantity = available };
|
|
|
|
await _mediator.Send(
|
|
new ReserveInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
ReferenceNumber = request.OrderId != null ? $"ORDER-{request.OrderId.Value}" : $"RESERVE-{DateTime.UtcNow.Ticks}"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new ReserveStockResponse { Success = true, Message = "رزرو با موفقیت انجام شد", AvailableQuantity = available - request.Quantity };
|
|
}
|
|
|
|
public override async Task<Empty> ReleaseReservation(ReleaseReservationRequest request, ServerCallContext context)
|
|
{
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
|
|
|
await _mediator.Send(
|
|
new ReleaseReservedInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
ReferenceNumber = request.OrderId != null ? $"RELEASE-ORDER-{request.OrderId.Value}" : $"RELEASE-{DateTime.UtcNow.Ticks}"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new Empty();
|
|
}
|
|
|
|
public override async Task<Empty> ConfirmSale(ConfirmSaleRequest request, ServerCallContext context)
|
|
{
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
|
|
|
await _mediator.Send(
|
|
new ReduceInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
FromReserved = request.FromReservation,
|
|
ReferenceNumber = request.OrderId != null ? $"SALE-ORDER-{request.OrderId.Value}" : $"SALE-{DateTime.UtcNow.Ticks}"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new Empty();
|
|
}
|
|
|
|
public override async Task<ProcessReturnResponse> ProcessReturn(ProcessReturnRequest request, ServerCallContext context)
|
|
{
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
throw new RpcException(new Status(StatusCode.NotFound, "آیتم موجودی یافت نشد"));
|
|
|
|
var result = await _mediator.Send(
|
|
new IncreaseInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
ReferenceNumber = request.OrderId != null ? $"RETURN-ORDER-{request.OrderId.Value}" : $"RETURN-{DateTime.UtcNow.Ticks}"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new ProcessReturnResponse { NewQuantity = result.NewQuantity };
|
|
}
|
|
|
|
public override async Task<Empty> RecordLoss(RecordLossRequest request, ServerCallContext context)
|
|
{
|
|
// Lookup InventoryItem by ProductId + ProductType
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = request.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)request.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
{
|
|
throw new RpcException(new Status(StatusCode.NotFound,
|
|
$"Inventory item not found for ProductId={request.ProductId}, ProductType={request.ProductType}"));
|
|
}
|
|
|
|
// Execute ReduceInventoryCommand to record the loss
|
|
await _mediator.Send(
|
|
new ReduceInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = request.Quantity,
|
|
FromReserved = false,
|
|
ReferenceNumber = request.ReferenceNumber ?? $"LOSS-{DateTime.UtcNow:yyyyMMddHHmmss}",
|
|
MovementType = (Domain.Enums.StockMovementType)request.LossType,
|
|
Note = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason
|
|
},
|
|
context.CancellationToken);
|
|
|
|
return new Empty();
|
|
}
|
|
|
|
// ========== Bulk Operations ==========
|
|
|
|
public override async Task<BulkAddStockResponse> BulkAddStock(BulkAddStockRequest request, ServerCallContext context)
|
|
{
|
|
var response = new BulkAddStockResponse();
|
|
|
|
foreach (var item in request.Items)
|
|
{
|
|
try
|
|
{
|
|
var inventoryItem = await _mediator.Send(
|
|
new GetInventoryByProductQuery
|
|
{
|
|
ProductId = item.ProductId,
|
|
ProductType = (Domain.Enums.ProductType)item.ProductType
|
|
},
|
|
context.CancellationToken);
|
|
|
|
if (inventoryItem == null)
|
|
{
|
|
response.FailedCount++;
|
|
response.Errors.Add($"ProductId={item.ProductId}: آیتم موجودی یافت نشد");
|
|
continue;
|
|
}
|
|
|
|
await _mediator.Send(
|
|
new IncreaseInventoryCommand
|
|
{
|
|
Id = inventoryItem.Id,
|
|
Quantity = item.Quantity,
|
|
ReferenceNumber = request.ReferenceNumber ?? $"BULK-{DateTime.UtcNow.Ticks}"
|
|
},
|
|
context.CancellationToken);
|
|
|
|
response.SuccessCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
response.FailedCount++;
|
|
response.Errors.Add($"ProductId={item.ProductId}: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
// ========== Stock Movements ==========
|
|
|
|
public override async Task<GetStockMovementsResponse> GetStockMovements(GetStockMovementsRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetStockMovementsRequest, GetStockMovementsQuery, GetStockMovementsResponse>(request, context);
|
|
}
|
|
|
|
public override async Task<GetStockMovementsByInventoryItemResponse> GetStockMovementsByInventoryItem(GetStockMovementsByInventoryItemRequest request, ServerCallContext context)
|
|
{
|
|
return await _dispatchRequestToCQRS.Handle<GetStockMovementsByInventoryItemRequest, GetStockMovementsByInventoryItemQuery, GetStockMovementsByInventoryItemResponse>(request, context);
|
|
}
|
|
|
|
// ========== Reports ==========
|
|
|
|
public override async Task<GetInventorySummaryResponse> GetInventorySummary(GetInventorySummaryRequest request, ServerCallContext context)
|
|
{
|
|
var query = _context.InventoryItems
|
|
.Include(i => i.Product)
|
|
.Include(i => i.DiscountProduct)
|
|
.Where(i => !i.IsDeleted);
|
|
|
|
if (request.WarehouseId != null)
|
|
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
|
|
|
|
var items = await query.ToListAsync(context.CancellationToken);
|
|
|
|
var regularProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.RegularProduct).ToList();
|
|
var discountProducts = items.Where(i => i.ProductType == Domain.Enums.ProductType.DiscountProduct).ToList();
|
|
|
|
long totalStockValue = items.Sum(i =>
|
|
{
|
|
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
|
|
return (long)i.Quantity * unitPrice;
|
|
});
|
|
|
|
return new GetInventorySummaryResponse
|
|
{
|
|
TotalProducts = regularProducts.Count,
|
|
TotalDiscountProducts = discountProducts.Count,
|
|
TotalQuantity = items.Sum(i => i.Quantity),
|
|
TotalReserved = items.Sum(i => i.ReservedQuantity),
|
|
LowStockCount = items.Count(i => i.Quantity <= i.LowStockThreshold && i.Quantity > 0),
|
|
OutOfStockCount = items.Count(i => i.Quantity == 0),
|
|
TotalStockValue = totalStockValue
|
|
};
|
|
}
|
|
|
|
public override async Task<GetStockValueReportResponse> GetStockValueReport(GetStockValueReportRequest request, ServerCallContext context)
|
|
{
|
|
var query = _context.InventoryItems
|
|
.Include(i => i.Product)
|
|
.Include(i => i.DiscountProduct)
|
|
.Where(i => !i.IsDeleted);
|
|
|
|
if (request.WarehouseId != null)
|
|
query = query.Where(i => i.WarehouseId == request.WarehouseId.Value);
|
|
|
|
if (request.ProductType != ProductType.Unspecified)
|
|
query = query.Where(i => i.ProductType == (Domain.Enums.ProductType)request.ProductType);
|
|
|
|
var dbItems = await query.ToListAsync(context.CancellationToken);
|
|
|
|
var items = dbItems.Select(i =>
|
|
{
|
|
long unitPrice = i.Product?.Price ?? i.DiscountProduct?.Price ?? 0;
|
|
string title = i.Product?.Title ?? i.DiscountProduct?.Title ?? string.Empty;
|
|
return new StockValueItem
|
|
{
|
|
ProductId = i.ProductId ?? i.DiscountProductId ?? 0,
|
|
ProductTitle = title,
|
|
ProductType = (ProductType)i.ProductType,
|
|
Quantity = i.Quantity,
|
|
UnitPrice = unitPrice,
|
|
TotalValue = (long)i.Quantity * unitPrice
|
|
};
|
|
}).ToList();
|
|
|
|
return new GetStockValueReportResponse
|
|
{
|
|
Items = { items },
|
|
TotalValue = items.Sum(i => i.TotalValue),
|
|
TotalItems = items.Count
|
|
};
|
|
}
|
|
}
|