using Distribution.Features.Stock; using Distribution.Features.DriverComplianceConfig; using Distribution.Features.DriverProfile; using Distribution.Infra.UoW; using Distribution.Models; namespace Distribution.Features.Ride; public class RideService( IRideRepository rideRepo, IDriverProfileRepository driverRepo, IDriverComplianceConfigRepository complianceRepo, IRideCancelRequestRepository cancelRepo, Features.Vehicle.IVehicleRepository vehicleRepo, IStockRepository stockRepo, IUnitOfWork uow) { public async Task> GetAllAsync( RideStatus? status, RideType? type, Guid? driverProfileId, Guid? vehicleId, int page, int pageSize) { var total = await rideRepo.GetCountAsync(status, type, driverProfileId, vehicleId); var items = await rideRepo.GetAllAsync(status, type, driverProfileId, vehicleId, page, pageSize); return new PagedResponse(page, pageSize, total, items.Select(ToResponse).ToList()); } public async Task GetByIdAsync(Guid id) { var ride = await rideRepo.GetByIdAsync(id); return ride is null ? null : ToResponse(ride); } public async Task CreateAsync(CreateRideRequest request, CancellationToken ct = default) { var vehicle = await vehicleRepo.GetByIdAsync(request.FK_VehicleId) ?? throw new InvalidOperationException("Vehicle not found"); var sourceWarehouseId = vehicle.FK_DefaultWarehouseId ?? request.FK_SourceWarehouseId; if (!sourceWarehouseId.HasValue) throw new InvalidOperationException("Source warehouse is required for this vehicle"); var ride = new DeliveryRun { RideType = request.RideType, FK_VehicleId = request.FK_VehicleId, FK_DriverProfileId = request.FK_DriverProfileId, FK_RouteId = request.FK_RouteId, FK_SourceWarehouseId = sourceWarehouseId.Value, Status = RideStatus.Draft }; await uow.BeginAsync(ct); try { await rideRepo.AddAsync(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await rideRepo.GetByIdAsync(ride.Id); return ToResponse(saved!); } public async Task OpenAsync(Guid rideId, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.Draft) throw new InvalidOperationException($"Cannot open a ride in {ride.Status} status"); if (await rideRepo.HasOpenOrActiveRideForVehicleAsync(ride.FK_VehicleId, ride.Id)) throw new InvalidOperationException("Vehicle already has an open or active ride"); await CheckDriverComplianceAsync(ride.FK_DriverProfileId); ride.Status = RideStatus.Open; ride.StartedAt = DateTime.Now; await uow.BeginAsync(ct); try { rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(ride); } public async Task AssignDriverAsync(Guid rideId, Guid driverProfileId, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.Draft) throw new InvalidOperationException("Driver can only be changed while the ride is in Draft status"); var driver = await driverRepo.GetByIdAsync(driverProfileId); if (driver is null || !driver.IsActive) throw new InvalidOperationException("Driver profile not found or inactive"); ride.FK_DriverProfileId = driverProfileId; await uow.BeginAsync(ct); try { rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } var saved = await rideRepo.GetByIdAsync(rideId); return ToResponse(saved!); } public async Task ActivateAsync(Guid rideId, ActivateRideRequest request, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.Open) throw new InvalidOperationException($"Cannot activate a ride in {ride.Status} status"); ride.Status = RideStatus.Active; ride.ActivatedAt = DateTime.Now; ride.Meterstart = request.Meterstart; await uow.BeginAsync(ct); try { rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(ride); } public async Task CloseAsync(Guid rideId, CloseRideRequest request, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.Active) throw new InvalidOperationException($"Cannot close a ride in {ride.Status} status"); await EnsureVehicleStockClearedAsync(ride.FK_VehicleId); ride.Status = RideStatus.Closed; ride.ClosedAt = DateTime.Now; ride.Meterend = request.Meterend; await uow.BeginAsync(ct); try { rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToResponse(ride); } public async Task RaiseCancelRequestAsync(Guid rideId, RaiseCancelRequest request, Guid requestedBy, CancellationToken ct = default) { var ride = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.Active) throw new InvalidOperationException($"Cannot raise a cancel request for a ride in {ride.Status} status"); var existing = await cancelRepo.GetPendingByRideIdAsync(rideId); if (existing is not null) throw new InvalidOperationException("There is already a pending cancel request for this ride"); var cancelRequest = new RideCancelRequest { FK_RideId = rideId, TypeCode = request.TypeCode, Reason = request.Reason, RequestedBy = requestedBy, RequestedAt = DateTime.Now, Status = CancelRequestStatus.Pending }; ride.Status = RideStatus.CancelRequested; await uow.BeginAsync(ct); try { await cancelRepo.AddAsync(cancelRequest); rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToCancelResponse(cancelRequest); } public async Task> GetCancelRequestsAsync(CancelRequestStatus? status, int page, int pageSize) { var total = await cancelRepo.GetCountAsync(status); var items = await cancelRepo.GetAllAsync(status, page, pageSize); return new PagedResponse(page, pageSize, total, items.Select(ToCancelResponse).ToList()); } public async Task ApproveCancelAsync(Guid cancelRequestId, Guid approvedBy, CancellationToken ct = default) { var cancelRequest = await cancelRepo.GetByIdAsync(cancelRequestId) ?? throw new KeyNotFoundException("Cancel request not found"); if (cancelRequest.Status != CancelRequestStatus.Pending) throw new InvalidOperationException("Cancel request is not pending"); var ride = await rideRepo.GetByIdAsync(cancelRequest.FK_RideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.CancelRequested) throw new InvalidOperationException("Ride is not in CancelRequested status"); await EnsureVehicleStockClearedAsync(ride.FK_VehicleId); cancelRequest.Status = CancelRequestStatus.Approved; cancelRequest.ApprovedBy = approvedBy; cancelRequest.ApprovedAt = DateTime.Now; ride.Status = RideStatus.Cancelled; await uow.BeginAsync(ct); try { cancelRepo.Update(cancelRequest); rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToCancelResponse(cancelRequest); } public async Task RejectCancelAsync(Guid cancelRequestId, Guid rejectedBy, CancellationToken ct = default) { var cancelRequest = await cancelRepo.GetByIdAsync(cancelRequestId) ?? throw new KeyNotFoundException("Cancel request not found"); if (cancelRequest.Status != CancelRequestStatus.Pending) throw new InvalidOperationException("Cancel request is not pending"); var ride = await rideRepo.GetByIdAsync(cancelRequest.FK_RideId) ?? throw new KeyNotFoundException("Ride not found"); if (ride.Status != RideStatus.CancelRequested) throw new InvalidOperationException("Ride is not in CancelRequested status"); cancelRequest.Status = CancelRequestStatus.Rejected; cancelRequest.ApprovedBy = rejectedBy; cancelRequest.ApprovedAt = DateTime.Now; // Restore to previous status: Active if ActivatedAt is set, else Open ride.Status = ride.ActivatedAt.HasValue ? RideStatus.Active : RideStatus.Open; await uow.BeginAsync(ct); try { cancelRepo.Update(cancelRequest); rideRepo.Update(ride); await uow.CommitAsync(ct); } catch { await uow.RollbackAsync(ct); throw; } return ToCancelResponse(cancelRequest); } public async Task GetCancelRequestAsync(Guid rideId) { var cancelRequest = await cancelRepo.GetByRideIdAsync(rideId); return cancelRequest is null ? null : ToCancelResponse(cancelRequest); } public async Task GetBreakdownAssistAsync(Guid rideId) { var strandedRide = await rideRepo.GetByIdAsync(rideId) ?? throw new KeyNotFoundException("Ride not found"); var activeRides = await rideRepo.GetActiveRidesAsync(rideId); var activeRideResponses = new List(); foreach (var ride in activeRides) { var stockLevels = await stockRepo.GetBalancesByLocationAsync(ride.FK_VehicleId, LocationType.Vehicle); activeRideResponses.Add(new BreakdownAssistRideResponse( ride.Id, ride.FK_VehicleId, ride.Vehicle?.NumberPlate ?? string.Empty, ride.FK_DriverProfileId, ride.DriverProfile?.User?.FullName ?? string.Empty, ride.FK_RouteId, ride.Route?.RouteName ?? string.Empty, stockLevels.Select(b => new StockBalanceResponse( b.FK_ItemId, b.Item.Name, b.LocationId, b.LocationType, b.Quantity, b.UpdatedAt)).ToList())); } var openOrders = await rideRepo.GetActiveOrdersByRouteIdAsync(strandedRide.FK_RouteId); return new BreakdownAssistResponse( strandedRide.Id, activeRideResponses, openOrders.Select(o => new BreakdownAssistOrderResponse( o.Id, o.StoreId, o.StoreName, o.OrderDate, o.TotalAmount)).ToList()); } private async Task CheckDriverComplianceAsync(Guid driverProfileId) { var driver = await driverRepo.GetByIdAsync(driverProfileId) ?? throw new InvalidOperationException("Driver profile not found"); if (!driver.IsActive) throw new InvalidOperationException("Driver profile is inactive"); var config = await complianceRepo.GetAsync(); if (config is null) return; var today = DateTime.Now.Date; if (config.EnforceLicenseExpiry && driver.LicenseExpiry.Date <= today) throw new InvalidOperationException("Driver license has expired"); if (config.EnforceMedicalClearance && driver.MedicalClearanceExpiry.Date <= today) 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.RideType, r.Status, r.FK_VehicleId, r.Vehicle?.NumberPlate ?? string.Empty, r.FK_DriverProfileId, r.DriverProfile?.User?.FullName ?? string.Empty, r.FK_RouteId, r.Route?.RouteName ?? string.Empty, r.FK_SourceWarehouseId, r.SourceWarehouse?.Name ?? string.Empty, r.Meterstart, r.Meterend, r.StartedAt, r.ActivatedAt, r.ClosedAt, r.CreatedAt); private static RideCancelRequestResponse ToCancelResponse(RideCancelRequest c) => new(c.Id, c.FK_RideId, c.TypeCode, c.Reason, c.RequestedBy, c.RequestedAt, c.Status, c.ApprovedBy, c.ApprovedAt); }