using Distribution.Features.Order; using Distribution.Features.Ride; using Distribution.Features.Sale; using Distribution.Features.Stock; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.DeliveryRide; public class DeliveryTaskService( IDeliveryTaskRepository repo, IRideRepository rideRepo, IOrderRepository orderRepo, ISaleRepository saleRepo, IStockRepository stockRepo, IUnitOfWork uow) { private static DeliveryTaskResponse ToResponse(DeliveryTask t) => new(t.Id, t.FK_RideId, t.FK_OrderId, t.Order.FK_StoreId, t.Order.Store?.StoreName ?? string.Empty, t.Sequence, t.Status, t.CustomerConfirmed, t.ConfirmedAt, t.Note, t.Order.OrderItems.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"); if (ride.Status != RideStatus.Active) throw new InvalidOperationException("Tasks can only be generated for an Active ride"); if (ride.RideType != RideType.Delivery) throw new InvalidOperationException("Tasks can only be generated for a Delivery ride"); var orders = await orderRepo.GetPendingByRouteIdAsync(ride.FK_RouteId); var created = new List(); int sequence = 1; foreach (var order in orders) { if (await repo.ExistsForOrderAndRideAsync(order.Id, rideId)) continue; var task = new DeliveryTask { FK_RideId = rideId, FK_OrderId = order.Id, Sequence = sequence++, Status = DeliveryTaskStatus.Pending }; created.Add(task); } if (created.Count == 0) return new List(); await uow.BeginAsync(ct); try { foreach (var task in created) 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 orderItem = task.Order.OrderItems.FirstOrDefault(i => i.FK_ItemId == input.ItemId) ?? throw new InvalidOperationException($"Item {input.ItemId} is not on this order"); if (input.QuantityFulfilled < 0) throw new InvalidOperationException($"QuantityFulfilled cannot be negative for item {input.ItemId}"); if (input.QuantityFulfilled > orderItem.Quantity) throw new InvalidOperationException($"QuantityFulfilled exceeds ordered quantity for item {input.ItemId}"); orderItem.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.Order.OrderItems.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.Order.OrderItems.All(i => i.QuantityFulfilled == i.Quantity); task.CustomerConfirmed = true; task.ConfirmedAt = DateTime.Now; task.Status = fullyDelivered ? DeliveryTaskStatus.Delivered : DeliveryTaskStatus.PartiallyDelivered; var order = task.Order; order.Status = fullyDelivered ? OrderStatus.Fulfilled : OrderStatus.PartiallyFulfilled; if (fullyDelivered) order.FulfilledAt = DateTime.Now; await uow.BeginAsync(ct); try { foreach (var item in task.Order.OrderItems.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, Quantity = item.QuantityFulfilled, Type = MovementType.Sale, ReferenceId = task.FK_OrderId }); } repo.Update(task); orderRepo.Update(order); if (fullyDelivered) { var sale = await saleRepo.GetByIdAsync(order.FK_SaleId) ?? throw new KeyNotFoundException("Sale not found"); sale.Status = SaleStatus.Completed; sale.CompletedAt = DateTime.Now; saleRepo.Update(sale); } 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); } }