admin/src/Oms.Application/Services/Orders/OrderService.StockBind.cs
zzh 188aed1ee6 feat: 店铺订单创建物流/代发,并补齐收件人字段与种子数据
- 新增 batch-create-logistics / batch-create-fulfill:按平台行建物流单并回写 LinkedOrderId,代发支持绑定冻结与失败回滚
- ParcelBuilder 支持无 SKU 映射的平台行申报;批量创建可指定 OrderSource=platform
- 库存绑定候选接口支持 orderId=0 草稿选仓
- 店铺订单增加结构化收件人字段(db.sql + 迁移脚本),拉单回填;补充 customer9 店铺/揽收种子与 ConcurrencyStamp 修复脚本

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 22:33:55 +08:00

797 lines
29 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<IOrderStockBindRepository>();
private ICInventoryRepository StockBindInventoryRepository =>
LazyServiceProvider.GetRequiredService<ICInventoryRepository>();
private ICParcelRepository StockBindParcelRepository =>
LazyServiceProvider.GetRequiredService<ICParcelRepository>();
private ICParcelItemRepository StockBindParcelItemRepository =>
LazyServiceProvider.GetRequiredService<ICParcelItemRepository>();
private ICSkuRepository StockBindSkuRepository =>
LazyServiceProvider.GetRequiredService<ICSkuRepository>();
private ICBinLocationRepository StockBindBinLocationRepository =>
LazyServiceProvider.GetRequiredService<ICBinLocationRepository>();
/// <summary>
/// 查询物流单当前仓储绑定明细。
/// </summary>
[HttpGet("/order/stock-bind/detail")]
public async Task<OrderStockBindDetailOutput> GetStockBindDetailAsync([FromQuery] int orderId)
{
var order = await GetOrderOrThrowAsync(orderId);
return await BuildStockBindDetailAsync(order);
}
/// <summary>
/// 库存候选(左侧库存 Tab)。
/// </summary>
[HttpPost("/order/stock-bind/inventory-candidates")]
public async Task<OmsPageResult<OrderStockBindInventoryCandidateOutput>> 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<OrderStockBindInventoryCandidateOutput>
{
Items = grouped.Skip(skip).Take(pageSize).ToList(),
Total = grouped.Count
};
}
/// <summary>
/// 包裹候选(左侧包裹 Tab,允许全部生命周期)。
/// </summary>
[HttpPost("/order/stock-bind/parcel-candidates")]
public async Task<OmsPageResult<OrderStockBindParcelCandidateOutput>> 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<OrderStockBindParcelCandidateOutput>();
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<OrderStockBindParcelCandidateOutput>
{
Items = outputs,
Total = parcels.Count
};
}
/// <summary>
/// 保存绑定(全量覆盖 + 冻结)。
/// </summary>
[HttpPost("/order/stock-bind/save")]
public async Task<OrderStockBindDetailOutput> 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);
}
/// <summary>
/// 清空绑定并释放冻结。
/// </summary>
[HttpPost("/order/stock-bind/clear")]
public async Task<OrderStockBindDetailOutput> ClearStockBindAsync([FromBody] OrderStockBindOrderInput input)
{
var order = await GetOrderOrThrowAsync(input.OrderId);
EnsureOrderCanBindStock(order);
await EnsureNoDeductedBindsAsync(order.Id);
await ReleaseBoundBindsAsync(order.Id);
return await BuildStockBindDetailAsync(order);
}
/// <summary>
/// 确认扣减:将 bound 明细转为 deducted,扣减实物并释放冻结。
/// </summary>
[HttpPost("/order/stock-bind/confirm-deduct")]
public async Task<OrderStockBindDetailOutput> 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<List<(int InventoryId, int BinLocationId, int Qty)>> FreezeInventoryAsync(
int customerId,
int warehouseId,
int skuId,
int requiredQty,
int? inventoryId)
{
List<CInventory> 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;
}
/// <summary>
/// 绑定客户上下文:客户登录与仓储模块一致(会话客户);管理端按订单客户。
/// </summary>
private async Task<int> 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<OrderStockBindDetailOutput> 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<OrderStockBindItemOutput>();
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);
}