93f2f1bd3b
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.
175 lines
6.8 KiB
C#
175 lines
6.8 KiB
C#
using Distribution.Features.Ride;
|
|
using Distribution.Features.Stock;
|
|
using Distribution.Infra.UoW;
|
|
using Distribution.Models;
|
|
|
|
namespace Distribution.Features.LoadingEvent;
|
|
|
|
public class LoadingSheetService(
|
|
ILoadingSheetRepository repo,
|
|
IRideRepository rideRepo,
|
|
IStockRepository stockRepo,
|
|
IUnitOfWork uow)
|
|
{
|
|
public async Task<PagedResponse<LoadingSheetResponse>> GetAllAsync(Guid? rideId, LoadingSheetStatus? status, int page, int pageSize)
|
|
{
|
|
var total = await repo.GetCountAsync(rideId, status);
|
|
var items = await repo.GetAllAsync(rideId, status, page, pageSize);
|
|
return new PagedResponse<LoadingSheetResponse>(page, pageSize, total, items.Select(ToResponse).ToList());
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse?> GetByIdAsync(Guid id)
|
|
{
|
|
var sheet = await repo.GetByIdAsync(id);
|
|
return sheet is null ? null : ToResponse(sheet);
|
|
}
|
|
|
|
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");
|
|
|
|
if (ride.Status != RideStatus.Open)
|
|
throw new InvalidOperationException("A loading sheet can only be created for an Open ride");
|
|
|
|
var existing = await repo.GetActiveByRideIdAsync(request.FK_RideId);
|
|
if (existing is not null)
|
|
throw new InvalidOperationException("This ride already has a Planned loading sheet");
|
|
|
|
var sheet = new LoadingSheet
|
|
{
|
|
FK_RideId = ride.Id,
|
|
Status = LoadingSheetStatus.Planned,
|
|
PopulatedBy = userId,
|
|
PopulatedAt = DateTime.Now
|
|
};
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await repo.AddAsync(sheet);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(sheet.Id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
public async Task<LoadingSheetResponse> SetItemsAsync(Guid id, SetItemsRequest request, CancellationToken ct = default)
|
|
{
|
|
var sheet = await repo.GetByIdAsync(id)
|
|
?? throw new KeyNotFoundException("Loading sheet not found");
|
|
|
|
if (sheet.Status != LoadingSheetStatus.Planned)
|
|
throw new InvalidOperationException("Items can only be changed while the sheet is Planned");
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
repo.RemoveItems(sheet.Items);
|
|
await repo.AddItemsAsync(request.Items.Select(i => new LoadingSheetItem
|
|
{
|
|
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);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(sheet.Id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
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.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");
|
|
|
|
var ride = await rideRepo.GetByIdAsync(sheet.FK_RideId)
|
|
?? throw new KeyNotFoundException("Ride not found");
|
|
|
|
await uow.BeginAsync(ct);
|
|
try
|
|
{
|
|
await ApplyLoadMovementsAsync(sheet.Id, ride.FK_VehicleId, sheet.Items, userId);
|
|
|
|
sheet.Status = LoadingSheetStatus.Confirmed;
|
|
sheet.ConfirmedAt = DateTime.Now;
|
|
sheet.ConfirmedBy = userId;
|
|
|
|
repo.Update(sheet);
|
|
await uow.CommitAsync(ct);
|
|
}
|
|
catch { await uow.RollbackAsync(ct); throw; }
|
|
|
|
var saved = await repo.GetByIdAsync(id);
|
|
return ToResponse(saved!);
|
|
}
|
|
|
|
private async Task ApplyLoadMovementsAsync(Guid sheetId, Guid vehicleId, List<LoadingSheetItem> items, Guid userId)
|
|
{
|
|
foreach (var item in items.Where(i => i.QuantityDispatched > 0))
|
|
{
|
|
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 (warehouseBalance.Quantity < item.QuantityDispatched)
|
|
throw new InvalidOperationException($"Insufficient warehouse stock for item {item.FK_ItemId}");
|
|
|
|
warehouseBalance.Quantity -= item.QuantityDispatched;
|
|
warehouseBalance.UpdatedAt = DateTime.Now;
|
|
stockRepo.UpdateBalance(warehouseBalance);
|
|
|
|
var vehicleBalance = await stockRepo.GetBalanceAsync(item.FK_ItemId, vehicleId);
|
|
if (vehicleBalance is null)
|
|
{
|
|
await stockRepo.AddBalanceAsync(new StockBalance
|
|
{
|
|
FK_ItemId = item.FK_ItemId,
|
|
LocationId = vehicleId,
|
|
LocationType = LocationType.Vehicle,
|
|
Quantity = item.QuantityDispatched
|
|
});
|
|
}
|
|
else
|
|
{
|
|
vehicleBalance.Quantity += item.QuantityDispatched;
|
|
vehicleBalance.UpdatedAt = DateTime.Now;
|
|
stockRepo.UpdateBalance(vehicleBalance);
|
|
}
|
|
|
|
await stockRepo.AddMovementAsync(new StockMovement
|
|
{
|
|
FK_ItemId = item.FK_ItemId,
|
|
FromLocationId = item.FK_WarehouseId,
|
|
FromLocationType = LocationType.Warehouse,
|
|
ToLocationId = vehicleId,
|
|
ToLocationType = LocationType.Vehicle,
|
|
Quantity = item.QuantityDispatched,
|
|
Type = MovementType.Load,
|
|
ReferenceId = sheetId,
|
|
CreatedBy = userId
|
|
});
|
|
}
|
|
}
|
|
|
|
private static LoadingSheetResponse ToResponse(LoadingSheet s) =>
|
|
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.FK_WarehouseId, i.Warehouse?.Name ?? string.Empty,
|
|
i.QuantityDispatched, i.FK_ShopPOItemId)).ToList(),
|
|
s.Orders.Select(o => new LoadingSheetOrderResponse(o.FK_ShopPOId)).ToList());
|
|
}
|