Files
2026-08-21 02:08:11 +05:30

354 lines
14 KiB
C#

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<PagedResponse<RideResponse>> 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<RideResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
}
public async Task<RideResponse?> GetByIdAsync(Guid id)
{
var ride = await rideRepo.GetByIdAsync(id);
return ride is null ? null : ToResponse(ride);
}
public async Task<RideResponse> 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<RideResponse> 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<RideResponse> 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<RideResponse> 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<RideResponse> 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<RideCancelRequestResponse> 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<PagedResponse<RideCancelRequestResponse>> GetCancelRequestsAsync(CancelRequestStatus? status, int page, int pageSize)
{
var total = await cancelRepo.GetCountAsync(status);
var items = await cancelRepo.GetAllAsync(status, page, pageSize);
return new PagedResponse<RideCancelRequestResponse>(page, pageSize, total, items.Select(ToCancelResponse).ToList());
}
public async Task<RideCancelRequestResponse> 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<RideCancelRequestResponse> 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<RideCancelRequestResponse?> GetCancelRequestAsync(Guid rideId)
{
var cancelRequest = await cancelRepo.GetByRideIdAsync(rideId);
return cancelRequest is null ? null : ToCancelResponse(cancelRequest);
}
public async Task<BreakdownAssistResponse> 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<BreakdownAssistRideResponse>();
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);
}