using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Shared; using Oms.Application.Shared; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; using Oms.Domain.WarehouseAggregate.Entities; using Oms.Domain.WarehouseAggregate.Repositories; namespace Oms.Application.Services.Orders; public partial class OrderService { private IOrderStockBindRepository OrderStockBindRepository => LazyServiceProvider.GetRequiredService(); private ICInventoryRepository StockBindInventoryRepository => LazyServiceProvider.GetRequiredService(); private ICParcelRepository StockBindParcelRepository => LazyServiceProvider.GetRequiredService(); private ICParcelItemRepository StockBindParcelItemRepository => LazyServiceProvider.GetRequiredService(); private ICSkuRepository StockBindSkuRepository => LazyServiceProvider.GetRequiredService(); private ICBinLocationRepository StockBindBinLocationRepository => LazyServiceProvider.GetRequiredService(); /// /// 查询物流单当前仓储绑定明细。 /// [HttpGet("/order/stock-bind/detail")] public async Task GetStockBindDetailAsync([FromQuery] int orderId) { var order = await GetOrderOrThrowAsync(orderId); return await BuildStockBindDetailAsync(order); } /// /// 库存候选(左侧库存 Tab)。 /// [HttpPost("/order/stock-bind/inventory-candidates")] public async Task> GetStockBindInventoryCandidatesAsync( [FromBody] OrderStockBindCandidatesInput input) { var (warehouseId, customerId) = await ResolveCandidatesContextAsync(input); var inventories = await StockBindInventoryRepository.GetListAsync(x => x.CustomerId == customerId && x.WarehouseId == warehouseId && (x.AvailableQty > 0 || x.Qty > x.FrozenQty)); var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray(); var skus = skuIds.Length == 0 ? [] : await StockBindSkuRepository.GetListAsync(x => x.CustomerId == customerId && skuIds.Contains(x.Id)); var skuMap = skus.ToDictionary(x => x.Id); var keyword = input.Keyword?.Trim(); var grouped = inventories .GroupBy(x => x.SkuId) .Select(g => { skuMap.TryGetValue(g.Key, out var sku); return new OrderStockBindInventoryCandidateOutput { SkuId = g.Key, SkuCode = sku?.SkuCode ?? string.Empty, SkuName = sku?.SkuName ?? string.Empty, SkuImageUrl = sku?.SkuImageUrl, WarehouseId = warehouseId, TotalQty = g.Sum(x => x.Qty), FrozenQty = g.Sum(x => x.FrozenQty), AvailableQty = g.Sum(x => x.AvailableQty), BinCount = g.Count() }; }) .Where(x => string.IsNullOrWhiteSpace(keyword) || x.SkuCode.Contains(keyword!, StringComparison.OrdinalIgnoreCase) || x.SkuName.Contains(keyword!, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.SkuCode) .ToList(); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var skip = (pageIndex - 1) * pageSize; return new OmsPageResult { Items = grouped.Skip(skip).Take(pageSize).ToList(), Total = grouped.Count }; } /// /// 包裹候选(左侧包裹 Tab,允许全部生命周期)。 /// [HttpPost("/order/stock-bind/parcel-candidates")] public async Task> GetStockBindParcelCandidatesAsync( [FromBody] OrderStockBindCandidatesInput input) { var (warehouseId, customerId) = await ResolveCandidatesContextAsync(input); var lifecycle = input.LifecycleStatus?.Trim(); var filterLifecycle = !string.IsNullOrWhiteSpace(lifecycle) && !string.Equals(lifecycle, "all", StringComparison.OrdinalIgnoreCase); var keyword = input.Keyword?.Trim(); var parcels = await StockBindParcelRepository.GetListAsync(x => x.CustomerId == customerId && x.WarehouseId == warehouseId && (!filterLifecycle || x.LifecycleStatus == lifecycle)); if (!string.IsNullOrWhiteSpace(keyword)) { parcels = parcels .Where(x => x.TrackingNo.Contains(keyword, StringComparison.OrdinalIgnoreCase) || (x.CarrierName?.Contains(keyword, StringComparison.OrdinalIgnoreCase) ?? false)) .ToList(); } parcels = parcels.OrderByDescending(x => x.Id).ToList(); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var pageParcels = parcels.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); var parcelIds = pageParcels.Select(x => x.Id).ToArray(); var allItems = parcelIds.Length == 0 ? [] : await StockBindParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId)); var itemsByParcel = allItems.GroupBy(x => x.ParcelId).ToDictionary(g => g.Key, g => g.ToList()); var outputs = new List(); foreach (var parcel in pageParcels) { itemsByParcel.TryGetValue(parcel.Id, out var items); items ??= []; var availableItems = items .Where(x => GetParcelItemAvailableQty(x) > 0) .Select(x => new OrderStockBindParcelItemCandidateOutput { ParcelItemId = x.Id, ProductName = x.ProductName, AttributeText = x.AttributeText, ImageUrl = x.ImageUrl, SkuId = x.SkuId, SkuCode = x.SkuCode, Quantity = x.Quantity, FrozenQty = x.FrozenQty, AvailableQty = GetParcelItemAvailableQty(x) }) .ToList(); if (availableItems.Count == 0) { continue; } outputs.Add(new OrderStockBindParcelCandidateOutput { ParcelId = parcel.Id, TrackingNo = parcel.TrackingNo, CarrierName = parcel.CarrierName, LifecycleStatus = parcel.LifecycleStatus, WarehouseId = parcel.WarehouseId, Items = availableItems }); } return new OmsPageResult { Items = outputs, Total = parcels.Count }; } /// /// 保存绑定(全量覆盖 + 冻结)。 /// [HttpPost("/order/stock-bind/save")] public async Task SaveStockBindAsync([FromBody] OrderStockBindSaveInput input) { var order = await GetOrderOrThrowAsync(input.OrderId); EnsureOrderCanBindStock(order); await EnsureNoDeductedBindsAsync(order.Id); var customerId = await ResolveStockBindCustomerIdAsync(order); var warehouseId = ResolveBindWarehouseId(order, null); var items = input.Items ?? []; await ReleaseBoundBindsAsync(order.Id); foreach (var item in items) { if (item.BindQty <= 0) { throw new BusinessException("绑定数量必须大于 0"); } var bindType = (item.BindType ?? string.Empty).Trim(); if (string.Equals(bindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase)) { await BindInventoryLineAsync(order, customerId, warehouseId, item); } else if (string.Equals(bindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase)) { await BindParcelItemLineAsync(order, customerId, warehouseId, item); } else { throw new BusinessException($"不支持的绑定类型:{item.BindType}"); } } if (!order.WarehouseId.HasValue || order.WarehouseId.Value <= 0) { order.WarehouseId = warehouseId; await UpdateOrderAsync(order); } return await BuildStockBindDetailAsync(order); } /// /// 清空绑定并释放冻结。 /// [HttpPost("/order/stock-bind/clear")] public async Task ClearStockBindAsync([FromBody] OrderStockBindOrderInput input) { var order = await GetOrderOrThrowAsync(input.OrderId); EnsureOrderCanBindStock(order); await EnsureNoDeductedBindsAsync(order.Id); await ReleaseBoundBindsAsync(order.Id); return await BuildStockBindDetailAsync(order); } /// /// 确认扣减:将 bound 明细转为 deducted,扣减实物并释放冻结。 /// [HttpPost("/order/stock-bind/confirm-deduct")] public async Task ConfirmStockBindDeductAsync([FromBody] OrderStockBindOrderInput input) { var order = await GetOrderOrThrowAsync(input.OrderId); var binds = await OrderStockBindRepository.GetListAsync(x => x.OrderId == order.Id && x.Status == OrderStockBindStatusCodes.Bound); foreach (var bind in binds) { await DeductBoundLineAsync(bind); bind.Status = OrderStockBindStatusCodes.Deducted; await OrderStockBindRepository.UpdateAsync(bind); } return await BuildStockBindDetailAsync(order); } private async Task BindInventoryLineAsync( Order order, int customerId, int warehouseId, OrderStockBindSaveItemInput item) { if (!item.SkuId.HasValue || item.SkuId.Value <= 0) { throw new BusinessException("库存绑定必须指定 skuId"); } var sku = await StockBindSkuRepository.FirstOrDefaultAsync(x => x.Id == item.SkuId.Value && x.CustomerId == customerId) ?? throw new BusinessException("SKU 不存在"); var deductions = await FreezeInventoryAsync( customerId, warehouseId, item.SkuId.Value, item.BindQty, item.InventoryId); foreach (var d in deductions) { await OrderStockBindRepository.InsertAsync(new OrderStockBind { OrderId = order.Id, CustomerId = customerId, WarehouseId = warehouseId, BindType = OrderStockBindTypeCodes.Inventory, SkuId = sku.Id, InventoryId = d.InventoryId, BinLocationId = d.BinLocationId, BindQty = d.Qty, Status = OrderStockBindStatusCodes.Bound, SkuCode = sku.SkuCode, SkuName = sku.SkuName, ImageUrl = sku.SkuImageUrl }); } } private async Task BindParcelItemLineAsync( Order order, int customerId, int warehouseId, OrderStockBindSaveItemInput item) { if (!item.ParcelItemId.HasValue || item.ParcelItemId.Value <= 0) { throw new BusinessException("包裹绑定必须指定 parcelItemId"); } var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == item.ParcelItemId.Value) ?? throw new BusinessException("包裹明细不存在"); var parcel = await StockBindParcelRepository.FirstOrDefaultAsync(x => x.Id == parcelItem.ParcelId && x.CustomerId == customerId) ?? throw new BusinessException("包裹不存在"); if (parcel.WarehouseId != warehouseId) { throw new BusinessException("包裹仓库与订单仓库不一致"); } if (item.ParcelId.HasValue && item.ParcelId.Value != parcel.Id) { throw new BusinessException("parcelId 与 parcelItemId 不匹配"); } var available = GetParcelItemAvailableQty(parcelItem); if (item.BindQty > available) { throw new BusinessException($"包裹明细可用不足,需要 {item.BindQty},可用 {available}"); } parcelItem.FrozenQty += item.BindQty; SyncParcelItemAvailable(parcelItem); await StockBindParcelItemRepository.UpdateAsync(parcelItem); // 已上架行同步冻结库存,避免重复售卖 if (parcelItem.SkuId is > 0) { await FreezeInventoryAsync(customerId, warehouseId, parcelItem.SkuId.Value, item.BindQty, null); } CSku? sku = null; if (parcelItem.SkuId is > 0) { sku = await StockBindSkuRepository.FirstOrDefaultAsync(x => x.Id == parcelItem.SkuId.Value); } await OrderStockBindRepository.InsertAsync(new OrderStockBind { OrderId = order.Id, CustomerId = customerId, WarehouseId = warehouseId, BindType = OrderStockBindTypeCodes.ParcelItem, SkuId = parcelItem.SkuId, ParcelId = parcel.Id, ParcelItemId = parcelItem.Id, BindQty = item.BindQty, Status = OrderStockBindStatusCodes.Bound, SkuCode = sku?.SkuCode ?? parcelItem.SkuCode, SkuName = sku?.SkuName, TrackingNo = parcel.TrackingNo, ProductName = parcelItem.ProductName, ImageUrl = parcelItem.ImageUrl, AttributeText = parcelItem.AttributeText }); } private async Task ReleaseBoundBindsAsync(int orderId) { var binds = await OrderStockBindRepository.GetListAsync(x => x.OrderId == orderId && x.Status == OrderStockBindStatusCodes.Bound); foreach (var bind in binds) { await UnfreezeBoundLineAsync(bind); bind.Status = OrderStockBindStatusCodes.Released; bind.IsDeleted = true; await OrderStockBindRepository.UpdateAsync(bind); } } private async Task UnfreezeBoundLineAsync(OrderStockBind bind) { if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase)) { if (bind.InventoryId is > 0) { await UnfreezeInventoryByIdAsync(bind.InventoryId.Value, bind.BindQty); } else if (bind.SkuId is > 0) { await UnfreezeInventoryBySkuAsync(bind.CustomerId, bind.WarehouseId, bind.SkuId.Value, bind.BindQty); } return; } if (string.Equals(bind.BindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase) && bind.ParcelItemId is > 0) { var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value); if (parcelItem != null) { parcelItem.FrozenQty = Math.Max(0, parcelItem.FrozenQty - bind.BindQty); SyncParcelItemAvailable(parcelItem); await StockBindParcelItemRepository.UpdateAsync(parcelItem); if (parcelItem.SkuId is > 0) { await UnfreezeInventoryBySkuAsync( bind.CustomerId, bind.WarehouseId, parcelItem.SkuId.Value, bind.BindQty); } } } } private async Task DeductBoundLineAsync(OrderStockBind bind) { if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase)) { if (bind.InventoryId is > 0) { var inv = await StockBindInventoryRepository.FirstOrDefaultAsync(x => x.Id == bind.InventoryId.Value) ?? throw new BusinessException("库存行不存在"); if (inv.FrozenQty < bind.BindQty || inv.Qty < bind.BindQty) { throw new BusinessException("库存冻结量不足,无法扣减"); } inv.Qty -= bind.BindQty; inv.FrozenQty -= bind.BindQty; SyncInventoryAvailable(inv); await StockBindInventoryRepository.UpdateAsync(inv); } else if (bind.SkuId is > 0) { await DeductFrozenInventoryBySkuAsync(bind.CustomerId, bind.WarehouseId, bind.SkuId.Value, bind.BindQty); } return; } if (string.Equals(bind.BindType, OrderStockBindTypeCodes.ParcelItem, StringComparison.OrdinalIgnoreCase) && bind.ParcelItemId is > 0) { var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value) ?? throw new BusinessException("包裹明细不存在"); if (parcelItem.FrozenQty < bind.BindQty || parcelItem.Quantity < bind.BindQty) { throw new BusinessException("包裹冻结量不足,无法扣减"); } parcelItem.Quantity -= bind.BindQty; parcelItem.FrozenQty -= bind.BindQty; SyncParcelItemAvailable(parcelItem); await StockBindParcelItemRepository.UpdateAsync(parcelItem); if (parcelItem.SkuId is > 0) { await DeductFrozenInventoryBySkuAsync( bind.CustomerId, bind.WarehouseId, parcelItem.SkuId.Value, bind.BindQty); } } } private async Task> FreezeInventoryAsync( int customerId, int warehouseId, int skuId, int requiredQty, int? inventoryId) { List rows; if (inventoryId is > 0) { var row = await StockBindInventoryRepository.FirstOrDefaultAsync(x => x.Id == inventoryId.Value && x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId) ?? throw new BusinessException("指定库存行不存在"); rows = [row]; } else { rows = await StockBindInventoryRepository.GetListAsync(x => x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId && (x.AvailableQty > 0 || x.Qty > x.FrozenQty)); rows = rows.OrderBy(x => x.BinLocationId).ToList(); } var available = rows.Sum(GetInventoryAvailableQty); if (available < requiredQty) { throw new BusinessException($"SKU {skuId} 库存可用不足,需要 {requiredQty},可用 {available}"); } var result = new List<(int InventoryId, int BinLocationId, int Qty)>(); var remaining = requiredQty; foreach (var row in rows) { if (remaining <= 0) { break; } var take = Math.Min(GetInventoryAvailableQty(row), remaining); if (take <= 0) { continue; } row.FrozenQty += take; SyncInventoryAvailable(row); await StockBindInventoryRepository.UpdateAsync(row); result.Add((row.Id, row.BinLocationId, take)); remaining -= take; } if (remaining > 0) { throw new BusinessException($"SKU {skuId} 冻结失败"); } return result; } private async Task UnfreezeInventoryByIdAsync(int inventoryId, int qty) { var inv = await StockBindInventoryRepository.FirstOrDefaultAsync(x => x.Id == inventoryId); if (inv == null) { return; } inv.FrozenQty = Math.Max(0, inv.FrozenQty - qty); SyncInventoryAvailable(inv); await StockBindInventoryRepository.UpdateAsync(inv); } private async Task UnfreezeInventoryBySkuAsync(int customerId, int warehouseId, int skuId, int qty) { var rows = await StockBindInventoryRepository.GetListAsync(x => x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId && x.FrozenQty > 0); rows = rows.OrderByDescending(x => x.FrozenQty).ToList(); var remaining = qty; foreach (var row in rows) { if (remaining <= 0) { break; } var release = Math.Min(row.FrozenQty, remaining); row.FrozenQty -= release; SyncInventoryAvailable(row); await StockBindInventoryRepository.UpdateAsync(row); remaining -= release; } } private async Task DeductFrozenInventoryBySkuAsync(int customerId, int warehouseId, int skuId, int qty) { var rows = await StockBindInventoryRepository.GetListAsync(x => x.CustomerId == customerId && x.WarehouseId == warehouseId && x.SkuId == skuId && x.FrozenQty > 0); rows = rows.OrderBy(x => x.BinLocationId).ToList(); var remaining = qty; foreach (var row in rows) { if (remaining <= 0) { break; } var take = Math.Min(row.FrozenQty, remaining); take = Math.Min(take, row.Qty); if (take <= 0) { continue; } row.Qty -= take; row.FrozenQty -= take; SyncInventoryAvailable(row); await StockBindInventoryRepository.UpdateAsync(row); remaining -= take; } if (remaining > 0) { throw new BusinessException($"SKU {skuId} 扣减失败,冻结量不足"); } } private async Task EnsureNoDeductedBindsAsync(int orderId) { var hasDeducted = await OrderStockBindRepository.AnyAsync(x => x.OrderId == orderId && x.Status == OrderStockBindStatusCodes.Deducted); if (hasDeducted) { throw new BusinessException("订单已确认扣减库存,不能改绑;如需调整请联系运营"); } } private static void EnsureOrderCanBindStock(Order order) { if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase)) { throw new BusinessException("已删除订单不能绑定"); } } private async Task<(int WarehouseId, int CustomerId)> ResolveCandidatesContextAsync(OrderStockBindCandidatesInput input) { if (input.OrderId > 0) { var order = await GetOrderOrThrowAsync(input.OrderId); EnsureOrderCanBindStock(order); var warehouseId = ResolveBindWarehouseId(order, input.WarehouseId); var customerId = await ResolveStockBindCustomerIdAsync(order); return (warehouseId, customerId); } if (input.WarehouseId is not > 0) { throw new BusinessException("请指定发货仓库"); } var sessionCustomerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); if (sessionCustomerId is not > 0) { throw new BusinessException("请指定客户"); } return (input.WarehouseId.Value, sessionCustomerId.Value); } private static int RequireOrderCustomerId(Order order) { if (!order.CustomerId.HasValue || order.CustomerId.Value <= 0) { throw new BusinessException("订单未关联客户,无法绑定仓储资源"); } return order.CustomerId.Value; } /// /// 绑定客户上下文:客户登录与仓储模块一致(会话客户);管理端按订单客户。 /// private async Task ResolveStockBindCustomerIdAsync(Order order) { var sessionCustomerId = await OmsSessionHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, null); if (sessionCustomerId is > 0) { return sessionCustomerId.Value; } return RequireOrderCustomerId(order); } private static int ResolveBindWarehouseId(Order order, int? requestedWarehouseId) { if (requestedWarehouseId is > 0) { return requestedWarehouseId.Value; } if (order.WarehouseId is > 0) { return order.WarehouseId.Value; } throw new BusinessException("请先为订单指定仓库"); } private async Task BuildStockBindDetailAsync(Order order) { var binds = await OrderStockBindRepository.GetListAsync(x => x.OrderId == order.Id && (x.Status == OrderStockBindStatusCodes.Bound || x.Status == OrderStockBindStatusCodes.Deducted)); binds = binds.OrderBy(x => x.Id).ToList(); string? warehouseName = null; var warehouseId = order.WarehouseId ?? binds.FirstOrDefault()?.WarehouseId; if (warehouseId is > 0) { var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == warehouseId.Value); warehouseName = warehouse?.WarehouseName; } var binIds = binds.Where(x => x.BinLocationId.HasValue).Select(x => x.BinLocationId!.Value).Distinct().ToArray(); var bins = binIds.Length == 0 ? [] : await StockBindBinLocationRepository.GetListAsync(x => binIds.Contains(x.Id)); var binMap = bins.ToDictionary(x => x.Id); var items = new List(); foreach (var bind in binds) { var availableQty = 0; if (string.Equals(bind.BindType, OrderStockBindTypeCodes.Inventory, StringComparison.OrdinalIgnoreCase) && bind.SkuId is > 0 && warehouseId is > 0) { var invRows = await StockBindInventoryRepository.GetListAsync(x => x.CustomerId == bind.CustomerId && x.WarehouseId == warehouseId.Value && x.SkuId == bind.SkuId.Value); availableQty = invRows.Sum(GetInventoryAvailableQty) + bind.BindQty; } else if (bind.ParcelItemId is > 0) { var parcelItem = await StockBindParcelItemRepository.FirstOrDefaultAsync(x => x.Id == bind.ParcelItemId.Value); if (parcelItem != null) { availableQty = GetParcelItemAvailableQty(parcelItem) + bind.BindQty; } } binMap.TryGetValue(bind.BinLocationId ?? 0, out var bin); items.Add(new OrderStockBindItemOutput { Id = bind.Id, BindType = bind.BindType, SkuId = bind.SkuId, SkuCode = bind.SkuCode, SkuName = bind.SkuName, InventoryId = bind.InventoryId, BinLocationId = bind.BinLocationId, BinCode = bin?.BinCode, ParcelId = bind.ParcelId, ParcelItemId = bind.ParcelItemId, TrackingNo = bind.TrackingNo, ProductName = bind.ProductName, AttributeText = bind.AttributeText, ImageUrl = bind.ImageUrl, BindQty = bind.BindQty, AvailableQty = availableQty, Status = bind.Status }); } var bindStatus = "none"; if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Deducted)) { bindStatus = OrderStockBindStatusCodes.Deducted; } else if (binds.Any(x => x.Status == OrderStockBindStatusCodes.Bound)) { bindStatus = OrderStockBindStatusCodes.Bound; } return new OrderStockBindDetailOutput { OrderId = order.Id, WarehouseId = warehouseId, WarehouseName = warehouseName, BindStatus = bindStatus, Items = items }; } private static int GetInventoryAvailableQty(CInventory inv) => inv.AvailableQty > 0 ? inv.AvailableQty : Math.Max(0, inv.Qty - inv.FrozenQty); private static int GetParcelItemAvailableQty(CParcelItem item) => Math.Max(0, item.Quantity - item.FrozenQty); private static void SyncInventoryAvailable(CInventory inv) => inv.AvailableQty = Math.Max(0, inv.Qty - inv.FrozenQty); private static void SyncParcelItemAvailable(CParcelItem item) => item.AvailableQty = Math.Max(0, item.Quantity - item.FrozenQty); }