ride reconcilation with driver profile update

This commit is contained in:
Kalana B. Thilakarathna
2026-08-31 12:14:30 +05:30
parent 75b5035354
commit 76ca9f874d
24 changed files with 3912 additions and 112 deletions
@@ -1,12 +0,0 @@
namespace Distribution.Features.DriverComplianceConfig;
public record DriverComplianceConfigResponse(
Guid Id,
bool EnforceLicenseExpiry,
bool EnforceMedicalClearance,
DateTime UpdatedAt,
Guid UpdatedBy);
public record UpdateDriverComplianceConfigRequest(
bool EnforceLicenseExpiry,
bool EnforceMedicalClearance);
@@ -1,16 +0,0 @@
using Distribution.Data;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.DriverComplianceConfig;
public class DriverComplianceConfigRepository(AppDBContext context) : IDriverComplianceConfigRepository
{
public async Task<Models.DriverComplianceConfig?> GetAsync() =>
await context.DriverComplianceConfigs.FirstOrDefaultAsync();
public async Task AddAsync(Models.DriverComplianceConfig config) =>
await context.DriverComplianceConfigs.AddAsync(config);
public void Update(Models.DriverComplianceConfig config) =>
context.DriverComplianceConfigs.Update(config);
}
@@ -1,62 +0,0 @@
using Distribution.Infra.UoW;
namespace Distribution.Features.DriverComplianceConfig;
public class DriverComplianceConfigService(IDriverComplianceConfigRepository repo, IUnitOfWork uow)
{
public async Task<DriverComplianceConfigResponse?> GetAsync()
{
var config = await repo.GetAsync();
return config is null
? null
: new DriverComplianceConfigResponse(
config.Id,
config.EnforceLicenseExpiry,
config.EnforceMedicalClearance,
config.UpdatedAt,
config.UpdatedBy);
}
public async Task<DriverComplianceConfigResponse> UpsertAsync(UpdateDriverComplianceConfigRequest request, Guid updatedBy, CancellationToken ct = default)
{
var config = await repo.GetAsync();
await uow.BeginAsync(ct);
try
{
if (config is null)
{
config = new global::Distribution.Models.DriverComplianceConfig
{
EnforceLicenseExpiry = request.EnforceLicenseExpiry,
EnforceMedicalClearance = request.EnforceMedicalClearance,
UpdatedBy = updatedBy,
UpdatedAt = DateTime.Now
};
await repo.AddAsync(config);
}
else
{
config.EnforceLicenseExpiry = request.EnforceLicenseExpiry;
config.EnforceMedicalClearance = request.EnforceMedicalClearance;
config.UpdatedBy = updatedBy;
config.UpdatedAt = DateTime.Now;
repo.Update(config);
}
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new DriverComplianceConfigResponse(
config.Id,
config.EnforceLicenseExpiry,
config.EnforceMedicalClearance,
config.UpdatedAt,
config.UpdatedBy);
}
}
@@ -1,8 +0,0 @@
namespace Distribution.Features.DriverComplianceConfig;
public interface IDriverComplianceConfigRepository
{
Task<Models.DriverComplianceConfig?> GetAsync();
Task AddAsync(Models.DriverComplianceConfig config);
void Update(Models.DriverComplianceConfig config);
}
@@ -1,4 +1,3 @@
using Distribution.Features.DriverComplianceConfig;
using Distribution.Infra.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -8,7 +7,6 @@ namespace Distribution.Features.DriverProfile;
[Route("api/driver-profiles")]
public class DriverProfileController(
DriverProfileService service,
DriverComplianceConfigService complianceService,
ICurrentUserService currentUser) : ControllerBase
{
[HttpGet]
@@ -60,7 +58,7 @@ public class DriverProfileController(
[HttpGet("compliance")]
public async Task<IActionResult> GetCompliance()
{
var result = await complianceService.GetAsync();
var result = await service.GetComplianceConfigAsync();
return result is null ? NoContent() : Ok(result);
}
@@ -68,7 +66,7 @@ public class DriverProfileController(
public async Task<IActionResult> UpsertCompliance(UpdateDriverComplianceConfigRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
var result = await complianceService.UpsertAsync(request, userId, ct);
var result = await service.UpsertComplianceConfigAsync(request, userId, ct);
return Ok(result);
}
}
@@ -20,3 +20,14 @@ public record UpdateDriverProfileRequest(
string LicenseNumber,
DateTime LicenseExpiry,
DateTime MedicalClearanceExpiry);
public record DriverComplianceConfigResponse(
Guid Id,
bool EnforceLicenseExpiry,
bool EnforceMedicalClearance,
DateTime UpdatedAt,
Guid UpdatedBy);
public record UpdateDriverComplianceConfigRequest(
bool EnforceLicenseExpiry,
bool EnforceMedicalClearance);
@@ -37,4 +37,13 @@ public class DriverProfileRepository(AppDBContext context) : IDriverProfileRepos
profile.IsActive = false;
context.DriverProfiles.Update(profile);
}
public async Task<global::Distribution.Models.DriverComplianceConfig?> GetComplianceConfigAsync() =>
await context.DriverComplianceConfigs.FirstOrDefaultAsync();
public async Task AddComplianceConfigAsync(global::Distribution.Models.DriverComplianceConfig config) =>
await context.DriverComplianceConfigs.AddAsync(config);
public void UpdateComplianceConfig(global::Distribution.Models.DriverComplianceConfig config) =>
context.DriverComplianceConfigs.Update(config);
}
@@ -98,6 +98,56 @@ public class DriverProfileService(IDriverProfileRepository repo, IUnitOfWork uow
return true;
}
public async Task<DriverComplianceConfigResponse?> GetComplianceConfigAsync()
{
var config = await repo.GetComplianceConfigAsync();
return config is null ? null
: new DriverComplianceConfigResponse(
config.Id, config.EnforceLicenseExpiry, config.EnforceMedicalClearance,
config.UpdatedAt, config.UpdatedBy);
}
public async Task<DriverComplianceConfigResponse> UpsertComplianceConfigAsync(
UpdateDriverComplianceConfigRequest request, Guid updatedBy, CancellationToken ct = default)
{
var config = await repo.GetComplianceConfigAsync();
await uow.BeginAsync(ct);
try
{
if (config is null)
{
config = new global::Distribution.Models.DriverComplianceConfig
{
EnforceLicenseExpiry = request.EnforceLicenseExpiry,
EnforceMedicalClearance = request.EnforceMedicalClearance,
UpdatedBy = updatedBy,
UpdatedAt = DateTime.Now
};
await repo.AddComplianceConfigAsync(config);
}
else
{
config.EnforceLicenseExpiry = request.EnforceLicenseExpiry;
config.EnforceMedicalClearance = request.EnforceMedicalClearance;
config.UpdatedBy = updatedBy;
config.UpdatedAt = DateTime.Now;
repo.UpdateComplianceConfig(config);
}
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new DriverComplianceConfigResponse(
config.Id, config.EnforceLicenseExpiry, config.EnforceMedicalClearance,
config.UpdatedAt, config.UpdatedBy);
}
private static DriverProfileResponse ToResponse(global::Distribution.Models.DriverProfile p) =>
new(p.Id, p.FK_UserId, p.User?.FullName ?? string.Empty,
p.LicenseNumber, p.LicenseExpiry, p.MedicalClearanceExpiry,
@@ -9,4 +9,8 @@ public interface IDriverProfileRepository
Task AddAsync(global::Distribution.Models.DriverProfile profile);
void Update(global::Distribution.Models.DriverProfile profile);
void Deactivate(global::Distribution.Models.DriverProfile profile);
Task<global::Distribution.Models.DriverComplianceConfig?> GetComplianceConfigAsync();
Task AddComplianceConfigAsync(global::Distribution.Models.DriverComplianceConfig config);
void UpdateComplianceConfig(global::Distribution.Models.DriverComplianceConfig config);
}
@@ -0,0 +1,21 @@
using Distribution.Models;
namespace Distribution.Features.Reconciliation;
public interface IReconciliationRepository
{
Task AddItemCountAsync(RideItemCount count);
Task<List<RideItemCount>> GetItemCountsByRideAsync(Guid rideId);
Task AddVarianceAsync(RideVariance variance);
Task<List<RideVariance>> GetVariancesByRideAsync(Guid rideId);
Task<RideVariance?> GetVarianceByIdAsync(Guid id);
void UpdateVariance(RideVariance variance);
Task<bool> HasPendingVariancesAsync(Guid rideId);
Task AddCloseSummaryAsync(RideCloseSummary summary);
Task<RideCloseSummary?> GetCloseSummaryByRideAsync(Guid rideId);
/// <summary>Sum of Cash payments on sales belonging to the given ride.</summary>
Task<decimal> GetCashCollectedByRideAsync(Guid rideId);
}
@@ -0,0 +1,73 @@
using Distribution.Infra.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Reconciliation;
[ApiController]
[Route("api/rides/{rideId:guid}")]
public class ReconciliationController(ReconciliationService service, ICurrentUserService currentUser) : ControllerBase
{
[HttpPost("cycle-count")]
public async Task<IActionResult> SubmitCycleCount(Guid rideId, SubmitCycleCountRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.SubmitCycleCountAsync(rideId, request, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpGet("variances")]
public async Task<IActionResult> GetVariances(Guid rideId)
{
try
{
var result = await service.GetVariancesAsync(rideId);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("variances/{id:guid}/approve")]
public async Task<IActionResult> ApproveVariance(Guid rideId, Guid id, ResolveVarianceRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.ApproveVarianceAsync(id, request, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("variances/{id:guid}/reject")]
public async Task<IActionResult> RejectVariance(Guid rideId, Guid id, ResolveVarianceRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.RejectVarianceAsync(id, request, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("close-ride")]
public async Task<IActionResult> CloseRide(Guid rideId, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.CloseRideAsync(rideId, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
}
@@ -0,0 +1,19 @@
using Distribution.Models;
namespace Distribution.Features.Reconciliation;
public record CycleCountItemLine(Guid ItemId, decimal PhysicalQuantity);
public record SubmitCycleCountRequest(List<CycleCountItemLine> Items, decimal CashOnHand);
public record RideVarianceResponse(
Guid Id, Guid RideId, Guid? ItemId, string? ItemName,
bool IsCash, decimal ExpectedQuantity, decimal ActualQuantity, decimal VarianceQuantity,
string? ReasonCode, VarianceStatus Status, Guid? ResolvedByUserId, DateTime? ResolvedAt);
public record ResolveVarianceRequest(string ReasonCode);
public record CycleCountResult(bool HasVariances, List<RideVarianceResponse> Variances);
public record RideCloseSummaryResponse(
Guid Id, Guid RideId, decimal ExpectedCash, decimal ActualCash, decimal CashVariance,
Guid ClosedByUserId, DateTime ClosedAt);
@@ -0,0 +1,51 @@
using Distribution.Data;
using Distribution.Models;
using Microsoft.EntityFrameworkCore;
namespace Distribution.Features.Reconciliation;
public class ReconciliationRepository(AppDBContext db) : IReconciliationRepository
{
public async Task AddItemCountAsync(RideItemCount count)
=> await db.RideItemCounts.AddAsync(count);
public async Task<List<RideItemCount>> GetItemCountsByRideAsync(Guid rideId)
=> await db.RideItemCounts
.Where(c => c.FK_RideId == rideId)
.ToListAsync();
public async Task AddVarianceAsync(RideVariance variance)
=> await db.RideVariances.AddAsync(variance);
public async Task<List<RideVariance>> GetVariancesByRideAsync(Guid rideId)
=> await db.RideVariances
.Include(v => v.Item)
.Include(v => v.ResolvedBy)
.Where(v => v.FK_RideId == rideId)
.ToListAsync();
public async Task<RideVariance?> GetVarianceByIdAsync(Guid id)
=> await db.RideVariances
.Include(v => v.Item)
.Include(v => v.ResolvedBy)
.FirstOrDefaultAsync(v => v.Id == id);
public void UpdateVariance(RideVariance variance)
=> db.RideVariances.Update(variance);
public async Task<bool> HasPendingVariancesAsync(Guid rideId)
=> await db.RideVariances
.AnyAsync(v => v.FK_RideId == rideId && v.Status == VarianceStatus.Pending);
public async Task AddCloseSummaryAsync(RideCloseSummary summary)
=> await db.RideCloseSummaries.AddAsync(summary);
public async Task<RideCloseSummary?> GetCloseSummaryByRideAsync(Guid rideId)
=> await db.RideCloseSummaries
.FirstOrDefaultAsync(s => s.FK_RideId == rideId);
public async Task<decimal> GetCashCollectedByRideAsync(Guid rideId)
=> await db.Payments
.Where(p => p.Sale.FK_RideId == rideId && p.Method == PaymentMethod.Cash)
.SumAsync(p => p.Amount);
}
@@ -0,0 +1,396 @@
using Distribution.Features.Ride;
using Distribution.Features.Stock;
using Distribution.Infra.UoW;
using Distribution.Models;
namespace Distribution.Features.Reconciliation;
public class ReconciliationService(
IReconciliationRepository reconciliationRepo,
IRideRepository rideRepo,
IStockRepository stockRepo,
IUnitOfWork uow)
{
public async Task<CycleCountResult> SubmitCycleCountAsync(
Guid rideId, SubmitCycleCountRequest request, Guid userId, CancellationToken ct = default)
{
var ride = await rideRepo.GetByIdAsync(rideId)
?? throw new KeyNotFoundException("Ride not found");
if (ride.Status != RideStatus.Active)
throw new InvalidOperationException($"Cannot submit cycle count for a ride in {ride.Status} status");
var existingCounts = await reconciliationRepo.GetItemCountsByRideAsync(rideId);
if (existingCounts.Count > 0)
throw new InvalidOperationException("Cycle count has already been submitted for this ride");
// Build submitted counts lookup
var submittedCounts = request.Items.ToDictionary(i => i.ItemId, i => i.PhysicalQuantity);
// Save all item count records
var itemCounts = request.Items.Select(line => new RideItemCount
{
FK_RideId = rideId,
FK_ItemId = line.ItemId,
PhysicalQuantity = line.PhysicalQuantity
}).ToList();
// Get current vehicle stock balances
var vehicleBalances = await stockRepo.GetBalancesByLocationAsync(ride.FK_VehicleId, LocationType.Vehicle);
var variances = new List<RideVariance>();
// Compare each stock balance against the physical count
foreach (var balance in vehicleBalances)
{
var physicalQty = submittedCounts.TryGetValue(balance.FK_ItemId, out var qty) ? qty : 0m;
if (balance.Quantity != physicalQty)
{
variances.Add(new RideVariance
{
FK_RideId = rideId,
FK_ItemId = balance.FK_ItemId,
IsCash = false,
ExpectedQuantity = balance.Quantity,
ActualQuantity = physicalQty,
VarianceQuantity = balance.Quantity - physicalQty,
Status = VarianceStatus.Pending
});
}
}
// Cash variance check
var expectedCash = await reconciliationRepo.GetCashCollectedByRideAsync(rideId);
if (expectedCash != request.CashOnHand)
{
variances.Add(new RideVariance
{
FK_RideId = rideId,
FK_ItemId = null,
IsCash = true,
ExpectedQuantity = expectedCash,
ActualQuantity = request.CashOnHand,
VarianceQuantity = expectedCash - request.CashOnHand,
Status = VarianceStatus.Pending
});
}
ride.Status = RideStatus.PendingClose;
await uow.BeginAsync(ct);
try
{
foreach (var count in itemCounts)
await reconciliationRepo.AddItemCountAsync(count);
foreach (var variance in variances)
await reconciliationRepo.AddVarianceAsync(variance);
rideRepo.Update(ride);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
var varianceResponses = variances.Select(ToVarianceResponse).ToList();
return new CycleCountResult(varianceResponses.Count > 0, varianceResponses);
}
public async Task<List<RideVarianceResponse>> GetVariancesAsync(Guid rideId)
{
var variances = await reconciliationRepo.GetVariancesByRideAsync(rideId);
return variances.Select(ToVarianceResponse).ToList();
}
public async Task<RideVarianceResponse> ApproveVarianceAsync(
Guid varianceId, ResolveVarianceRequest request, Guid userId, CancellationToken ct = default)
{
var variance = await reconciliationRepo.GetVarianceByIdAsync(varianceId)
?? throw new KeyNotFoundException("Variance not found");
if (variance.Status != VarianceStatus.Pending)
throw new InvalidOperationException($"Variance is not in Pending status");
var ride = await rideRepo.GetByIdAsync(variance.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
if (ride.Status != RideStatus.PendingClose)
throw new InvalidOperationException($"Ride is not in PendingClose status");
StockMovement? movement = null;
StockBalance? balance = null;
if (!variance.IsCash)
{
// adjustmentQty = -variance.VarianceQuantity
// positive variance = short → debit vehicle (reduce stock)
// negative variance = surplus → credit vehicle (add stock)
var adjustmentQty = -variance.VarianceQuantity;
balance = await stockRepo.GetBalanceAsync(variance.FK_ItemId!.Value, ride.FK_VehicleId);
if (adjustmentQty < 0)
{
// Debit vehicle: FromLocationId=vehicleId, Quantity=abs(adjustmentQty)
var absQty = Math.Abs(adjustmentQty);
movement = new StockMovement
{
FK_ItemId = variance.FK_ItemId!.Value,
FromLocationId = ride.FK_VehicleId,
FromLocationType = LocationType.Vehicle,
ToLocationId = null,
ToLocationType = null,
Quantity = absQty,
Type = MovementType.Adjustment,
ReasonCode = request.ReasonCode,
ReferenceId = variance.Id,
CreatedAt = DateTime.Now,
CreatedBy = userId
};
if (balance is not null)
{
balance.Quantity -= absQty;
balance.UpdatedAt = DateTime.Now;
}
}
else if (adjustmentQty > 0)
{
// Credit vehicle: ToLocationId=vehicleId, Quantity=adjustmentQty
movement = new StockMovement
{
FK_ItemId = variance.FK_ItemId!.Value,
FromLocationId = null,
FromLocationType = null,
ToLocationId = ride.FK_VehicleId,
ToLocationType = LocationType.Vehicle,
Quantity = adjustmentQty,
Type = MovementType.Adjustment,
ReasonCode = request.ReasonCode,
ReferenceId = variance.Id,
CreatedAt = DateTime.Now,
CreatedBy = userId
};
if (balance is not null)
{
balance.Quantity += adjustmentQty;
balance.UpdatedAt = DateTime.Now;
}
else
{
balance = new StockBalance
{
FK_ItemId = variance.FK_ItemId!.Value,
LocationId = ride.FK_VehicleId,
LocationType = LocationType.Vehicle,
Quantity = adjustmentQty,
UpdatedAt = DateTime.Now
};
}
}
}
variance.Status = VarianceStatus.Approved;
variance.ReasonCode = request.ReasonCode;
variance.FK_ResolvedByUserId = userId;
variance.ResolvedAt = DateTime.Now;
await uow.BeginAsync(ct);
try
{
reconciliationRepo.UpdateVariance(variance);
if (movement is not null)
await stockRepo.AddMovementAsync(movement);
if (balance is not null)
stockRepo.UpdateBalance(balance);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToVarianceResponse(variance);
}
public async Task<RideVarianceResponse> RejectVarianceAsync(
Guid varianceId, ResolveVarianceRequest request, Guid userId, CancellationToken ct = default)
{
var variance = await reconciliationRepo.GetVarianceByIdAsync(varianceId)
?? throw new KeyNotFoundException("Variance not found");
if (variance.Status != VarianceStatus.Pending)
throw new InvalidOperationException($"Variance is not in Pending status");
var ride = await rideRepo.GetByIdAsync(variance.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
if (ride.Status != RideStatus.PendingClose)
throw new InvalidOperationException($"Ride is not in PendingClose status");
StockMovement? movement = null;
StockBalance? balance = null;
if (!variance.IsCash)
{
var adjustmentQty = -variance.VarianceQuantity;
balance = await stockRepo.GetBalanceAsync(variance.FK_ItemId!.Value, ride.FK_VehicleId);
if (adjustmentQty < 0)
{
var absQty = Math.Abs(adjustmentQty);
movement = new StockMovement
{
FK_ItemId = variance.FK_ItemId!.Value,
FromLocationId = ride.FK_VehicleId,
FromLocationType = LocationType.Vehicle,
ToLocationId = null,
ToLocationType = null,
Quantity = absQty,
Type = MovementType.Adjustment,
ReasonCode = request.ReasonCode,
ReferenceId = variance.Id,
CreatedAt = DateTime.Now,
CreatedBy = userId
};
if (balance is not null)
{
balance.Quantity -= absQty;
balance.UpdatedAt = DateTime.Now;
}
}
else if (adjustmentQty > 0)
{
movement = new StockMovement
{
FK_ItemId = variance.FK_ItemId!.Value,
FromLocationId = null,
FromLocationType = null,
ToLocationId = ride.FK_VehicleId,
ToLocationType = LocationType.Vehicle,
Quantity = adjustmentQty,
Type = MovementType.Adjustment,
ReasonCode = request.ReasonCode,
ReferenceId = variance.Id,
CreatedAt = DateTime.Now,
CreatedBy = userId
};
if (balance is not null)
{
balance.Quantity += adjustmentQty;
balance.UpdatedAt = DateTime.Now;
}
else
{
balance = new StockBalance
{
FK_ItemId = variance.FK_ItemId!.Value,
LocationId = ride.FK_VehicleId,
LocationType = LocationType.Vehicle,
Quantity = adjustmentQty,
UpdatedAt = DateTime.Now
};
}
}
}
variance.Status = VarianceStatus.Rejected;
variance.ReasonCode = request.ReasonCode;
variance.FK_ResolvedByUserId = userId;
variance.ResolvedAt = DateTime.Now;
await uow.BeginAsync(ct);
try
{
reconciliationRepo.UpdateVariance(variance);
if (movement is not null)
await stockRepo.AddMovementAsync(movement);
if (balance is not null)
stockRepo.UpdateBalance(balance);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return ToVarianceResponse(variance);
}
public async Task<RideCloseSummaryResponse> CloseRideAsync(
Guid rideId, Guid userId, CancellationToken ct = default)
{
var ride = await rideRepo.GetByIdAsync(rideId)
?? throw new KeyNotFoundException("Ride not found");
if (ride.Status != RideStatus.PendingClose)
throw new InvalidOperationException($"Cannot close a ride in {ride.Status} status");
if (await reconciliationRepo.HasPendingVariancesAsync(rideId))
throw new InvalidOperationException("All variances must be resolved before the ride can be closed");
var allVariances = await reconciliationRepo.GetVariancesByRideAsync(rideId);
var cashVariance = allVariances.FirstOrDefault(v => v.IsCash);
var expectedCash = cashVariance?.ExpectedQuantity ?? 0m;
var actualCash = cashVariance?.ActualQuantity ?? 0m;
var cashVarianceAmount = cashVariance?.VarianceQuantity ?? 0m;
var summary = new RideCloseSummary
{
FK_RideId = rideId,
ExpectedCash = expectedCash,
ActualCash = actualCash,
CashVariance = cashVarianceAmount,
FK_ClosedByUserId = userId,
ClosedAt = DateTime.Now
};
ride.Status = RideStatus.Closed;
ride.ClosedAt = DateTime.Now;
await uow.BeginAsync(ct);
try
{
await reconciliationRepo.AddCloseSummaryAsync(summary);
rideRepo.Update(ride);
await uow.CommitAsync(ct);
}
catch
{
await uow.RollbackAsync(ct);
throw;
}
return new RideCloseSummaryResponse(
summary.Id,
summary.FK_RideId,
summary.ExpectedCash,
summary.ActualCash,
summary.CashVariance,
summary.FK_ClosedByUserId,
summary.ClosedAt);
}
private static RideVarianceResponse ToVarianceResponse(RideVariance v) =>
new(v.Id, v.FK_RideId, v.FK_ItemId, v.Item?.Name,
v.IsCash, v.ExpectedQuantity, v.ActualQuantity, v.VarianceQuantity,
v.ReasonCode, v.Status, v.FK_ResolvedByUserId, v.ResolvedAt);
}
+2 -6
View File
@@ -1,5 +1,4 @@
using Distribution.Features.Stock;
using Distribution.Features.DriverComplianceConfig;
using Distribution.Features.DriverProfile;
using Distribution.Infra.UoW;
using Distribution.Models;
@@ -9,7 +8,6 @@ namespace Distribution.Features.Ride;
public class RideService(
IRideRepository rideRepo,
IDriverProfileRepository driverRepo,
IDriverComplianceConfigRepository complianceRepo,
IRideCancelRequestRepository cancelRepo,
Features.Vehicle.IVehicleRepository vehicleRepo,
IStockRepository stockRepo,
@@ -138,11 +136,9 @@ public class RideService(
var ride = await rideRepo.GetByIdAsync(rideId)
?? throw new KeyNotFoundException("Ride not found");
if (ride.Status != RideStatus.Active)
if (ride.Status != RideStatus.PendingClose)
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;
@@ -319,7 +315,7 @@ public class RideService(
if (!driver.IsActive)
throw new InvalidOperationException("Driver profile is inactive");
var config = await complianceRepo.GetAsync();
var config = await driverRepo.GetComplianceConfigAsync();
if (config is null) return;
var today = DateTime.Now.Date;