Files
ERP_Distribution/Distribution/Features/Grn/GrnService.cs
T

138 lines
5.8 KiB
C#

using Distribution.Features.Po;
using Distribution.Features.Stock;
using Distribution.Infra.UoW;
using Distribution.Models;
namespace Distribution.Features.Grn;
public class GrnService(
IGrnRepository repo,
IPoRepository poRepo,
IStockRepository stockRepo,
IUnitOfWork uow)
{
private static GrnItemResponse MapItem(Models.GoodsReceiptItem i) =>
new(i.Id, i.FK_PurchaseOrderItemId, i.PurchaseOrderItem.FK_ItemId, i.PurchaseOrderItem.Item.Sku,
i.PurchaseOrderItem.Item.Name, i.AcceptedQuantity, i.RejectedQuantity);
private static GrnResponse Map(Models.GoodsReceipt gr) =>
new(gr.Id, gr.FK_PurchaseOrderId, gr.FK_ReceivedByUserId, gr.FK_WarehouseId, gr.Warehouse?.Name ?? string.Empty,
gr.ReceivedAt, gr.CreatedAt, gr.Items.Where(i => i.IsActive).Select(MapItem).ToList());
public async Task<GrnResponse?> GetByIdAsync(Guid id)
{
var gr = await repo.GetByIdAsync(id);
if (gr is null || !gr.IsActive) return null;
return Map(gr);
}
public async Task<List<GrnResponse>> GetByPurchaseOrderIdAsync(Guid purchaseOrderId) =>
(await repo.GetByPurchaseOrderIdAsync(purchaseOrderId)).Select(Map).ToList();
/// <summary>
/// Validates the Po status, increments PurchaseOrderItem.ReceivedQuantity, writes StockBalance/
/// StockMovement for AcceptedQuantity only (RejectedQuantity never touches stock), and
/// transitions the Po to PartiallyReceived/Received. All in one unit of work.
/// </summary>
public async Task<GrnResponse> CreateAsync(Guid purchaseOrderId, CreateGrnRequest request, Guid receivedByUserId, CancellationToken ct = default)
{
var po = await poRepo.GetByIdAsync(purchaseOrderId)
?? throw new InvalidOperationException("Purchase order not found.");
if (!po.IsActive)
throw new InvalidOperationException("Purchase order not found.");
if (po.Status is not (PurchaseOrderStatus.Issued or PurchaseOrderStatus.PartiallyReceived))
throw new InvalidOperationException(
$"Cannot receive goods against a purchase order in {po.Status} status.");
if (request.Items is null || request.Items.Count == 0)
throw new InvalidOperationException("A goods receipt must have at least one line item.");
var goodsReceipt = new Models.GoodsReceipt
{
FK_PurchaseOrderId = po.Id,
FK_ReceivedByUserId = receivedByUserId,
FK_WarehouseId = request.WarehouseId,
ReceivedAt = DateTime.Now
};
foreach (var line in request.Items)
{
var poItem = po.Items.FirstOrDefault(i => i.Id == line.PurchaseOrderItemId && i.IsActive)
?? throw new InvalidOperationException($"Purchase order line {line.PurchaseOrderItemId} not found on this Po.");
if (poItem.IsCancelled)
throw new InvalidOperationException($"Line for item {poItem.FK_ItemId} is cancelled and cannot receive stock.");
if (line.AcceptedQuantity < 0 || line.RejectedQuantity < 0)
throw new InvalidOperationException("Accepted/rejected quantities cannot be negative.");
if (line.AcceptedQuantity + line.RejectedQuantity + poItem.ReceivedQuantity > poItem.Quantity)
throw new InvalidOperationException(
$"Accepted + rejected quantity for item {poItem.FK_ItemId} would exceed the ordered quantity.");
poItem.ReceivedQuantity += line.AcceptedQuantity;
goodsReceipt.Items.Add(new Models.GoodsReceiptItem
{
FK_PurchaseOrderItemId = poItem.Id,
AcceptedQuantity = line.AcceptedQuantity,
RejectedQuantity = line.RejectedQuantity
});
}
var activeLines = po.Items.Where(i => i.IsActive && !i.IsCancelled).ToList();
po.Status = activeLines.Count > 0 && activeLines.All(i => i.ReceivedQuantity >= i.Quantity)
? PurchaseOrderStatus.Received
: PurchaseOrderStatus.PartiallyReceived;
await uow.BeginAsync(ct);
try
{
await repo.AddAsync(goodsReceipt);
foreach (var line in goodsReceipt.Items.Where(i => i.AcceptedQuantity > 0))
{
var poItem = po.Items.First(i => i.Id == line.FK_PurchaseOrderItemId);
var balance = await stockRepo.GetBalanceAsync(poItem.FK_ItemId, request.WarehouseId);
if (balance is null)
{
await stockRepo.AddBalanceAsync(new StockBalance
{
FK_ItemId = poItem.FK_ItemId,
LocationId = request.WarehouseId,
LocationType = LocationType.Warehouse,
Quantity = line.AcceptedQuantity
});
}
else
{
balance.Quantity += line.AcceptedQuantity;
balance.UpdatedAt = DateTime.Now;
stockRepo.UpdateBalance(balance);
}
await stockRepo.AddMovementAsync(new StockMovement
{
FK_ItemId = poItem.FK_ItemId,
ToLocationId = request.WarehouseId,
ToLocationType = LocationType.Warehouse,
Quantity = line.AcceptedQuantity,
Type = MovementType.PurchaseReceipt,
ReferenceId = goodsReceipt.Id,
CreatedBy = receivedByUserId
});
}
poRepo.Update(po);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
var saved = await repo.GetByIdAsync(goodsReceipt.Id);
return Map(saved!);
}
}