93f2f1bd3b
Major refactor replacing Sale/Order with ShopPO → Invoice → LoadingSheet flow. Removed legacy Sale/Order models and features; introduced ShopPO, ShopPOItem, Invoice, LoadingSheetOrder, and SysConfig models. Updated related entities, enums, repositories, and controllers. Added new migrations for schema changes and system config. Overhauled business logic and DI registrations. Updated documentation with migration plan.
174 lines
6.5 KiB
C#
174 lines
6.5 KiB
C#
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<List<DeliveryTaskResponse>> GenerateAsync(Guid rideId, CancellationToken ct = default) =>
|
|
throw new NotImplementedException("Delivery task auto-generation is deferred — route-based dispatch not yet implemented.");
|
|
|
|
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 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, 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<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);
|
|
}
|
|
}
|