Refactor sales pipeline: ShopPO, Invoice, SysConfig models

Major refactor replacing Sale/Order with ShopPO → Invoice → LoadingSheet flow. Removed legacy Sale/Order models and features; introduced ShopPO, ShopPOItem, Invoice, LoadingSheetOrder, and SysConfig models. Updated related entities, enums, repositories, and controllers. Added new migrations for schema changes and system config. Overhauled business logic and DI registrations. Updated documentation with migration plan.
This commit is contained in:
Kalana B. Thilakarathna
2026-09-17 10:03:22 +05:30
parent 6c40421b2d
commit 93f2f1bd3b
83 changed files with 11274 additions and 2538 deletions
@@ -8,7 +8,6 @@ public interface ILoadingSheetRepository
Task<LoadingSheet?> GetActiveByRideIdAsync(Guid rideId);
Task<List<LoadingSheet>> GetAllAsync(Guid? rideId, LoadingSheetStatus? status, int page, int pageSize);
Task<int> GetCountAsync(Guid? rideId, LoadingSheetStatus? status);
Task<List<(Guid ItemId, decimal TotalQty)>> GetAggregatedOrderItemsByRouteIdAsync(Guid routeId);
Task AddAsync(LoadingSheet sheet);
void Update(LoadingSheet sheet);
void RemoveItems(IEnumerable<LoadingSheetItem> items);
@@ -1,4 +1,4 @@
using Distribution.Models;
using Distribution.Models;
using Distribution.Infra.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -29,9 +29,10 @@ public class LoadingSheetController(LoadingSheetService service, ICurrentUserSer
[HttpPost]
public async Task<IActionResult> Create(CreateLoadingSheetRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.CreateAsync(request, ct);
var result = await service.CreateAsync(request, userId, ct);
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
}
catch (KeyNotFoundException) { return NotFound(); }
@@ -50,90 +51,13 @@ public class LoadingSheetController(LoadingSheetService service, ICurrentUserSer
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("{id:guid}/confirm-dispatch")]
public async Task<IActionResult> ConfirmDispatch(Guid id, CancellationToken ct)
[HttpPost("{id:guid}/confirm")]
public async Task<IActionResult> Confirm(Guid id, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.ConfirmDispatchAsync(id, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPut("{id:guid}/receipt-items")]
public async Task<IActionResult> SetReceiptItems(Guid id, SetReceiptItemsRequest request, CancellationToken ct)
{
try
{
var result = await service.SetReceiptItemsAsync(id, request, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("{id:guid}/confirm-receipt")]
public async Task<IActionResult> ConfirmReceipt(Guid id, ConfirmReceiptRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.ConfirmReceiptAsync(id, request, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPost("{id:guid}/resolve")]
public async Task<IActionResult> Resolve(Guid id, ResolveDiscrepancyRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.ResolveAsync(id, request, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPatch("{id:guid}/dispatch")]
public async Task<IActionResult> PatchDispatch(Guid id, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.ConfirmDispatchAsync(id, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPatch("{id:guid}/receipt")]
public async Task<IActionResult> PatchReceipt(Guid id, PatchReceiptRequest request, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.PatchReceiptAsync(id, request.Items, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
catch (InvalidOperationException ex) { return BadRequest(new { error = ex.Message }); }
}
[HttpPatch("{id:guid}/resolve")]
public async Task<IActionResult> PatchResolve(Guid id, CancellationToken ct)
{
var userId = currentUser.UserId ?? throw new UnauthorizedAccessException();
try
{
var result = await service.PatchResolveAsync(id, userId, ct);
var result = await service.ConfirmAsync(id, userId, ct);
return Ok(result);
}
catch (KeyNotFoundException) { return NotFound(); }
@@ -5,37 +5,27 @@ namespace Distribution.Features.LoadingEvent;
public record LoadingSheetResponse(
Guid Id,
Guid FK_RideId,
Guid FK_SourceWarehouseId,
string WarehouseName,
LoadingSheetStatus Status,
DateTime? DispatchConfirmedAt,
Guid? DispatchConfirmedBy,
DateTime? ReceiptConfirmedAt,
Guid? ReceiptConfirmedBy,
Guid PopulatedBy,
DateTime PopulatedAt,
DateTime? ConfirmedAt,
Guid? ConfirmedBy,
DateTime CreatedAt,
List<LoadingSheetItemResponse> Items);
List<LoadingSheetItemResponse> Items,
List<LoadingSheetOrderResponse> Orders);
public record LoadingSheetItemResponse(
Guid Id,
Guid FK_ItemId,
string ItemName,
Guid FK_WarehouseId,
string WarehouseName,
decimal QuantityDispatched,
decimal? QuantityReceived,
decimal? QuantityResolved);
Guid? FK_ShopPOItemId);
public record LoadingSheetOrderResponse(Guid ShopPOId);
public record CreateLoadingSheetRequest(Guid FK_RideId);
public record SetItemsRequest(List<SetItemInput> Items);
public record SetItemInput(Guid FK_ItemId, decimal Quantity);
public record SetReceiptItemsRequest(List<ReceiptItemInput> Items);
public record ReceiptItemInput(Guid FK_ItemId, decimal QuantityReceived);
public record ConfirmReceiptRequest(int Meterstart);
public record ResolveDiscrepancyRequest(List<ResolveItemInput> Items, int Meterstart);
public record ResolveItemInput(Guid FK_ItemId, decimal QuantityResolved);
// Combined PATCH endpoints used by the frontend
public record PatchReceiptItemInput(Guid ItemId, decimal QuantityReceived);
public record PatchReceiptRequest(List<PatchReceiptItemInput> Items);
public record SetItemInput(Guid FK_ItemId, Guid FK_WarehouseId, decimal Quantity, Guid? FK_ShopPOItemId = null);
@@ -8,20 +8,20 @@ public class LoadingSheetRepository(AppDBContext context) : ILoadingSheetReposit
{
public async Task<LoadingSheet?> GetByIdAsync(Guid id) =>
await context.LoadingSheets
.Include(s => s.SourceWarehouse)
.Include(s => s.Items).ThenInclude(i => i.Item)
.Include(s => s.Items).ThenInclude(i => i.Warehouse)
.Include(s => s.Orders)
.FirstOrDefaultAsync(s => s.Id == id);
public async Task<LoadingSheet?> GetActiveByRideIdAsync(Guid rideId) =>
await context.LoadingSheets
.FirstOrDefaultAsync(s =>
s.FK_RideId == rideId &&
s.Status != LoadingSheetStatus.ReceiptConfirmed &&
s.Status != LoadingSheetStatus.Resolved);
.FirstOrDefaultAsync(s => s.FK_RideId == rideId && s.Status == LoadingSheetStatus.Planned);
public async Task<List<LoadingSheet>> GetAllAsync(Guid? rideId, LoadingSheetStatus? status, int page, int pageSize) =>
await context.LoadingSheets
.Include(s => s.SourceWarehouse)
.Include(s => s.Items).ThenInclude(i => i.Item)
.Include(s => s.Items).ThenInclude(i => i.Warehouse)
.Include(s => s.Orders)
.Where(s =>
(rideId == null || s.FK_RideId == rideId) &&
(status == null || s.Status == status))
@@ -36,19 +36,6 @@ public class LoadingSheetRepository(AppDBContext context) : ILoadingSheetReposit
(rideId == null || s.FK_RideId == rideId) &&
(status == null || s.Status == status));
public async Task<List<(Guid ItemId, decimal TotalQty)>> GetAggregatedOrderItemsByRouteIdAsync(Guid routeId)
{
var groups = await context.OrderItems
.Where(oi => oi.IsActive
&& oi.Order.IsActive
&& oi.Order.Store.FK_RouteId == routeId
&& (oi.Order.Status == OrderStatus.Pending || oi.Order.Status == OrderStatus.PartiallyFulfilled))
.GroupBy(oi => oi.FK_ItemId)
.Select(g => new { ItemId = g.Key, TotalQty = g.Sum(oi => (decimal)(oi.Quantity - oi.QuantityFulfilled)) })
.ToListAsync();
return groups.Select(x => (x.ItemId, x.TotalQty)).ToList();
}
public async Task AddAsync(LoadingSheet sheet) =>
await context.LoadingSheets.AddAsync(sheet);
@@ -24,7 +24,7 @@ public class LoadingSheetService(
return sheet is null ? null : ToResponse(sheet);
}
public async Task<LoadingSheetResponse> CreateAsync(CreateLoadingSheetRequest request, CancellationToken ct = default)
public async Task<LoadingSheetResponse> CreateAsync(CreateLoadingSheetRequest request, Guid userId, CancellationToken ct = default)
{
var ride = await rideRepo.GetByIdAsync(request.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
@@ -34,22 +34,16 @@ public class LoadingSheetService(
var existing = await repo.GetActiveByRideIdAsync(request.FK_RideId);
if (existing is not null)
throw new InvalidOperationException("This ride already has an active loading sheet");
throw new InvalidOperationException("This ride already has a Planned loading sheet");
var sheet = new LoadingSheet
{
FK_RideId = ride.Id,
FK_SourceWarehouseId = ride.FK_SourceWarehouseId,
Status = LoadingSheetStatus.Pending
Status = LoadingSheetStatus.Planned,
PopulatedBy = userId,
PopulatedAt = DateTime.Now
};
if (ride.RideType == RideType.Delivery)
{
var orderItems = await repo.GetAggregatedOrderItemsByRouteIdAsync(ride.FK_RouteId);
foreach (var (itemId, qty) in orderItems)
sheet.Items.Add(new LoadingSheetItem { FK_ItemId = itemId, QuantityDispatched = qty });
}
await uow.BeginAsync(ct);
try
{
@@ -67,8 +61,8 @@ public class LoadingSheetService(
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.Pending)
throw new InvalidOperationException("Items can only be changed while the sheet is Pending");
if (sheet.Status != LoadingSheetStatus.Planned)
throw new InvalidOperationException("Items can only be changed while the sheet is Planned");
await uow.BeginAsync(ct);
try
@@ -78,6 +72,8 @@ public class LoadingSheetService(
{
FK_LoadingSheetId = sheet.Id,
FK_ItemId = i.FK_ItemId,
FK_WarehouseId = i.FK_WarehouseId,
FK_ShopPOItemId = i.FK_ShopPOItemId,
QuantityDispatched = i.Quantity
}));
await uow.CommitAsync(ct);
@@ -88,289 +84,91 @@ public class LoadingSheetService(
return ToResponse(saved!);
}
public async Task<LoadingSheetResponse> ConfirmDispatchAsync(Guid id, Guid userId, CancellationToken ct = default)
public async Task<LoadingSheetResponse> ConfirmAsync(Guid id, Guid userId, CancellationToken ct = default)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.Pending)
throw new InvalidOperationException($"Cannot confirm dispatch for a sheet in {sheet.Status} status");
if (sheet.Status != LoadingSheetStatus.Planned)
throw new InvalidOperationException($"Cannot confirm a sheet in {sheet.Status} status");
if (!sheet.Items.Any(i => i.QuantityDispatched > 0))
throw new InvalidOperationException("Sheet must have at least one item with quantity greater than zero");
sheet.Status = LoadingSheetStatus.DispatchConfirmed;
sheet.DispatchConfirmedAt = DateTime.Now;
sheet.DispatchConfirmedBy = userId;
await uow.BeginAsync(ct);
try { repo.Update(sheet); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
}
public async Task<LoadingSheetResponse> SetReceiptItemsAsync(Guid id, SetReceiptItemsRequest request, CancellationToken ct = default)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.DispatchConfirmed)
throw new InvalidOperationException("Receipt quantities can only be set after dispatch is confirmed");
foreach (var input in request.Items)
{
var item = sheet.Items.FirstOrDefault(i => i.FK_ItemId == input.FK_ItemId)
?? throw new InvalidOperationException($"Item {input.FK_ItemId} is not on this loading sheet");
item.QuantityReceived = input.QuantityReceived;
}
await uow.BeginAsync(ct);
try { repo.Update(sheet); await uow.CommitAsync(ct); }
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
}
public async Task<LoadingSheetResponse> ConfirmReceiptAsync(Guid id, ConfirmReceiptRequest request, Guid userId, CancellationToken ct = default)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.DispatchConfirmed)
throw new InvalidOperationException($"Cannot confirm receipt for a sheet in {sheet.Status} status");
if (sheet.Items.Any(i => !i.QuantityReceived.HasValue))
throw new InvalidOperationException("All items must have a received quantity before confirming receipt");
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
var hasDiscrepancy = sheet.Items.Any(i => i.QuantityReceived != i.QuantityDispatched);
await uow.BeginAsync(ct);
try
{
if (hasDiscrepancy)
{
sheet.Status = LoadingSheetStatus.DiscrepancyFlagged;
}
else
{
sheet.Status = LoadingSheetStatus.ReceiptConfirmed;
sheet.ReceiptConfirmedAt = DateTime.Now;
sheet.ReceiptConfirmedBy = userId;
await ApplyLoadMovementsAsync(sheet.Id, ride.FK_VehicleId, sheet.Items, userId);
var lines = sheet.Items.Select(i => (i.FK_ItemId, i.QuantityReceived!.Value)).ToList();
await ApplyLoadMovementsAsync(sheet, ride.FK_VehicleId, lines, userId);
ride.Status = RideStatus.Active;
ride.ActivatedAt = DateTime.Now;
ride.Meterstart = request.Meterstart;
rideRepo.Update(ride);
}
sheet.Status = LoadingSheetStatus.Confirmed;
sheet.ConfirmedAt = DateTime.Now;
sheet.ConfirmedBy = userId;
repo.Update(sheet);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
var saved = await repo.GetByIdAsync(id);
return ToResponse(saved!);
}
public async Task<LoadingSheetResponse> ResolveAsync(Guid id, ResolveDiscrepancyRequest request, Guid userId, CancellationToken ct = default)
private async Task ApplyLoadMovementsAsync(Guid sheetId, Guid vehicleId, List<LoadingSheetItem> items, Guid userId)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.DiscrepancyFlagged)
throw new InvalidOperationException($"Cannot resolve a sheet in {sheet.Status} status");
foreach (var input in request.Items)
foreach (var item in items.Where(i => i.QuantityDispatched > 0))
{
var item = sheet.Items.FirstOrDefault(i => i.FK_ItemId == input.FK_ItemId)
?? throw new InvalidOperationException($"Item {input.FK_ItemId} is not on this loading sheet");
if (input.QuantityResolved < 0)
throw new InvalidOperationException("Resolved quantity cannot be negative");
item.QuantityResolved = input.QuantityResolved;
}
var warehouseBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, item.FK_WarehouseId)
?? throw new InvalidOperationException($"No stock found in warehouse for item {item.FK_ItemId}");
if (sheet.Items.Any(i => !i.QuantityResolved.HasValue))
throw new InvalidOperationException("All items must have a resolved quantity");
if (warehouseBalance.Quantity < item.QuantityDispatched)
throw new InvalidOperationException($"Insufficient warehouse stock for item {item.FK_ItemId}");
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
sheet.Status = LoadingSheetStatus.Resolved;
sheet.ReceiptConfirmedAt = DateTime.Now;
sheet.ReceiptConfirmedBy = userId;
var lines = sheet.Items.Select(i => (i.FK_ItemId, i.QuantityResolved!.Value)).ToList();
await uow.BeginAsync(ct);
try
{
await ApplyLoadMovementsAsync(sheet, ride.FK_VehicleId, lines, userId);
ride.Status = RideStatus.Active;
ride.ActivatedAt = DateTime.Now;
ride.Meterstart = request.Meterstart;
rideRepo.Update(ride);
repo.Update(sheet);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
}
public async Task<LoadingSheetResponse> PatchReceiptAsync(Guid id, List<PatchReceiptItemInput> items, Guid userId, CancellationToken ct = default)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.DispatchConfirmed)
throw new InvalidOperationException($"Cannot set receipt for a sheet in {sheet.Status} status");
foreach (var input in items)
{
var sheetItem = sheet.Items.FirstOrDefault(i => i.FK_ItemId == input.ItemId)
?? throw new InvalidOperationException($"Item {input.ItemId} is not on this loading sheet");
sheetItem.QuantityReceived = input.QuantityReceived;
}
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
var hasDiscrepancy = sheet.Items.Any(i => i.QuantityReceived != i.QuantityDispatched);
await uow.BeginAsync(ct);
try
{
if (hasDiscrepancy)
{
sheet.Status = LoadingSheetStatus.DiscrepancyFlagged;
}
else
{
sheet.Status = LoadingSheetStatus.ReceiptConfirmed;
sheet.ReceiptConfirmedAt = DateTime.Now;
sheet.ReceiptConfirmedBy = userId;
var lines = sheet.Items.Select(i => (i.FK_ItemId, i.QuantityReceived!.Value)).ToList();
await ApplyLoadMovementsAsync(sheet, ride.FK_VehicleId, lines, userId);
ride.Status = RideStatus.Active;
ride.ActivatedAt = DateTime.Now;
ride.Meterstart = 0;
rideRepo.Update(ride);
}
repo.Update(sheet);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
}
public async Task<LoadingSheetResponse> PatchResolveAsync(Guid id, Guid userId, CancellationToken ct = default)
{
var sheet = await repo.GetByIdAsync(id)
?? throw new KeyNotFoundException("Loading sheet not found");
if (sheet.Status != LoadingSheetStatus.DiscrepancyFlagged)
throw new InvalidOperationException($"Cannot resolve a sheet in {sheet.Status} status");
foreach (var item in sheet.Items)
{
if (!item.QuantityReceived.HasValue)
throw new InvalidOperationException($"Item {item.FK_ItemId} has no received quantity to resolve from");
item.QuantityResolved = item.QuantityReceived.Value;
}
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
?? throw new KeyNotFoundException("Ride not found");
sheet.Status = LoadingSheetStatus.Resolved;
sheet.ReceiptConfirmedAt = DateTime.Now;
sheet.ReceiptConfirmedBy = userId;
var lines = sheet.Items.Select(i => (i.FK_ItemId, i.QuantityResolved!.Value)).ToList();
await uow.BeginAsync(ct);
try
{
await ApplyLoadMovementsAsync(sheet, ride.FK_VehicleId, lines, userId);
ride.Status = RideStatus.Active;
ride.ActivatedAt = DateTime.Now;
ride.Meterstart = 0;
rideRepo.Update(ride);
repo.Update(sheet);
await uow.CommitAsync(ct);
}
catch { await uow.RollbackAsync(ct); throw; }
return ToResponse(sheet);
}
private async Task ApplyLoadMovementsAsync(LoadingSheet sheet, Guid vehicleId, List<(Guid ItemId, decimal Qty)> lines, Guid userId)
{
foreach (var (itemId, qty) in lines.Where(l => l.Qty > 0))
{
var warehouseBalance = await stockRepo.GetBalanceAsync(itemId, sheet.FK_SourceWarehouseId)
?? throw new InvalidOperationException($"No stock found in warehouse for item {itemId}");
if (warehouseBalance.Quantity < qty)
throw new InvalidOperationException($"Insufficient warehouse stock for item {itemId}");
warehouseBalance.Quantity -= qty;
warehouseBalance.Quantity -= item.QuantityDispatched;
warehouseBalance.UpdatedAt = DateTime.Now;
stockRepo.UpdateBalance(warehouseBalance);
var vehicleBalance = await stockRepo.GetBalanceAsync(itemId, vehicleId);
var vehicleBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId);
if (vehicleBalance is null)
{
await stockRepo.AddBalanceAsync(new StockBalance
{
FK_ItemId = itemId,
FK_ItemId = item.FK_ItemId,
LocationId = vehicleId,
LocationType = LocationType.Vehicle,
Quantity = qty
Quantity = item.QuantityDispatched
});
}
else
{
vehicleBalance.Quantity += qty;
vehicleBalance.Quantity += item.QuantityDispatched;
vehicleBalance.UpdatedAt = DateTime.Now;
stockRepo.UpdateBalance(vehicleBalance);
}
await stockRepo.AddMovementAsync(new StockMovement
{
FK_ItemId = itemId,
FromLocationId = sheet.FK_SourceWarehouseId,
FK_ItemId = item.FK_ItemId,
FromLocationId = item.FK_WarehouseId,
FromLocationType = LocationType.Warehouse,
ToLocationId = vehicleId,
ToLocationType = LocationType.Vehicle,
Quantity = qty,
Quantity = item.QuantityDispatched,
Type = MovementType.Load,
ReferenceId = sheet.Id,
ReferenceId = sheetId,
CreatedBy = userId
});
}
}
private static LoadingSheetResponse ToResponse(LoadingSheet s) =>
new(s.Id, s.FK_RideId, s.FK_SourceWarehouseId,
s.SourceWarehouse?.Name ?? string.Empty,
s.Status,
s.DispatchConfirmedAt, s.DispatchConfirmedBy,
s.ReceiptConfirmedAt, s.ReceiptConfirmedBy,
s.CreatedAt,
new(s.Id, s.FK_RideId, s.Status, s.PopulatedBy, s.PopulatedAt,
s.ConfirmedAt, s.ConfirmedBy, s.CreatedAt,
s.Items.Select(i => new LoadingSheetItemResponse(
i.Id, i.FK_ItemId, i.Item?.Name ?? string.Empty,
i.QuantityDispatched, i.QuantityReceived, i.QuantityResolved)).ToList());
i.FK_WarehouseId, i.Warehouse?.Name ?? string.Empty,
i.QuantityDispatched, i.FK_ShopPOItemId)).ToList(),
s.Orders.Select(o => new LoadingSheetOrderResponse(o.FK_ShopPOId)).ToList());
}