using Distribution.Features.LoadingEvent; using Distribution.Features.Ride; using Distribution.Features.ShopPO; using Distribution.Features.Stock; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.DeliveryRide; public class DeliveryTaskService( IDeliveryTaskRepository repo, IRideRepository rideRepo, IShopPORepository shopPORepo, ILoadingSheetRepository loadingSheetRepo, IStockRepository stockRepo, IUnitOfWork uow) { private static DeliveryTaskResponse ToResponse(DeliveryTask t) => new(t.Id, t.FK_RideId, t.FK_ShopPOId, t.ShopPO.FK_StoreId, t.ShopPO.Store?.StoreName ?? string.Empty, t.Sequence, t.Status, t.CustomerConfirmed, t.ConfirmedAt, t.Note, t.ShopPO.Items.Select(i => new DeliveryTaskItemResponse( i.FK_ItemId, i.Item?.Name ?? string.Empty, i.Quantity, i.QuantityFulfilled, i.UnitPrice)).ToList(), t.CreatedAt); public async Task> GenerateAsync(Guid rideId, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); var confirmedSheets = await loadingSheetRepo.GetAllAsync(rideId, LoadingSheetStatus.Confirmed, null, null, 1, int.MaxValue); if (confirmedSheets.Count == 0) throw new InvalidOperationException("Ride has no confirmed loading sheet to generate delivery tasks from"); var shopPOIds = confirmedSheets .SelectMany(s => s.Orders) .Select(o => o.FK_ShopPOId) .Distinct() .ToList(); var existingTasks = await repo.GetByRideIdAsync(rideId); var nextSequence = existingTasks.Count > 0 ? existingTasks.Max(t => t.Sequence) + 1 : 1; var newTasks = new List(); foreach (var shopPOId in shopPOIds) { if (await repo.ExistsForOrderAndRideAsync(shopPOId, rideId)) continue; newTasks.Add(new DeliveryTask { FK_RideId = rideId, FK_ShopPOId = shopPOId, Sequence = nextSequence++, Status = DeliveryTaskStatus.Pending }); } if (newTasks.Count > 0) { await uow.BeginAsync(ct); try { foreach (var task in newTasks) await repo.AddAsync(task); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } } var saved = await repo.GetByRideIdAsync(rideId); return saved.Select(ToResponse).ToList(); } public async Task> GetByRideIdAsync(Guid rideId) { var tasks = await repo.GetByRideIdAsync(rideId); return tasks.Select(ToResponse).ToList(); } public async Task GetByIdAsync(Guid id) { var task = await repo.GetByIdAsync(id); return task is null ? null : ToResponse(task); } public async Task SetQuantitiesAsync(Guid taskId, SetQuantitiesRequest request, CancellationToken ct = default) { var task = await repo.GetByIdAsync(taskId) ?? throw new KeyNotFoundException("Delivery task not found"); if (task.Status != DeliveryTaskStatus.Pending) throw new InvalidOperationException("Quantities can only be set for a Pending task"); foreach (var input in request.Items) { var shopPOItem = task.ShopPO.Items.FirstOrDefault(i => i.FK_ItemId == input.ItemId) ?? throw new InvalidOperationException($"Item {input.ItemId} is not on this ShopPO"); if (input.QuantityFulfilled < 0) throw new InvalidOperationException($"QuantityFulfilled cannot be negative for item {input.ItemId}"); if (input.QuantityFulfilled > shopPOItem.Quantity) throw new InvalidOperationException($"QuantityFulfilled exceeds ordered quantity for item {input.ItemId}"); shopPOItem.QuantityFulfilled = input.QuantityFulfilled; } await uow.BeginAsync(ct); try { repo.Update(task); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await repo.GetByIdAsync(taskId); return ToResponse(saved!); } public async Task ConfirmAsync(Guid taskId, CancellationToken ct = default) { var task = await repo.GetByIdAsync(taskId) ?? throw new KeyNotFoundException("Delivery task not found"); if (task.Status != DeliveryTaskStatus.Pending) throw new InvalidOperationException("Only a Pending task can be confirmed"); if (!task.ShopPO.Items.Any(i => i.QuantityFulfilled > 0)) throw new InvalidOperationException("Set quantities before confirming — at least one item must have a fulfilled quantity greater than zero"); var ride = await rideRepo.GetByIdAsync(task.FK_RideId) ?? throw new KeyNotFoundException("Ride not found"); var vehicleId = ride.FK_VehicleId; bool fullyDelivered = task.ShopPO.Items.All(i => i.QuantityFulfilled == i.Quantity); task.CustomerConfirmed = true; task.ConfirmedAt = DateTime.Now; task.Status = fullyDelivered ? DeliveryTaskStatus.Delivered : DeliveryTaskStatus.PartiallyDelivered; var shopPO = task.ShopPO; shopPO.Status = fullyDelivered ? ShopPOStatus.Fulfilled : ShopPOStatus.PartiallyFulfilled; if (fullyDelivered) shopPO.FulfilledAt = DateTime.Now; await uow.BeginAsync(ct); try { foreach (var item in task.ShopPO.Items.Where(i => i.QuantityFulfilled > 0)) { var vehicleBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId); var availableQuantity = vehicleBalance?.Quantity ?? 0; var itemName = item.Item?.Name ?? item.FK_ItemId.ToString(); if (availableQuantity < item.QuantityFulfilled) throw new InvalidOperationException( $"Insufficient vehicle stock for item '{itemName}': available {availableQuantity}, required {item.QuantityFulfilled}"); vehicleBalance!.Quantity -= item.QuantityFulfilled; vehicleBalance.UpdatedAt = DateTime.Now; stockRepo.UpdateBalance(vehicleBalance); await stockRepo.AddMovementAsync(new StockMovement { FK_ItemId = item.FK_ItemId, FromLocationId = vehicleId, FromLocationType = LocationType.Vehicle, ToLocationId = StockService.ExternalLocationId, ToLocationType = LocationType.External, Quantity = item.QuantityFulfilled, Type = MovementType.Sale, ReferenceId = task.FK_ShopPOId }); } repo.Update(task); shopPORepo.Update(shopPO); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await repo.GetByIdAsync(taskId); return ToResponse(saved!); } public async Task FailAsync(Guid taskId, string note, CancellationToken ct = default) { var task = await repo.GetByIdAsync(taskId) ?? throw new KeyNotFoundException("Delivery task not found"); if (task.Status != DeliveryTaskStatus.Pending) throw new InvalidOperationException("Only a Pending task can be failed"); task.Status = DeliveryTaskStatus.Failed; task.Note = note; await uow.BeginAsync(ct); try { repo.Update(task); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(task); } }