235 lines
8.7 KiB
C#
235 lines
8.7 KiB
C#
using Distribution.Features.LoadingEvent;
|
|
using Distribution.Features.Notifications;
|
|
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,
|
|
StockMover mover,
|
|
INotificationService notifications,
|
|
IUnitOfWork uow)
|
|
{
|
|
private const string DeliveriesLink = "/operations/deliveries";
|
|
|
|
// Route owner + managers whose scope covers the store being delivered to.
|
|
private static List<RecipientRule> DeliveryRecipients(Guid storeId) =>
|
|
[
|
|
RecipientRule.RouteOwner(ScopeKind.Store, storeId),
|
|
RecipientRule.PermissionInScope("delivery-task.manage", ScopeKind.Store, storeId),
|
|
];
|
|
|
|
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<List<DeliveryTaskResponse>> 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<DeliveryTask>();
|
|
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<List<DeliveryTaskResponse>> GetByRideIdAsync(Guid rideId)
|
|
{
|
|
var tasks = await repo.GetByRideIdAsync(rideId);
|
|
return tasks.Select(ToResponse).ToList();
|
|
}
|
|
|
|
public async Task<PagedResponse<DeliveryTaskResponse>> GetAllAsync(DeliveryTaskStatus? status, int page, int pageSize)
|
|
{
|
|
var items = await repo.GetAllAsync(status, page, pageSize);
|
|
var total = await repo.GetCountAsync(status);
|
|
return new PagedResponse<DeliveryTaskResponse>(page, pageSize, total, items.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 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<DeliveryTaskResponse> ConfirmAsync(Guid taskId, Guid userId, 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 itemName = item.Item?.Name ?? item.FK_ItemId.ToString();
|
|
|
|
await mover.MoveAsync(item.FK_ItemId, itemName,
|
|
vehicleId, LocationType.Vehicle,
|
|
StockService.ExternalLocationId, LocationType.External,
|
|
item.QuantityFulfilled, MovementType.Sale, task.FK_ShopPOId, userId);
|
|
}
|
|
|
|
repo.Update(task);
|
|
shopPORepo.Update(shopPO);
|
|
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
if (!fullyDelivered && shopPO.Store is not null)
|
|
await notifications.NotifyAsync(
|
|
NotificationTypes.DeliveryTaskPartiallyDelivered,
|
|
DeliveryRecipients(shopPO.FK_StoreId),
|
|
new NotificationArgs(new { shopPO.Store.StoreName }, "DeliveryTask", task.Id, DeliveriesLink, userId),
|
|
ct);
|
|
|
|
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; }
|
|
|
|
if (task.ShopPO?.Store is not null)
|
|
await notifications.NotifyAsync(
|
|
NotificationTypes.DeliveryTaskFailed,
|
|
DeliveryRecipients(task.ShopPO.FK_StoreId),
|
|
new NotificationArgs(new { task.ShopPO.Store.StoreName, Note = note }, "DeliveryTask", task.Id, DeliveriesLink),
|
|
ct);
|
|
|
|
return ToResponse(task);
|
|
}
|
|
}
|