6d8b6b3e59
Implemented DeliveryTask, Payment, and Sale entities with full CRUD, business logic, and API endpoints. Updated Order/OrderItem for sale linkage and fulfillment tracking. Registered new DbSets and DI services. Revised documentation for delivery, sale, and payment workflows.
225 lines
8.0 KiB
C#
225 lines
8.0 KiB
C#
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<List<DeliveryTaskResponse>> 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<DeliveryTask>();
|
|
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<DeliveryTaskResponse>();
|
|
|
|
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<List<DeliveryTaskResponse>> GetByRideIdAsync(Guid rideId)
|
|
{
|
|
var tasks = await repo.GetByRideIdAsync(rideId);
|
|
return tasks.Select(ToResponse).ToList();
|
|
}
|
|
|
|
public async Task<DeliveryTaskResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var task = await repo.GetByIdAsync(id);
|
|
return task is null ? null : ToResponse(task);
|
|
}
|
|
|
|
public async Task<DeliveryTaskResponse> 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<DeliveryTaskResponse> 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)
|
|
?? throw new InvalidOperationException($"No vehicle stock found for item {item.FK_ItemId}");
|
|
|
|
if (vehicleBalance.Quantity < item.QuantityFulfilled)
|
|
throw new InvalidOperationException($"Insufficient vehicle stock for item {item.FK_ItemId}");
|
|
|
|
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<DeliveryTaskResponse> 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);
|
|
}
|
|
}
|