Refactor Return and Ride services to integrate StockMover and enhance error handling
This commit is contained in:
@@ -42,7 +42,7 @@ public class ReturnController(ReturnService service, ICurrentUserService current
|
|||||||
var result = await service.CreateAsync(id, request, userId.Value, ct);
|
var result = await service.CreateAsync(id, request, userId.Value, ct);
|
||||||
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
||||||
}
|
}
|
||||||
catch (NotImplementedException ex) { return StatusCode(501, new { error = ex.Message }); }
|
|
||||||
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
catch (InvalidOperationException ex) { return Conflict(new { error = ex.Message }); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Distribution.Features.Category;
|
||||||
using Distribution.Features.Item;
|
using Distribution.Features.Item;
|
||||||
using Distribution.Features.ShopPO;
|
using Distribution.Features.ShopPO;
|
||||||
using Distribution.Features.Stock;
|
using Distribution.Features.Stock;
|
||||||
@@ -10,7 +11,8 @@ public class ReturnService(
|
|||||||
IReturnRepository repo,
|
IReturnRepository repo,
|
||||||
IShopPORepository shopPORepo,
|
IShopPORepository shopPORepo,
|
||||||
IItemRepository itemRepo,
|
IItemRepository itemRepo,
|
||||||
IStockRepository stockRepo,
|
ICategoryRepository categoryRepo,
|
||||||
|
StockMover mover,
|
||||||
IUnitOfWork uow)
|
IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
private static SaleReturnItemResponse MapItem(SaleReturnItem i) =>
|
private static SaleReturnItemResponse MapItem(SaleReturnItem i) =>
|
||||||
@@ -57,6 +59,72 @@ public class ReturnService(
|
|||||||
public async Task<List<SaleReturnResponse>> GetByShopPOIdAsync(Guid shopPOId) =>
|
public async Task<List<SaleReturnResponse>> GetByShopPOIdAsync(Guid shopPOId) =>
|
||||||
(await repo.GetByShopPOIdAsync(shopPOId)).Select(Map).ToList();
|
(await repo.GetByShopPOIdAsync(shopPOId)).Select(Map).ToList();
|
||||||
|
|
||||||
public Task<SaleReturnResponse> CreateAsync(Guid shopPOId, CreateSaleReturnRequest request, Guid createdByUserId, CancellationToken ct = default) =>
|
/// <summary>
|
||||||
throw new NotImplementedException("Return flow is being redesigned — not yet implemented.");
|
/// Stock-only: records the return and puts the goods back on the vehicle that delivered them (restoring the
|
||||||
|
/// batches/serials that were sold). Credit/exchange money handling is not applied here, so TotalCreditAmount stays 0.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<SaleReturnResponse> CreateAsync(Guid shopPOId, CreateSaleReturnRequest request, Guid createdByUserId, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var shopPO = await shopPORepo.GetByIdAsync(shopPOId);
|
||||||
|
if (shopPO is null || !shopPO.IsActive)
|
||||||
|
throw new InvalidOperationException("Order not found.");
|
||||||
|
|
||||||
|
if (request.Items is null || request.Items.Count == 0)
|
||||||
|
throw new InvalidOperationException("A return must have at least one line item.");
|
||||||
|
|
||||||
|
if (request.Items.GroupBy(i => i.ItemId).Any(g => g.Count() > 1))
|
||||||
|
throw new InvalidOperationException("Each item can appear only once on a return.");
|
||||||
|
|
||||||
|
var saleReturn = new SaleReturn
|
||||||
|
{
|
||||||
|
FK_ShopPOId = shopPOId,
|
||||||
|
Mode = request.Mode,
|
||||||
|
TotalCreditAmount = 0,
|
||||||
|
CreatedByUserId = createdByUserId
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var line in request.Items)
|
||||||
|
{
|
||||||
|
if (line.Quantity <= 0)
|
||||||
|
throw new InvalidOperationException("Return quantity must be greater than zero.");
|
||||||
|
|
||||||
|
var shopPOItem = shopPO.Items.FirstOrDefault(i => i.FK_ItemId == line.ItemId && i.IsActive)
|
||||||
|
?? throw new InvalidOperationException($"Item {line.ItemId} is not on this order.");
|
||||||
|
|
||||||
|
var item = await itemRepo.GetByIdAsync(line.ItemId)
|
||||||
|
?? throw new InvalidOperationException("Item not found.");
|
||||||
|
|
||||||
|
var category = await categoryRepo.GetByIdAsync(item.FK_CategoryId)
|
||||||
|
?? throw new InvalidOperationException("Category not found.");
|
||||||
|
|
||||||
|
if (!category.AllowCustomerReturn)
|
||||||
|
throw new InvalidOperationException($"Customer returns are not allowed for category '{category.Name}'.");
|
||||||
|
|
||||||
|
saleReturn.Items.Add(new SaleReturnItem
|
||||||
|
{
|
||||||
|
FK_ItemId = line.ItemId,
|
||||||
|
Quantity = line.Quantity,
|
||||||
|
ReasonCode = line.ReasonCode,
|
||||||
|
UnitPrice = shopPOItem.UnitPrice
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.BeginAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await repo.AddAsync(saleReturn);
|
||||||
|
|
||||||
|
foreach (var line in saleReturn.Items)
|
||||||
|
{
|
||||||
|
var itemName = shopPO.Items.First(i => i.FK_ItemId == line.FK_ItemId).Item?.Name ?? line.FK_ItemId.ToString();
|
||||||
|
await mover.ReturnToVehicleAsync(line.FK_ItemId, itemName, line.Quantity, shopPOId, createdByUserId, line.ReasonCode.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
|
var saved = await repo.GetByIdAsync(saleReturn.Id);
|
||||||
|
return Map(saved!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
using Distribution.Models;
|
using Distribution.Models;
|
||||||
|
using Distribution.Infra.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
namespace Distribution.Features.Ride;
|
namespace Distribution.Features.Ride;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("api/rides")]
|
[Route("api/rides")]
|
||||||
public class RideController(RideService service) : ControllerBase
|
public class RideController(RideService service, ICurrentUserService currentUser) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> GetAll(
|
public async Task<IActionResult> GetAll(
|
||||||
@@ -54,7 +55,7 @@ public class RideController(RideService service) : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await service.CloseAsync(id, request, ct);
|
var result = await service.CloseAsync(id, request, currentUser.UserId ?? Guid.Empty, ct);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { return NotFound(); }
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
@@ -66,7 +67,7 @@ public class RideController(RideService service) : ControllerBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await service.CancelAsync(id, request, ct);
|
var result = await service.CancelAsync(id, request, currentUser.UserId ?? Guid.Empty, ct);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
catch (KeyNotFoundException) { return NotFound(); }
|
catch (KeyNotFoundException) { return NotFound(); }
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public class RideService(
|
|||||||
IRideRepository rideRepo,
|
IRideRepository rideRepo,
|
||||||
IDriverProfileRepository driverRepo,
|
IDriverProfileRepository driverRepo,
|
||||||
IDriverComplianceConfigRepository complianceRepo,
|
IDriverComplianceConfigRepository complianceRepo,
|
||||||
IStockRepository stockRepo,
|
StockMover mover,
|
||||||
IUnitOfWork uow)
|
IUnitOfWork uow)
|
||||||
{
|
{
|
||||||
public async Task<PagedResponse<RideResponse>> GetAllAsync(RideStatus? status, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize)
|
public async Task<PagedResponse<RideResponse>> GetAllAsync(RideStatus? status, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize)
|
||||||
@@ -76,7 +76,7 @@ public class RideService(
|
|||||||
return ToResponse(saved!);
|
return ToResponse(saved!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RideResponse> CloseAsync(Guid rideId, CloseRideRequest request, CancellationToken ct = default)
|
public async Task<RideResponse> CloseAsync(Guid rideId, CloseRideRequest request, Guid userId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||||
?? throw new KeyNotFoundException("Ride not found");
|
?? throw new KeyNotFoundException("Ride not found");
|
||||||
@@ -87,20 +87,24 @@ public class RideService(
|
|||||||
if (request.Meterend < ride.Meterstart)
|
if (request.Meterend < ride.Meterstart)
|
||||||
throw new InvalidOperationException($"Meter end ({request.Meterend}) cannot be less than meter start ({ride.Meterstart})");
|
throw new InvalidOperationException($"Meter end ({request.Meterend}) cannot be less than meter start ({ride.Meterstart})");
|
||||||
|
|
||||||
await EnsureVehicleStockClearedAsync(ride.FK_VehicleId);
|
|
||||||
|
|
||||||
ride.Status = RideStatus.Closed;
|
ride.Status = RideStatus.Closed;
|
||||||
ride.ClosedAt = DateTime.Now;
|
ride.ClosedAt = DateTime.Now;
|
||||||
ride.Meterend = request.Meterend;
|
ride.Meterend = request.Meterend;
|
||||||
|
|
||||||
|
// Whatever wasn't delivered goes back to the warehouse it was loaded from.
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
try
|
||||||
|
{
|
||||||
|
await mover.UnloadVehicleAsync(ride.FK_VehicleId, ride.Id, userId, "RideClosed");
|
||||||
|
rideRepo.Update(ride);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
return ToResponse(ride);
|
return ToResponse(ride);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<RideResponse> CancelAsync(Guid rideId, CancelRideRequest request, CancellationToken ct = default)
|
public async Task<RideResponse> CancelAsync(Guid rideId, CancelRideRequest request, Guid userId, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var ride = await rideRepo.GetByIdAsync(rideId)
|
var ride = await rideRepo.GetByIdAsync(rideId)
|
||||||
?? throw new KeyNotFoundException("Ride not found");
|
?? throw new KeyNotFoundException("Ride not found");
|
||||||
@@ -113,7 +117,12 @@ public class RideService(
|
|||||||
ride.CancelReason = request.Reason;
|
ride.CancelReason = request.Reason;
|
||||||
|
|
||||||
await uow.BeginAsync(ct);
|
await uow.BeginAsync(ct);
|
||||||
try { rideRepo.Update(ride); await uow.CommitAsync(ct); }
|
try
|
||||||
|
{
|
||||||
|
await mover.UnloadVehicleAsync(ride.FK_VehicleId, ride.Id, userId, "RideCancelled");
|
||||||
|
rideRepo.Update(ride);
|
||||||
|
await uow.CommitAsync(ct);
|
||||||
|
}
|
||||||
catch { await uow.RollbackAsync(ct); throw; }
|
catch { await uow.RollbackAsync(ct); throw; }
|
||||||
|
|
||||||
return ToResponse(ride);
|
return ToResponse(ride);
|
||||||
@@ -139,13 +148,6 @@ public class RideService(
|
|||||||
throw new InvalidOperationException("Driver medical clearance has expired");
|
throw new InvalidOperationException("Driver medical clearance has expired");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureVehicleStockClearedAsync(Guid vehicleId)
|
|
||||||
{
|
|
||||||
var balances = await stockRepo.GetBalancesByLocationAsync(vehicleId, LocationType.Vehicle);
|
|
||||||
if (balances.Any(b => b.Quantity > 0))
|
|
||||||
throw new InvalidOperationException("All vehicle stock must be returned before this ride can be completed");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static RideResponse ToResponse(DeliveryRun r) =>
|
private static RideResponse ToResponse(DeliveryRun r) =>
|
||||||
new(r.Id, r.Status,
|
new(r.Id, r.Status,
|
||||||
r.FK_VehicleId, r.Vehicle?.NumberPlate ?? string.Empty,
|
r.FK_VehicleId, r.Vehicle?.NumberPlate ?? string.Empty,
|
||||||
|
|||||||
Reference in New Issue
Block a user