feat(order): add order edit APIs, inbound lookup, and fee output
Support order detail/weight/declaration updates, receivable and payable recalculation, inbound scan order lookup, receive billing overwrite, and OrderOutput feeList for my-orders drawer. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
5e26de6ff3
commit
82df5d1e79
@ -65,8 +65,18 @@ public class SalesProductPriceListOutput
|
||||
/// </summary>
|
||||
public DateTime CreateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人。
|
||||
/// </summary>
|
||||
public string? CreateBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 更新时间。
|
||||
/// </summary>
|
||||
public DateTime? UpdateTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 修改人。
|
||||
/// </summary>
|
||||
public string? UpdateBy { get; set; }
|
||||
}
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
namespace Oms.Application.Dtos.Operations;
|
||||
|
||||
/// <summary>
|
||||
/// 入库扫描按单号查订单(只读展示销售产品、目的国)。
|
||||
/// </summary>
|
||||
public class InboundOrderLookupOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
public int OrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务单号。
|
||||
/// </summary>
|
||||
public string ServiceNo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 客户单号。
|
||||
/// </summary>
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跟踪号。
|
||||
/// </summary>
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 产品编码。
|
||||
/// </summary>
|
||||
public string? ProductCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售产品主键。
|
||||
/// </summary>
|
||||
public int? SalesProductId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 销售产品名称。
|
||||
/// </summary>
|
||||
public string? SalesProductName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目的国家编码。
|
||||
/// </summary>
|
||||
public string? DestinationCountry { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 目的国家名称。
|
||||
/// </summary>
|
||||
public string? DestinationCountryName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态码。
|
||||
/// </summary>
|
||||
public string OrderStatus { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 订单状态名称。
|
||||
/// </summary>
|
||||
public string? OrderStatusName { get; set; }
|
||||
}
|
||||
52
src/Oms.Application/Dtos/Orders/OrderFeeOutput.cs
Normal file
52
src/Oms.Application/Dtos/Orders/OrderFeeOutput.cs
Normal file
@ -0,0 +1,52 @@
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 订单费用出参。
|
||||
/// </summary>
|
||||
public class OrderFeeOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 主键。
|
||||
/// </summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类型:receivable / payable。
|
||||
/// </summary>
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 费用项目。
|
||||
/// </summary>
|
||||
public string CostItem { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 金额。
|
||||
/// </summary>
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 币种。
|
||||
/// </summary>
|
||||
public string Currency { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 下游服务商。
|
||||
/// </summary>
|
||||
public string? DownstreamProvider { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 价格版本号。
|
||||
/// </summary>
|
||||
public string? PriceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 价格生效时间。
|
||||
/// </summary>
|
||||
public DateTime? PriceEffectiveTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 计费重量阶段。
|
||||
/// </summary>
|
||||
public string? ChargingStage { get; set; }
|
||||
}
|
||||
@ -149,4 +149,19 @@ public class OrderOutput
|
||||
/// 重量明细集合。
|
||||
/// </summary>
|
||||
public List<OrderWeightOutput> WeightList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 费用明细集合。
|
||||
/// </summary>
|
||||
public List<OrderFeeOutput> FeeList { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 计费重(优先收货行,其次预报行)。
|
||||
/// </summary>
|
||||
public decimal? ChargeableWeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 应收合计(当前有效应收费用之和)。
|
||||
/// </summary>
|
||||
public decimal? ReceivableAmount { get; set; }
|
||||
}
|
||||
|
||||
91
src/Oms.Application/Dtos/Orders/OrderUpdateInput.cs
Normal file
91
src/Oms.Application/Dtos/Orders/OrderUpdateInput.cs
Normal file
@ -0,0 +1,91 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 订单详情编辑入参。
|
||||
/// </summary>
|
||||
public class OrderUpdateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 客户单号。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? ClientNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跟踪号。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? TrackNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 渠道编码。
|
||||
/// </summary>
|
||||
[StringLength(64)]
|
||||
public string? ChannelCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 货物属性。
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? CargoAttribute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 仓库主键。
|
||||
/// </summary>
|
||||
public int? WarehouseId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否 COD。
|
||||
/// </summary>
|
||||
public bool? IsCod { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// COD 币种。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? CodCurrency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// COD 金额。
|
||||
/// </summary>
|
||||
public decimal? CodSum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申报币种。
|
||||
/// </summary>
|
||||
[StringLength(8)]
|
||||
public string? DeclaredCurrency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申报金额。
|
||||
/// </summary>
|
||||
public decimal? DeclaredSum { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 件数。
|
||||
/// </summary>
|
||||
public int? Quantity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 寄件人。
|
||||
/// </summary>
|
||||
public OrderParcelSenderInput? Sender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 收件人。
|
||||
/// </summary>
|
||||
public OrderParcelReceiverInput? Receiver { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 申报明细;传入时全量替换原申报行。
|
||||
/// </summary>
|
||||
public List<OrderParcelItemInput>? Items { get; set; }
|
||||
}
|
||||
54
src/Oms.Application/Dtos/Orders/OrderWeightUpdateInput.cs
Normal file
54
src/Oms.Application/Dtos/Orders/OrderWeightUpdateInput.cs
Normal file
@ -0,0 +1,54 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// 订单件重尺编辑入参。
|
||||
/// </summary>
|
||||
public class OrderWeightUpdateInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单主键。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 件重尺行。
|
||||
/// </summary>
|
||||
[Required]
|
||||
public List<OrderWeightItemInput> Weights { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单条件重尺编辑项。
|
||||
/// </summary>
|
||||
public class OrderWeightItemInput
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型:forecast/receive/ship/downstream。
|
||||
/// </summary>
|
||||
[Required]
|
||||
[StringLength(16)]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 长度(cm)。
|
||||
/// </summary>
|
||||
public decimal? Length { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 宽度(cm)。
|
||||
/// </summary>
|
||||
public decimal? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 高度(cm)。
|
||||
/// </summary>
|
||||
public decimal? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 实重(kg)。
|
||||
/// </summary>
|
||||
public decimal? ActualWeight { get; set; }
|
||||
}
|
||||
@ -88,7 +88,9 @@ public partial class SalesProductService
|
||||
Factors = factorMap.TryGetValue(price.Id, out var factors) ? factors.Select(MapPriceFactorOutput).ToList() : [],
|
||||
Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [],
|
||||
CreateTime = price.CreateTime,
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(price.UpdateTime)
|
||||
CreateBy = price.CreateUserId <= 0 ? null : price.CreateUserId.ToString(),
|
||||
UpdateTime = OmsDocHelper.NullIfDefault(price.UpdateTime),
|
||||
UpdateBy = price.UpdateUserId <= 0 ? null : price.UpdateUserId.ToString()
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
|
||||
@ -3,9 +3,11 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Operations;
|
||||
using Oms.Application.Dtos.Shared;
|
||||
using Oms.Application.Services.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OperationsAggregate.Entities;
|
||||
using Oms.Domain.OperationsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
using Oms.Domain.SystemAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Services.Operations;
|
||||
@ -17,10 +19,60 @@ namespace Oms.Application.Services.Operations;
|
||||
public class InboundRecordService : ApplicationService
|
||||
{
|
||||
private IInboundRecordRepository InboundRecordRepository => LazyServiceProvider.GetRequiredService<IInboundRecordRepository>();
|
||||
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||||
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||||
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||||
|
||||
/// <summary>
|
||||
/// 按单号查询订单(入库扫描展示销售产品、目的国,只读)。
|
||||
/// </summary>
|
||||
[HttpGet("/operation/inbound/order-lookup")]
|
||||
public async Task<InboundOrderLookupOutput> LookupOrderAsync([FromQuery] string waybillNo)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(waybillNo))
|
||||
{
|
||||
throw new BusinessException("请扫描或输入单号");
|
||||
}
|
||||
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(waybillNo.Trim());
|
||||
if (order == null)
|
||||
{
|
||||
throw new BusinessException("未找到对应订单,请检查单号");
|
||||
}
|
||||
|
||||
var output = new InboundOrderLookupOutput
|
||||
{
|
||||
OrderId = order.Id,
|
||||
ServiceNo = order.ServiceNo,
|
||||
ClientNo = order.ClientNo,
|
||||
TrackNo = order.TrackNo,
|
||||
ProductCode = order.ProductCode,
|
||||
DestinationCountry = order.ConsigneeCountry,
|
||||
OrderStatus = order.OrderStatus,
|
||||
OrderStatusName = OmsDocHelper.GetOrderStatusName(order.OrderStatus)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product != null)
|
||||
{
|
||||
output.SalesProductId = product.Id;
|
||||
output.SalesProductName = product.ProductName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(order.ConsigneeCountry))
|
||||
{
|
||||
var country = await CountryRepository.FirstOrDefaultAsync(x => x.CountryCode == order.ConsigneeCountry);
|
||||
output.DestinationCountryName = country?.ChineseName ?? country?.EnglishName ?? order.ConsigneeCountry;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增入库记录。
|
||||
/// </summary>
|
||||
@ -35,29 +87,55 @@ public class InboundRecordService : ApplicationService
|
||||
[HttpPost("/operation/inbound/create")]
|
||||
public async Task<InboundRecordOutput> CreateAsync(CreateInboundRecordInput input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input.WaybillNo))
|
||||
{
|
||||
throw new BusinessException("运单号不能为空");
|
||||
}
|
||||
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(input.WaybillNo.Trim());
|
||||
if (order == null)
|
||||
{
|
||||
throw new BusinessException("未找到对应订单,请检查单号");
|
||||
}
|
||||
|
||||
if (!input.Weight.HasValue || !input.Length.HasValue || !input.Width.HasValue || !input.Height.HasValue
|
||||
|| input.Weight <= 0 || input.Length <= 0 || input.Width <= 0 || input.Height <= 0)
|
||||
{
|
||||
throw new BusinessException("收货实重与长宽高不能为空且须大于 0");
|
||||
}
|
||||
|
||||
int? salesProductId = null;
|
||||
string? destinationCountry = order.ConsigneeCountry;
|
||||
if (!string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
salesProductId = product?.Id;
|
||||
}
|
||||
|
||||
var now = DateTime.Now;
|
||||
var operatorId = CurrentUser.GetId<int>();
|
||||
var entity = new InboundRecord
|
||||
{
|
||||
WaybillNo = input.WaybillNo,
|
||||
WaybillNo = input.WaybillNo.Trim(),
|
||||
Weight = input.Weight,
|
||||
Length = input.Length,
|
||||
Width = input.Width,
|
||||
Height = input.Height,
|
||||
SalesProductId = input.SalesProductId,
|
||||
DestinationCountry = input.DestinationCountry,
|
||||
WarehouseId = input.WarehouseId,
|
||||
SalesProductId = salesProductId,
|
||||
DestinationCountry = destinationCountry,
|
||||
WarehouseId = input.WarehouseId ?? order.WarehouseId,
|
||||
OperateTime = now,
|
||||
OperatorId = operatorId
|
||||
};
|
||||
|
||||
var savedEntity = await InboundRecordRepository.InsertAsync(entity);
|
||||
await OrderService.ApplyReceiveBillingByWaybillAsync(
|
||||
input.WaybillNo,
|
||||
input.WaybillNo.Trim(),
|
||||
input.Length,
|
||||
input.Width,
|
||||
input.Height,
|
||||
input.Weight);
|
||||
|
||||
var savedEntity = await InboundRecordRepository.InsertAsync(entity);
|
||||
return ObjectMapper.Map<InboundRecord, InboundRecordOutput>(savedEntity);
|
||||
}
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ public partial class OrderService
|
||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync(orderIds);
|
||||
var declarations = await OrderRepository.GetDeclarationsByOrderIdsAsync(orderIds);
|
||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds);
|
||||
var fees = await OrderRepository.GetFeesByOrderIdsAsync(orderIds);
|
||||
var warehouses = warehouseIds.Length == 0
|
||||
? []
|
||||
: await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
|
||||
@ -35,6 +36,7 @@ public partial class OrderService
|
||||
var recipientMap = recipients.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.First());
|
||||
var declarationMap = declarations.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var weightMap = weights.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var feeMap = fees.GroupBy(x => x.OrderId).ToDictionary(x => x.Key, x => x.ToList());
|
||||
var warehouseMap = warehouses.ToDictionary(x => x.Id);
|
||||
|
||||
return orders.Select(order =>
|
||||
@ -43,6 +45,12 @@ public partial class OrderService
|
||||
recipientMap.TryGetValue(order.Id, out var recipient);
|
||||
declarationMap.TryGetValue(order.Id, out var items);
|
||||
weightMap.TryGetValue(order.Id, out var orderWeights);
|
||||
feeMap.TryGetValue(order.Id, out var orderFees);
|
||||
var feeOutputs = (orderFees ?? []).Select(MapFee).ToList();
|
||||
var chargeableWeight = ResolveDisplayChargeableWeight(orderWeights);
|
||||
var receivableAmount = feeOutputs
|
||||
.Where(x => x.Type == OrderFeeTypeCodes.Receivable)
|
||||
.Sum(x => x.Amount);
|
||||
return new OrderOutput
|
||||
{
|
||||
Id = order.Id,
|
||||
@ -75,7 +83,10 @@ public partial class OrderService
|
||||
Sender = MapSender(sender),
|
||||
Receiver = MapReceiver(recipient),
|
||||
Items = (items ?? []).Select(MapDeclaration).ToList(),
|
||||
WeightList = (orderWeights ?? []).Select(MapWeight).ToList()
|
||||
WeightList = (orderWeights ?? []).Select(MapWeight).ToList(),
|
||||
FeeList = feeOutputs,
|
||||
ChargeableWeight = chargeableWeight,
|
||||
ReceivableAmount = feeOutputs.Count == 0 ? null : receivableAmount
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
@ -543,6 +554,37 @@ public partial class OrderService
|
||||
};
|
||||
}
|
||||
|
||||
private static OrderFeeOutput MapFee(OrderFee x) =>
|
||||
new()
|
||||
{
|
||||
Id = x.Id,
|
||||
Type = x.Type,
|
||||
CostItem = x.CostItem,
|
||||
Amount = x.Amount,
|
||||
Currency = x.Currency,
|
||||
DownstreamProvider = x.DownstreamProvider,
|
||||
PriceNo = x.PriceNo,
|
||||
PriceEffectiveTime = x.PriceEffectiveTime,
|
||||
ChargingStage = x.ChargingStage
|
||||
};
|
||||
|
||||
private static decimal? ResolveDisplayChargeableWeight(IReadOnlyList<OrderWeight>? weights)
|
||||
{
|
||||
if (weights == null || weights.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var receive = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Receive);
|
||||
if (receive?.ChargeableWeight is > 0)
|
||||
{
|
||||
return receive.ChargeableWeight;
|
||||
}
|
||||
|
||||
var forecast = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Forecast);
|
||||
return forecast?.ChargeableWeight;
|
||||
}
|
||||
|
||||
private static OrderTaskFail BuildTaskFail(int taskId, int rowNumber, string reason, object rawData)
|
||||
{
|
||||
return new OrderTaskFail
|
||||
|
||||
648
src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs
Normal file
648
src/Oms.Application/Services/Orders/OrderService.OrderEdit.cs
Normal file
@ -0,0 +1,648 @@
|
||||
using Fake.Application;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Application.Shared;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.CustomersAggregate.Enums;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.Shared.Enums;
|
||||
|
||||
namespace Oms.Application.Services.Orders;
|
||||
|
||||
public partial class OrderService
|
||||
{
|
||||
private IServiceChannelRepository ServiceChannelRepository =>
|
||||
LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||||
|
||||
/// <summary>
|
||||
/// 编辑订单详情(基本信息、发件人、收件人)。
|
||||
/// </summary>
|
||||
[HttpPost("/order/update")]
|
||||
public async Task<OrderOutput> UpdateOrderDetailAsync(OrderUpdateInput input)
|
||||
{
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
|
||||
if (input.ClientNo != null)
|
||||
{
|
||||
order.ClientNo = string.IsNullOrWhiteSpace(input.ClientNo) ? null : input.ClientNo.Trim();
|
||||
}
|
||||
|
||||
if (input.TrackNo != null)
|
||||
{
|
||||
order.TrackNo = string.IsNullOrWhiteSpace(input.TrackNo) ? null : input.TrackNo.Trim();
|
||||
}
|
||||
|
||||
if (input.ChannelCode != null)
|
||||
{
|
||||
order.ChannelCode = string.IsNullOrWhiteSpace(input.ChannelCode) ? null : input.ChannelCode.Trim();
|
||||
}
|
||||
|
||||
if (input.CargoAttribute != null)
|
||||
{
|
||||
order.CargoAttribute = input.CargoAttribute.Trim();
|
||||
}
|
||||
|
||||
if (input.WarehouseId.HasValue)
|
||||
{
|
||||
order.WarehouseId = input.WarehouseId.Value > 0 ? input.WarehouseId : null;
|
||||
}
|
||||
|
||||
if (input.IsCod.HasValue)
|
||||
{
|
||||
order.IsCod = input.IsCod.Value ? (byte)1 : (byte)0;
|
||||
}
|
||||
|
||||
if (input.CodCurrency != null)
|
||||
{
|
||||
order.CodCurrency = input.CodCurrency;
|
||||
}
|
||||
|
||||
if (input.CodSum.HasValue)
|
||||
{
|
||||
order.CodSum = input.CodSum;
|
||||
}
|
||||
|
||||
if (input.DeclaredCurrency != null)
|
||||
{
|
||||
order.DeclaredCurrency = input.DeclaredCurrency;
|
||||
}
|
||||
|
||||
if (input.DeclaredSum.HasValue)
|
||||
{
|
||||
order.DeclaredSum = input.DeclaredSum;
|
||||
}
|
||||
|
||||
if (input.Quantity.HasValue)
|
||||
{
|
||||
order.Quantity = input.Quantity;
|
||||
}
|
||||
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
|
||||
if (input.Sender != null && order.SenderId.HasValue)
|
||||
{
|
||||
var senders = await OrderRepository.GetSendersByOrderIdsAsync([order.Id]);
|
||||
var sender = senders.FirstOrDefault();
|
||||
if (sender != null)
|
||||
{
|
||||
ApplySenderInput(sender, input.Sender);
|
||||
if (input.Sender.CountryCode != null)
|
||||
{
|
||||
var country = await GetCountryOrThrowAsync(input.Sender.CountryCode, "发件国家不存在");
|
||||
sender.Country = country.Id;
|
||||
}
|
||||
|
||||
await OrderRepository.UpdateSenderAsync(sender);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.Receiver != null && order.RecipientId.HasValue)
|
||||
{
|
||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
if (recipient != null)
|
||||
{
|
||||
await ApplyReceiverInputAsync(order, recipient, input.Receiver);
|
||||
await OrderRepository.UpdateRecipientAsync(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.Items != null)
|
||||
{
|
||||
await OrderRepository.ReplaceDeclarationsAsync(
|
||||
order.Id,
|
||||
input.Items.Select(x => MapDeclarationInput(order.Id, x)));
|
||||
}
|
||||
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 编辑订单件重尺并重算计费重。
|
||||
/// </summary>
|
||||
[HttpPost("/order/weight/update")]
|
||||
public async Task<OrderOutput> UpdateOrderWeightsAsync(OrderWeightUpdateInput input)
|
||||
{
|
||||
if (input.Weights.Count == 0)
|
||||
{
|
||||
throw new BusinessException("件重尺不能为空");
|
||||
}
|
||||
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法重算计费重");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException("销售产品不存在");
|
||||
}
|
||||
|
||||
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList();
|
||||
if (weights.Count == 0)
|
||||
{
|
||||
throw new BusinessException("订单件重尺不存在");
|
||||
}
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
var updatedTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in input.Weights)
|
||||
{
|
||||
var type = item.Type?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var row = weights.FirstOrDefault(x => string.Equals(x.Type, type, StringComparison.OrdinalIgnoreCase));
|
||||
if (row == null)
|
||||
{
|
||||
throw new BusinessException($"缺少件重尺类型 {type}");
|
||||
}
|
||||
|
||||
if (item.Length.HasValue)
|
||||
{
|
||||
row.Length = item.Length;
|
||||
}
|
||||
|
||||
if (item.Width.HasValue)
|
||||
{
|
||||
row.Width = item.Width;
|
||||
}
|
||||
|
||||
if (item.Height.HasValue)
|
||||
{
|
||||
row.Height = item.Height;
|
||||
}
|
||||
|
||||
if (item.ActualWeight.HasValue)
|
||||
{
|
||||
row.ActualWeight = item.ActualWeight;
|
||||
}
|
||||
|
||||
if (row.Length.HasValue && row.Width.HasValue && row.Height.HasValue)
|
||||
{
|
||||
row.Cubic = ParcelBillingCalculator.CalculateCubic(row.Length.Value, row.Width.Value, row.Height.Value);
|
||||
}
|
||||
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, row);
|
||||
updatedTypes.Add(type);
|
||||
}
|
||||
|
||||
if (updatedTypes.Contains(OrderWeightTypeCodes.Forecast))
|
||||
{
|
||||
var forecast = weights.First(x => x.Type == OrderWeightTypeCodes.Forecast);
|
||||
SyncWeightRows(weights, forecast);
|
||||
}
|
||||
else if (updatedTypes.Contains(OrderWeightTypeCodes.Receive))
|
||||
{
|
||||
var receive = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Receive);
|
||||
if (receive != null)
|
||||
{
|
||||
SyncReceiveToShipAndDownstream(weights, receive);
|
||||
}
|
||||
}
|
||||
else if (updatedTypes.Contains(OrderWeightTypeCodes.Ship))
|
||||
{
|
||||
var ship = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Ship);
|
||||
if (ship != null)
|
||||
{
|
||||
SyncShipToDownstream(weights, ship);
|
||||
}
|
||||
}
|
||||
|
||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重算订单应收费用。
|
||||
/// </summary>
|
||||
[HttpPost("/order/fee/recalculate-receivable")]
|
||||
public async Task<OrderOutput> RecalculateReceivableFeesAsync(OrderActionInput input)
|
||||
{
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
await RecalculateReceivableCoreAsync(order);
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重算订单应付费用。
|
||||
/// </summary>
|
||||
[HttpPost("/order/fee/recalculate-payable")]
|
||||
public async Task<OrderOutput> RecalculatePayableFeesAsync(OrderActionInput input)
|
||||
{
|
||||
var order = await GetOrderOrThrowAsync(input.Id);
|
||||
await RecalculatePayableCoreAsync(order);
|
||||
return (await MapOrderOutputsAsync([order])).First();
|
||||
}
|
||||
|
||||
private async Task RecalculateReceivableCoreAsync(Order order)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法重算应收");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException("销售产品不存在");
|
||||
}
|
||||
|
||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||
var (weightType, chargingStage) = ResolveReceivableBillingStage(order);
|
||||
var weightRow = weights.FirstOrDefault(x => x.Type == weightType)
|
||||
?? throw new BusinessException($"缺少件重尺类型 {weightType}");
|
||||
if (!weightRow.ChargeableWeight.HasValue || weightRow.ChargeableWeight <= 0)
|
||||
{
|
||||
throw new BusinessException("计费重无效,请先完善件重尺");
|
||||
}
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
||||
product,
|
||||
snapshotBinding.Rules,
|
||||
snapshotBinding.Price,
|
||||
order.ConsigneeCountry ?? string.Empty,
|
||||
recipient?.Province,
|
||||
recipient?.City,
|
||||
recipient?.ZipCode);
|
||||
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||
{
|
||||
throw new BusinessException("价格分区未匹配(规则快照)");
|
||||
}
|
||||
|
||||
var customer = order.CustomerId.HasValue
|
||||
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||
: null;
|
||||
|
||||
await UpsertReceivableFeesAsync(
|
||||
order,
|
||||
snapshotBinding,
|
||||
weightRow.ChargeableWeight ?? 0,
|
||||
zoneKeys,
|
||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||
customer?.CustomerGroup,
|
||||
chargingStage,
|
||||
replaceExistingReceivable: true);
|
||||
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
}
|
||||
|
||||
private async Task RecalculatePayableCoreAsync(Order order)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定服务渠道,无法重算应付");
|
||||
}
|
||||
|
||||
var channel = await ServiceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim());
|
||||
if (channel == null)
|
||||
{
|
||||
throw new BusinessException("服务渠道不存在");
|
||||
}
|
||||
|
||||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]);
|
||||
var downstream = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream)
|
||||
?? throw new BusinessException("缺少 downstream 件重尺");
|
||||
if (!downstream.ChargeableWeight.HasValue || downstream.ChargeableWeight <= 0)
|
||||
{
|
||||
throw new BusinessException("downstream 计费重无效,请先完善件重尺");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法匹配分区");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
throw new BusinessException("销售产品不存在");
|
||||
}
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
var zoneKeys = await ResolvePriceZoneKeysAsync(
|
||||
product,
|
||||
snapshotBinding.Rules,
|
||||
snapshotBinding.Price,
|
||||
order.ConsigneeCountry ?? string.Empty,
|
||||
recipient?.Province,
|
||||
recipient?.City,
|
||||
recipient?.ZipCode);
|
||||
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||
{
|
||||
throw new BusinessException("价格分区未匹配(规则快照)");
|
||||
}
|
||||
|
||||
var channelPrices = await BuildChannelPriceOutputsAsync(channel.Id);
|
||||
var (_, chargingStage) = ResolveReceivableBillingStage(order);
|
||||
var customer = order.CustomerId.HasValue
|
||||
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||
: null;
|
||||
|
||||
var fees = ParcelBillingCalculator.CalculatePayableFees(
|
||||
channelPrices,
|
||||
$"LIVE-{channel.Id}",
|
||||
order.CreateTime == default ? DateTime.Now : order.CreateTime,
|
||||
downstream.ChargeableWeight ?? 0,
|
||||
zoneKeys,
|
||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||
customer?.CustomerGroup,
|
||||
chargingStage);
|
||||
|
||||
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledPayablePriceItems(channelPrices))
|
||||
{
|
||||
throw new BusinessException("渠道买价未配置或未匹配到有效价格");
|
||||
}
|
||||
|
||||
if (fees.Count > 0)
|
||||
{
|
||||
await OrderRepository.DeletePayableFeesAsync(order.Id);
|
||||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||||
}
|
||||
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
}
|
||||
|
||||
private async Task<List<ServiceChannelPriceListOutput>> BuildChannelPriceOutputsAsync(int serviceChannelId)
|
||||
{
|
||||
var prices = await ServiceChannelRepository.GetPricesAsync(serviceChannelId);
|
||||
var priceIds = prices.Select(x => x.Id).ToArray();
|
||||
var factorMap = (await ServiceChannelRepository.GetPriceFactorsAsync(priceIds))
|
||||
.GroupBy(x => x.PriceId)
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.SerialNumber).ToList());
|
||||
var detailMap = (await ServiceChannelRepository.GetPriceDetailsAsync(priceIds))
|
||||
.GroupBy(x => x.PriceId)
|
||||
.ToDictionary(x => x.Key, x => x.OrderBy(item => item.Id).ToList());
|
||||
|
||||
return prices.Select(price => new ServiceChannelPriceListOutput
|
||||
{
|
||||
Id = price.Id,
|
||||
ServiceChannelId = price.ServiceChannelId,
|
||||
SerialNumber = price.SerialNumber,
|
||||
Status = (byte)price.Status,
|
||||
PriceName = price.PriceName,
|
||||
CostItem = price.CostItem,
|
||||
CostUnit = price.CostUnit,
|
||||
Currency = price.Currency,
|
||||
ChargingCondition = price.ChargingCondition,
|
||||
Factors = factorMap.TryGetValue(price.Id, out var factors)
|
||||
? factors.Select(MapChannelPriceFactorOutput).ToList()
|
||||
: [],
|
||||
Details = detailMap.TryGetValue(price.Id, out var details)
|
||||
? details.Select(MapChannelPriceDetailOutput).ToList()
|
||||
: []
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private static ChannelPriceFactorOutput MapChannelPriceFactorOutput(ServiceChannelPriceFactor entity) =>
|
||||
new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
PriceId = entity.PriceId,
|
||||
SerialNumber = entity.SerialNumber,
|
||||
Factor = entity.Factor,
|
||||
FactorValues = OmsDocHelper.DeserializeJson<List<string>>(entity.FactorValues) ?? []
|
||||
};
|
||||
|
||||
private static ChannelPriceDetailOutput MapChannelPriceDetailOutput(ServiceChannelPriceDetail entity) =>
|
||||
new()
|
||||
{
|
||||
Id = entity.Id,
|
||||
PriceId = entity.PriceId,
|
||||
FactorValuesJson = OmsDocHelper.DeserializeJsonObject(entity.FactorValuesJson),
|
||||
CalculationFormula = entity.CalculationFormula,
|
||||
CalculatedValue = entity.CalculatedValue,
|
||||
CustomerGroupValues = OmsDocHelper.DeserializeJsonObject(entity.CustomerGroupValues)
|
||||
};
|
||||
|
||||
private static (string WeightType, string ChargingStage) ResolveReceivableBillingStage(Order order) =>
|
||||
order.OrderStatus switch
|
||||
{
|
||||
OrderStatusCodes.Received or OrderStatusCodes.PendingPayment or OrderStatusCodes.PendingShip
|
||||
=> (OrderWeightTypeCodes.Receive, OrderFeeChargingStageCodes.Inbound),
|
||||
OrderStatusCodes.PendingOnline or OrderStatusCodes.Shipping
|
||||
or OrderStatusCodes.DeliverySuccess or OrderStatusCodes.DeliveryFailed
|
||||
=> (OrderWeightTypeCodes.Ship, OrderFeeChargingStageCodes.Outbound),
|
||||
_ => (OrderWeightTypeCodes.Forecast, OrderFeeChargingStageCodes.Forecast)
|
||||
};
|
||||
|
||||
private static OrderDeclaration MapDeclarationInput(int orderId, OrderParcelItemInput x)
|
||||
{
|
||||
return new OrderDeclaration
|
||||
{
|
||||
OrderId = orderId,
|
||||
CnName = x.CnName,
|
||||
EnName = x.EnName,
|
||||
Sku = x.Sku,
|
||||
Unit = x.Unit,
|
||||
UnitPrice = x.UnitPrice,
|
||||
UnitWeight = x.UnitWeight,
|
||||
Quantity = x.Quantity,
|
||||
Amount = x.Amount,
|
||||
Weight = x.Weight,
|
||||
HsCode = x.ExHsCode ?? x.ImHsCode,
|
||||
ImHsCode = x.ImHsCode,
|
||||
ExHsCode = x.ExHsCode ?? x.ImHsCode,
|
||||
SkuImage = x.SkuImage,
|
||||
Category = x.Category,
|
||||
Brand = x.Brand,
|
||||
BrandType = x.BrandType,
|
||||
IsMagnetic = x.IsMagnetic ? (byte)1 : (byte)0,
|
||||
Distribution = x.Distribution,
|
||||
Material = x.Material,
|
||||
EnMaterial = x.EnMaterial,
|
||||
Purpose = x.Purpose,
|
||||
Attributes = x.Attributes,
|
||||
OriginCountry = x.OriginCountry,
|
||||
SalesLink = x.SalesLink,
|
||||
Model = x.Model,
|
||||
Specifications = x.Specifications,
|
||||
Comment = x.Comment
|
||||
};
|
||||
}
|
||||
|
||||
private static void ApplySenderInput(OrderSender sender, OrderParcelSenderInput input)
|
||||
{
|
||||
if (input.SenderName != null)
|
||||
{
|
||||
sender.SenderName = input.SenderName;
|
||||
}
|
||||
|
||||
if (input.Company != null)
|
||||
{
|
||||
sender.Company = input.Company;
|
||||
}
|
||||
|
||||
if (input.Phone != null)
|
||||
{
|
||||
sender.Phone = input.Phone;
|
||||
}
|
||||
|
||||
if (input.TelePhone != null)
|
||||
{
|
||||
sender.TelePhone = input.TelePhone;
|
||||
}
|
||||
|
||||
if (input.Email != null)
|
||||
{
|
||||
sender.Email = input.Email;
|
||||
}
|
||||
|
||||
if (input.Province != null)
|
||||
{
|
||||
sender.Province = input.Province;
|
||||
}
|
||||
|
||||
if (input.City != null)
|
||||
{
|
||||
sender.City = input.City;
|
||||
}
|
||||
|
||||
if (input.ZipCode != null)
|
||||
{
|
||||
sender.ZipCode = input.ZipCode;
|
||||
}
|
||||
|
||||
if (input.StreetAddress1 != null)
|
||||
{
|
||||
sender.StreetAddress1 = input.StreetAddress1;
|
||||
}
|
||||
|
||||
if (input.StreetAddress2 != null)
|
||||
{
|
||||
sender.StreetAddress2 = input.StreetAddress2;
|
||||
}
|
||||
|
||||
if (input.StreetAddress3 != null)
|
||||
{
|
||||
sender.StreetAddress3 = input.StreetAddress3;
|
||||
}
|
||||
|
||||
if (input.HouseNumber != null)
|
||||
{
|
||||
sender.HouseNumber = input.HouseNumber;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ApplyReceiverInputAsync(Order order, OrderRecipient recipient, OrderParcelReceiverInput input)
|
||||
{
|
||||
if (input.RecipientsName != null)
|
||||
{
|
||||
recipient.RecipientsName = input.RecipientsName;
|
||||
}
|
||||
|
||||
if (input.CompanyName != null)
|
||||
{
|
||||
recipient.CompanyName = input.CompanyName;
|
||||
}
|
||||
|
||||
if (input.Phone != null)
|
||||
{
|
||||
recipient.Phone = input.Phone;
|
||||
}
|
||||
|
||||
if (input.TelePhone != null)
|
||||
{
|
||||
recipient.TelePhone = input.TelePhone;
|
||||
}
|
||||
|
||||
if (input.Email != null)
|
||||
{
|
||||
recipient.Email = input.Email;
|
||||
}
|
||||
|
||||
if (input.Province != null)
|
||||
{
|
||||
recipient.Province = input.Province;
|
||||
}
|
||||
|
||||
if (input.City != null)
|
||||
{
|
||||
recipient.City = input.City;
|
||||
}
|
||||
|
||||
if (input.StreetNumber != null)
|
||||
{
|
||||
recipient.StreetNumber = input.StreetNumber;
|
||||
}
|
||||
|
||||
if (input.StreetAddress1 != null)
|
||||
{
|
||||
recipient.StreetAddress1 = input.StreetAddress1;
|
||||
}
|
||||
|
||||
if (input.StreetAddress2 != null)
|
||||
{
|
||||
recipient.StreetAddress2 = input.StreetAddress2;
|
||||
}
|
||||
|
||||
if (input.StreetAddress3 != null)
|
||||
{
|
||||
recipient.StreetAddress3 = input.StreetAddress3;
|
||||
}
|
||||
|
||||
if (input.ZipCode != null)
|
||||
{
|
||||
recipient.ZipCode = input.ZipCode;
|
||||
}
|
||||
|
||||
if (input.TaxType.HasValue)
|
||||
{
|
||||
recipient.TaxType = input.TaxType;
|
||||
}
|
||||
|
||||
if (input.TaxNo != null)
|
||||
{
|
||||
recipient.TaxNo = input.TaxNo;
|
||||
}
|
||||
|
||||
if (input.CertificateType.HasValue)
|
||||
{
|
||||
recipient.CertificateType = input.CertificateType;
|
||||
}
|
||||
|
||||
if (input.CertificateNo != null)
|
||||
{
|
||||
recipient.CertificateNo = input.CertificateNo;
|
||||
}
|
||||
|
||||
if (input.Eori != null)
|
||||
{
|
||||
recipient.Eori = input.Eori;
|
||||
}
|
||||
|
||||
if (input.Vat != null)
|
||||
{
|
||||
recipient.Vat = input.Vat;
|
||||
}
|
||||
|
||||
if (input.CountryCode != null)
|
||||
{
|
||||
var country = await GetCountryOrThrowAsync(input.CountryCode, "收件国家不存在");
|
||||
recipient.Country = country.Id;
|
||||
order.ConsigneeCountry = country.CountryCode;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -366,7 +366,8 @@ public partial class OrderService
|
||||
IReadOnlyList<string> zoneKeys,
|
||||
string cargoAttribute,
|
||||
CustomerGroup? customerGroup,
|
||||
string chargingStage)
|
||||
string chargingStage,
|
||||
bool replaceExistingReceivable = false)
|
||||
{
|
||||
var fees = ParcelBillingCalculator.CalculateReceivableFees(
|
||||
snapshotBinding.Price,
|
||||
@ -385,6 +386,11 @@ public partial class OrderService
|
||||
|
||||
if (fees.Count > 0)
|
||||
{
|
||||
if (replaceExistingReceivable)
|
||||
{
|
||||
await OrderRepository.DeleteReceivableFeesAsync(order.Id);
|
||||
}
|
||||
|
||||
await OrderRepository.UpsertFeesAsync(order.Id, fees);
|
||||
}
|
||||
}
|
||||
@ -857,6 +863,23 @@ public partial class OrderService
|
||||
}
|
||||
}
|
||||
|
||||
private static void SyncShipToDownstream(List<OrderWeight> weights, OrderWeight ship)
|
||||
{
|
||||
var row = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Downstream);
|
||||
if (row == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
row.Length = ship.Length;
|
||||
row.Width = ship.Width;
|
||||
row.Height = ship.Height;
|
||||
row.ActualWeight = ship.ActualWeight;
|
||||
row.VolumetricWeight = ship.VolumetricWeight;
|
||||
row.ChargeableWeight = ship.ChargeableWeight;
|
||||
row.Cubic = ship.Cubic;
|
||||
}
|
||||
|
||||
private static Order BuildDraftOrderEntity(
|
||||
OrderParcelCreateInput input,
|
||||
SalesProduct product,
|
||||
@ -1028,7 +1051,7 @@ public partial class OrderService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 入库收货后:覆盖 receive 件重尺、重算计费重,写入 inbound 阶段应收,并同步 ship/downstream 重量行。
|
||||
/// 入库收货后:覆盖 receive 件重尺、重算计费重,覆盖写入 inbound 阶段应收,并同步 ship/downstream 重量行。
|
||||
/// </summary>
|
||||
internal async Task ApplyReceiveBillingByWaybillAsync(
|
||||
string waybillNo,
|
||||
@ -1039,30 +1062,35 @@ public partial class OrderService
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(waybillNo))
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("运单号不能为空");
|
||||
}
|
||||
|
||||
if (!length.HasValue || !width.HasValue || !height.HasValue || !actualWeight.HasValue
|
||||
|| length <= 0 || width <= 0 || height <= 0 || actualWeight <= 0)
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("收货实重与长宽高不能为空且须大于 0");
|
||||
}
|
||||
|
||||
var order = await OrderRepository.GetByServiceNoAsync(waybillNo.Trim());
|
||||
if (order == null || string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
var order = await OrderRepository.GetByWaybillNoAsync(waybillNo.Trim());
|
||||
if (order == null)
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("未找到对应订单,请检查单号");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(order.ProductCode))
|
||||
{
|
||||
throw new BusinessException("订单未绑定销售产品,无法入库计费");
|
||||
}
|
||||
|
||||
if (order.OrderStatus is not (OrderStatusCodes.Predeclared or OrderStatusCodes.Received))
|
||||
{
|
||||
return;
|
||||
throw new BusinessException($"订单状态为「{OmsDocHelper.GetOrderStatusName(order.OrderStatus)}」,不允许入库收货");
|
||||
}
|
||||
|
||||
var product = await SalesProductRepository.GetByProductCodeAsync(order.ProductCode.Trim());
|
||||
if (product == null)
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("订单关联的销售产品不存在");
|
||||
}
|
||||
|
||||
var weights = (await OrderRepository.GetWeightsByOrderIdsAsync([order.Id]))
|
||||
@ -1071,7 +1099,7 @@ public partial class OrderService
|
||||
var receive = weights.FirstOrDefault(x => x.Type == OrderWeightTypeCodes.Receive);
|
||||
if (receive == null)
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("订单缺少收货件重尺记录");
|
||||
}
|
||||
|
||||
receive.Length = length;
|
||||
@ -1080,25 +1108,8 @@ public partial class OrderService
|
||||
receive.ActualWeight = actualWeight;
|
||||
receive.Cubic = ParcelBillingCalculator.CalculateCubic(length.Value, width.Value, height.Value);
|
||||
|
||||
ProductSnapshotBinding snapshotBinding;
|
||||
try
|
||||
{
|
||||
snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
}
|
||||
catch (BusinessException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, receive);
|
||||
}
|
||||
catch (BusinessException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var snapshotBinding = await ResolveSnapshotBindingFromOrderAsync(product, order);
|
||||
await ApplyChargeableWeightAsync(product, snapshotBinding.Rules, receive);
|
||||
SyncReceiveToShipAndDownstream(weights, receive);
|
||||
await OrderRepository.ReplaceWeightsAsync(order.Id, weights);
|
||||
|
||||
@ -1114,28 +1125,22 @@ public partial class OrderService
|
||||
recipient?.ZipCode);
|
||||
if (zoneKeys == null || zoneKeys.Count == 0)
|
||||
{
|
||||
return;
|
||||
throw new BusinessException("价格分区未匹配(规则快照)");
|
||||
}
|
||||
|
||||
var customer = order.CustomerId.HasValue
|
||||
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||
: null;
|
||||
|
||||
try
|
||||
{
|
||||
await UpsertReceivableFeesAsync(
|
||||
order,
|
||||
snapshotBinding,
|
||||
receive.ChargeableWeight ?? 0,
|
||||
zoneKeys,
|
||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||
customer?.CustomerGroup,
|
||||
OrderFeeChargingStageCodes.Inbound);
|
||||
}
|
||||
catch (BusinessException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await UpsertReceivableFeesAsync(
|
||||
order,
|
||||
snapshotBinding,
|
||||
receive.ChargeableWeight ?? 0,
|
||||
zoneKeys,
|
||||
order.CargoAttribute ?? CargoAttributeCodes.General,
|
||||
customer?.CustomerGroup,
|
||||
OrderFeeChargingStageCodes.Inbound,
|
||||
replaceExistingReceivable: true);
|
||||
|
||||
order.OrderStatus = OrderStatusCodes.Received;
|
||||
order.ReceiveTime = DateTime.Now;
|
||||
|
||||
@ -11,6 +11,8 @@ public interface IOrderRepository : IRepository<Order>
|
||||
|
||||
Task<Order?> GetByTrackNoAsync(string trackNo, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Order?> GetByWaybillNoAsync(string waybillNo, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Order?> GetByCustomerAndClientNoAsync(int customerId, string clientNo, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<Order?> GetByRequestIdAsync(string requestId, CancellationToken cancellationToken = default);
|
||||
@ -29,11 +31,21 @@ public interface IOrderRepository : IRepository<Order>
|
||||
|
||||
Task InsertDeclarationsAsync(IEnumerable<OrderDeclaration> declarations, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ReplaceDeclarationsAsync(int orderId, IEnumerable<OrderDeclaration> declarations, CancellationToken cancellationToken = default);
|
||||
|
||||
Task InsertWeightsAsync(IEnumerable<OrderWeight> weights, CancellationToken cancellationToken = default);
|
||||
|
||||
Task ReplaceWeightsAsync(int orderId, IEnumerable<OrderWeight> weights, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpsertFeesAsync(int orderId, IEnumerable<OrderFee> fees, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task DeletePayableFeesAsync(int orderId, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateSenderAsync(OrderSender sender, CancellationToken cancellationToken = default);
|
||||
|
||||
Task UpdateRecipientAsync(OrderRecipient recipient, CancellationToken cancellationToken = default);
|
||||
|
||||
Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
using Oms.SqlSugarCore.Repositories;
|
||||
@ -22,6 +23,29 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
return FirstOrDefaultAsync(x => x.TrackNo == trackNo, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Order?> GetByWaybillNoAsync(string waybillNo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = waybillNo.Trim();
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var order = await GetByServiceNoAsync(normalized, cancellationToken);
|
||||
if (order != null)
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
order = await GetByTrackNoAsync(normalized, cancellationToken);
|
||||
if (order != null)
|
||||
{
|
||||
return order;
|
||||
}
|
||||
|
||||
return await FirstOrDefaultAsync(x => x.ClientNo == normalized, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<Order?> GetByCustomerAndClientNoAsync(int customerId, string clientNo, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return FirstOrDefaultAsync(x => x.CustomerId == customerId && x.ClientNo == clientNo, cancellationToken);
|
||||
@ -112,6 +136,17 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
await db.Insertable(items).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ReplaceDeclarationsAsync(int orderId, IEnumerable<OrderDeclaration> declarations, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = declarations.ToArray();
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderDeclaration>().Where(x => x.OrderId == orderId).ExecuteCommandAsync(cancellationToken);
|
||||
if (items.Length > 0)
|
||||
{
|
||||
await db.Insertable(items).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task InsertWeightsAsync(IEnumerable<OrderWeight> weights, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var items = weights.ToArray();
|
||||
@ -171,6 +206,36 @@ public class OrderRepository : OmsSqlSugarRepository<Order>, IOrderRepository, I
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteReceivableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Receivable)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeletePayableFeesAsync(int orderId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
await db.Deleteable<OrderFee>()
|
||||
.Where(x => x.OrderId == orderId && x.Type == OrderFeeTypeCodes.Payable)
|
||||
.ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdateSenderAsync(OrderSender sender, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
sender.UpdateTime = DateTime.Now;
|
||||
await db.Updateable(sender).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task UpdateRecipientAsync(OrderRecipient recipient, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var db = await GetDbContextAsync(cancellationToken);
|
||||
recipient.UpdateTime = DateTime.Now;
|
||||
await db.Updateable(recipient).ExecuteCommandAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<List<OrderFee>> GetFeesByOrderIdsAsync(IEnumerable<int> orderIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ids = orderIds.Distinct().ToArray();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user