From c626f56cdbfb6ea7ac493fc157533375c8fe0a13 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Fri, 16 Oct 2026 15:37:05 +0530 Subject: [PATCH] Refactor Return and Ride services to integrate StockMover and enhance error handling --- .../Features/Returns/ReturnController.cs | 2 +- .../Features/Returns/ReturnService.cs | 74 ++++++++++++++++++- Distribution/Features/Ride/RideController.cs | 7 +- Distribution/Features/Ride/RideService.cs | 30 ++++---- 4 files changed, 92 insertions(+), 21 deletions(-) diff --git a/Distribution/Features/Returns/ReturnController.cs b/Distribution/Features/Returns/ReturnController.cs index 24cfb7c..b3eb4f3 100644 --- a/Distribution/Features/Returns/ReturnController.cs +++ b/Distribution/Features/Returns/ReturnController.cs @@ -42,7 +42,7 @@ public class ReturnController(ReturnService service, ICurrentUserService current var result = await service.CreateAsync(id, request, userId.Value, ct); 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 }); } } } diff --git a/Distribution/Features/Returns/ReturnService.cs b/Distribution/Features/Returns/ReturnService.cs index 83ab2e5..77c6fc2 100644 --- a/Distribution/Features/Returns/ReturnService.cs +++ b/Distribution/Features/Returns/ReturnService.cs @@ -1,3 +1,4 @@ +using Distribution.Features.Category; using Distribution.Features.Item; using Distribution.Features.ShopPO; using Distribution.Features.Stock; @@ -10,7 +11,8 @@ public class ReturnService( IReturnRepository repo, IShopPORepository shopPORepo, IItemRepository itemRepo, - IStockRepository stockRepo, + ICategoryRepository categoryRepo, + StockMover mover, IUnitOfWork uow) { private static SaleReturnItemResponse MapItem(SaleReturnItem i) => @@ -57,6 +59,72 @@ public class ReturnService( public async Task> GetByShopPOIdAsync(Guid shopPOId) => (await repo.GetByShopPOIdAsync(shopPOId)).Select(Map).ToList(); - public Task CreateAsync(Guid shopPOId, CreateSaleReturnRequest request, Guid createdByUserId, CancellationToken ct = default) => - 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. + /// + public async Task 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!); + } } diff --git a/Distribution/Features/Ride/RideController.cs b/Distribution/Features/Ride/RideController.cs index ec0086d..82660a4 100644 --- a/Distribution/Features/Ride/RideController.cs +++ b/Distribution/Features/Ride/RideController.cs @@ -1,11 +1,12 @@ using Distribution.Models; +using Distribution.Infra.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Ride; [ApiController] [Route("api/rides")] -public class RideController(RideService service) : ControllerBase +public class RideController(RideService service, ICurrentUserService currentUser) : ControllerBase { [HttpGet] public async Task GetAll( @@ -54,7 +55,7 @@ public class RideController(RideService service) : ControllerBase { try { - var result = await service.CloseAsync(id, request, ct); + var result = await service.CloseAsync(id, request, currentUser.UserId ?? Guid.Empty, ct); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } @@ -66,7 +67,7 @@ public class RideController(RideService service) : ControllerBase { try { - var result = await service.CancelAsync(id, request, ct); + var result = await service.CancelAsync(id, request, currentUser.UserId ?? Guid.Empty, ct); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } diff --git a/Distribution/Features/Ride/RideService.cs b/Distribution/Features/Ride/RideService.cs index 8301b2b..2e1291d 100644 --- a/Distribution/Features/Ride/RideService.cs +++ b/Distribution/Features/Ride/RideService.cs @@ -10,7 +10,7 @@ public class RideService( IRideRepository rideRepo, IDriverProfileRepository driverRepo, IDriverComplianceConfigRepository complianceRepo, - IStockRepository stockRepo, + StockMover mover, IUnitOfWork uow) { public async Task> GetAllAsync(RideStatus? status, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize) @@ -76,7 +76,7 @@ public class RideService( return ToResponse(saved!); } - public async Task CloseAsync(Guid rideId, CloseRideRequest request, CancellationToken ct = default) + public async Task CloseAsync(Guid rideId, CloseRideRequest request, Guid userId, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); @@ -87,20 +87,24 @@ public class RideService( if (request.Meterend < 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.ClosedAt = DateTime.Now; ride.Meterend = request.Meterend; + // Whatever wasn't delivered goes back to the warehouse it was loaded from. 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; } return ToResponse(ride); } - public async Task CancelAsync(Guid rideId, CancelRideRequest request, CancellationToken ct = default) + public async Task CancelAsync(Guid rideId, CancelRideRequest request, Guid userId, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); @@ -113,7 +117,12 @@ public class RideService( ride.CancelReason = request.Reason; 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; } return ToResponse(ride); @@ -139,13 +148,6 @@ public class RideService( 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) => new(r.Id, r.Status, r.FK_VehicleId, r.Vehicle?.NumberPlate ?? string.Empty,