ride reconcilation with driver profile update
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user