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> GetAllAsync(Guid? rideId, LoadingSheetStatus? status, int page, int pageSize) { var total = await repo.GetCountAsync(rideId, status); var items = await repo.GetAllAsync(rideId, status, page, pageSize); return new PagedResponse(page, pageSize, total, items.Select(ToResponse).ToList()); } public async Task GetByIdAsync(Guid id) { var sheet = await repo.GetByIdAsync(id); return sheet is null ? null : ToResponse(sheet); } public async Task 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 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"); await uow.BeginAsync(ct); try { repo.RemoveItems(sheet.Items); await repo.AddItemsAsync(request.Items.Select(i => new LoadingSheetItem { FK_LoadingSheetId = sheet.Id, FK_ItemId = i.FK_ItemId, FK_WarehouseId = i.FK_WarehouseId, FK_ShopPOItemId = i.FK_ShopPOItemId, QuantityDispatched = i.Quantity })); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await repo.GetByIdAsync(sheet.Id); return ToResponse(saved!); } public async Task 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 items, Guid userId) { 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 in warehouse for item {item.FK_ItemId}"); if (warehouseBalance.Quantity < item.QuantityDispatched) throw new InvalidOperationException($"Insufficient warehouse stock for item {item.FK_ItemId}"); warehouseBalance.Quantity -= item.QuantityDispatched; warehouseBalance.UpdatedAt = DateTime.Now; stockRepo.UpdateBalance(warehouseBalance); var vehicleBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId); if (vehicleBalance is null) { await stockRepo.AddBalanceAsync(new StockBalance { FK_ItemId = item.FK_ItemId, LocationId = vehicleId, LocationType = LocationType.Vehicle, Quantity = item.QuantityDispatched }); } 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)).ToList(), s.Orders.Select(o => new LoadingSheetOrderResponse(o.FK_ShopPOId)).ToList()); }