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, 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 Task> GenerateAsync(Guid rideId, CancellationToken ct = default) => throw new NotImplementedException("Delivery task auto-generation is deferred — route-based dispatch not yet implemented."); 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); } }