225 lines
9.4 KiB
C#
225 lines
9.4 KiB
C#
using Distribution.Features.Ride;
|
|
using Distribution.Features.Stock;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
|
|
namespace Distribution.Features.LoadingEvent;
|
|
|
|
public class LoadingSheetService(
|
|
ILoadingSheetRepository repo,
|
|
IRideRepository rideRepo,
|
|
IStockRepository stockRepo,
|
|
IUnitOfWork uow)
|
|
{
|
|
public async Task<PagedResponse<LoadingSheetResponse>> GetAllAsync(Guid? rideId, LoadingSheetStatus? status, Guid? routeId, Guid? regionId, int page, int pageSize)
|
|
{
|
|
var total = await repo.GetCountAsync(rideId, status, routeId, regionId);
|
|
var items = await repo.GetAllAsync(rideId, status, routeId, regionId, page, pageSize);
|
|
return new PagedResponse<LoadingSheetResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var sheet = await repo.GetByIdAsync(id);
|
|
return sheet is null ? null : ToResponse(sheet);
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse> CreateAsync(CreateLoadingSheetRequest request, Guid userId, CancellationToken ct = default)
|
|
{
|
|
var ride = await rideRepo.GetByIdAsync(request.FK_RideId)
|
|
?? throw new KeyNotFoundException("Ride not found");
|
|
|
|
if (ride.Status != RideStatus.Open)
|
|
throw new InvalidOperationException("A loading sheet can only be created for an Open ride");
|
|
|
|
var existing = await repo.GetActiveByRideIdAsync(request.FK_RideId);
|
|
if (existing is not null)
|
|
throw new InvalidOperationException("This ride already has a Planned loading sheet");
|
|
|
|
var sheet = new LoadingSheet
|
|
{
|
|
FK_RideId = ride.Id,
|
|
Status = LoadingSheetStatus.Planned,
|
|
PopulatedBy = userId,
|
|
PopulatedAt = DateTime.Now
|
|
};
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(sheet);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(sheet.Id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse> SetItemsAsync(Guid id, SetItemsRequest request, CancellationToken ct = default)
|
|
{
|
|
var sheet = await repo.GetByIdAsync(id)
|
|
?? throw new KeyNotFoundException("Loading sheet not found");
|
|
|
|
if (sheet.Status != LoadingSheetStatus.Planned)
|
|
throw new InvalidOperationException("Items can only be changed while the sheet is Planned");
|
|
|
|
// The request is the full desired item list: a row matching (item, warehouse,
|
|
// shop PO item) gets its quantity updated in place, a new combination is added,
|
|
// and rows for combinations not present in the request are removed.
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
var newItems = new List<LoadingSheetItem>();
|
|
var keptItems = new List<LoadingSheetItem>();
|
|
foreach (var input in request.Items)
|
|
{
|
|
var existing = sheet.Items.FirstOrDefault(i =>
|
|
i.FK_ItemId == input.FK_ItemId &&
|
|
i.FK_WarehouseId == input.FK_WarehouseId &&
|
|
i.FK_ShopPOItemId == input.FK_ShopPOItemId);
|
|
|
|
if (existing is not null)
|
|
{
|
|
existing.QuantityDispatched = input.Quantity;
|
|
keptItems.Add(existing);
|
|
}
|
|
else
|
|
newItems.Add(new LoadingSheetItem
|
|
{
|
|
FK_LoadingSheetId = sheet.Id,
|
|
FK_ItemId = input.FK_ItemId,
|
|
FK_WarehouseId = input.FK_WarehouseId,
|
|
FK_ShopPOItemId = input.FK_ShopPOItemId,
|
|
QuantityDispatched = input.Quantity
|
|
});
|
|
}
|
|
|
|
var removedItems = sheet.Items.Where(i => !keptItems.Contains(i)).ToList();
|
|
if (removedItems.Count > 0)
|
|
repo.RemoveItems(removedItems);
|
|
if (newItems.Count > 0)
|
|
await repo.AddItemsAsync(newItems);
|
|
|
|
var shopPOItemIds = keptItems.Select(i => i.FK_ShopPOItemId)
|
|
.Concat(newItems.Select(i => i.FK_ShopPOItemId))
|
|
.Where(x => x.HasValue)
|
|
.Select(x => x!.Value)
|
|
.Distinct()
|
|
.ToList();
|
|
var shopPOIds = shopPOItemIds.Count > 0
|
|
? await repo.GetShopPOIdsByItemIdsAsync(shopPOItemIds)
|
|
: new List<Guid>();
|
|
|
|
await repo.ReplaceOrdersAsync(sheet.Id, shopPOIds);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(sheet.Id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse> ConfirmAsync(Guid id, Guid userId, CancellationToken ct = default)
|
|
{
|
|
var sheet = await repo.GetByIdAsync(id)
|
|
?? throw new KeyNotFoundException("Loading sheet not found");
|
|
|
|
if (sheet.Status != LoadingSheetStatus.Planned)
|
|
throw new InvalidOperationException($"Cannot confirm a sheet in {sheet.Status} status");
|
|
|
|
if (!sheet.Items.Any(i => i.QuantityDispatched > 0))
|
|
throw new InvalidOperationException("Sheet must have at least one item with quantity greater than zero");
|
|
|
|
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
|
|
?? throw new KeyNotFoundException("Ride not found");
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await ApplyLoadMovementsAsync(sheet.Id, ride.FK_VehicleId, sheet.Items, userId);
|
|
|
|
sheet.Status = LoadingSheetStatus.Confirmed;
|
|
sheet.ConfirmedAt = DateTime.Now;
|
|
sheet.ConfirmedBy = userId;
|
|
|
|
repo.Update(sheet);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
private async Task ApplyLoadMovementsAsync(Guid sheetId, Guid vehicleId, List<LoadingSheetItem> items, Guid userId)
|
|
{
|
|
// Vehicle balances created during this confirm are only tracked, not yet saved, so a
|
|
// later row for the same item would not find them via the repository and would try to
|
|
// insert a duplicate (item, vehicle) balance.
|
|
var createdVehicleBalances = new Dictionary<Guid, StockBalance>();
|
|
|
|
foreach (var item in items.Where(i => i.QuantityDispatched > 0))
|
|
{
|
|
var warehouseBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, item.FK_WarehouseId)
|
|
?? throw new InvalidOperationException(
|
|
$"No stock found for '{item.Item?.Name ?? item.FK_ItemId.ToString()}' in warehouse '{item.Warehouse?.Name ?? item.FK_WarehouseId.ToString()}'");
|
|
|
|
if (warehouseBalance.Quantity < item.QuantityDispatched)
|
|
throw new InvalidOperationException(
|
|
$"Insufficient stock for '{item.Item?.Name ?? item.FK_ItemId.ToString()}' in warehouse '{item.Warehouse?.Name ?? item.FK_WarehouseId.ToString()}' " +
|
|
$"(available {warehouseBalance.Quantity}, needed {item.QuantityDispatched})");
|
|
|
|
warehouseBalance.Quantity -= item.QuantityDispatched;
|
|
warehouseBalance.UpdatedAt = DateTime.Now;
|
|
stockRepo.UpdateBalance(warehouseBalance);
|
|
|
|
var vehicleBalance = createdVehicleBalances.GetValueOrDefault(item.FK_ItemId)
|
|
?? await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId);
|
|
if (vehicleBalance is null)
|
|
{
|
|
var created = new StockBalance
|
|
{
|
|
FK_ItemId = item.FK_ItemId,
|
|
LocationId = vehicleId,
|
|
LocationType = LocationType.Vehicle,
|
|
Quantity = item.QuantityDispatched
|
|
};
|
|
createdVehicleBalances[item.FK_ItemId] = created;
|
|
await stockRepo.AddBalanceAsync(created);
|
|
}
|
|
else
|
|
{
|
|
vehicleBalance.Quantity += item.QuantityDispatched;
|
|
vehicleBalance.UpdatedAt = DateTime.Now;
|
|
stockRepo.UpdateBalance(vehicleBalance);
|
|
}
|
|
|
|
await stockRepo.AddMovementAsync(new StockMovement
|
|
{
|
|
FK_ItemId = item.FK_ItemId,
|
|
FromLocationId = item.FK_WarehouseId,
|
|
FromLocationType = LocationType.Warehouse,
|
|
ToLocationId = vehicleId,
|
|
ToLocationType = LocationType.Vehicle,
|
|
Quantity = item.QuantityDispatched,
|
|
Type = MovementType.Load,
|
|
ReferenceId = sheetId,
|
|
CreatedBy = userId
|
|
});
|
|
}
|
|
}
|
|
|
|
private static LoadingSheetResponse ToResponse(LoadingSheet s) =>
|
|
new(s.Id, s.FK_RideId, s.Status, s.PopulatedBy, s.PopulatedAt,
|
|
s.ConfirmedAt, s.ConfirmedBy, s.CreatedAt,
|
|
s.Items.Select(i => new LoadingSheetItemResponse(
|
|
i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty,
|
|
i.FK_WarehouseId, i.Warehouse?.Name ?? string.Empty,
|
|
i.QuantityDispatched, i.FK_ShopPOItemId,
|
|
i.ShopPOItem?.FK_ShopPOId,
|
|
i.ShopPOItem?.ShopPO?.FK_StoreId,
|
|
i.ShopPOItem?.ShopPO?.Store?.StoreName)).ToList(),
|
|
s.Orders.Select(o => new LoadingSheetOrderResponse(o.FK_ShopPOId)).ToList());
|
|
}
|