feat: add inbound hold lifecycle

This commit is contained in:
2026-09-07 11:20:10 +08:00
parent e52ecb9315
commit 0c8f6fe2b3
48 changed files with 1280 additions and 16 deletions
@@ -50,6 +50,7 @@ public interface FirstlegUnifiedOrderClient {
LocalDateTime expectedArrivalAt, LocalDateTime requestedPickupAt, String feeReviewStatus,
String companyName, String businessType, String taxMode, String containerMode,
Boolean customsInspection, String destinationType, String destinationCode,
Boolean holdFlag, String currentHoldReason,
BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg, BigDecimal actualVolumeCbm,
BigDecimal volumeWeightKg, BigDecimal chargeableVolumeCbm, Integer palletCount,
BigDecimal chargeableWeightKg, LocalDateTime measureCompletedAt,
@@ -19,6 +19,7 @@ public final class UnifiedOrderResponse {
Long receivingWarehouseId, String receivingWarehouseName,
Long destinationWarehouseId, String destinationWarehouseName,
Long inboundId, Integer inboundVersion, String inboundCode, LocalDate expectedShipDate,
Boolean holdFlag, String currentHoldReason,
Integer totalPackageCount, String recommendedPalletType, String locationCodes,
BigDecimal estimatedVolumeCbm, BigDecimal expectedVolumeCbm,
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
@@ -287,7 +287,7 @@ public class UnifiedOrderQueryService {
order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), null, null,
order.getInboundCode(),
order.getExpectedShipDate(), totalPackageCount(cargo), null, null,
order.getExpectedShipDate(), null, null, totalPackageCount(cargo), null, null,
order.getEstimatedVolumeCbm(), volumeCbm(cargo), null, null,
order.getExpectedArrivalAt(), null, null, null, order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
@@ -306,7 +306,8 @@ public class UnifiedOrderQueryService {
order.getCustomerId(), order.getCustomerName(),
order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(),
order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(),
order.getExpectedShipDate(), displayedPackageCount(order, cargo, inbound), null,
order.getExpectedShipDate(), inbound.holdFlag(), inbound.currentHoldReason(),
displayedPackageCount(order, cargo, inbound), null,
distinctLocationCodes(inbound.locationCodes()),
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
@@ -333,7 +334,7 @@ public class UnifiedOrderQueryService {
inbound.customerId(), inbound.customerName(), inbound.channelCode(), inbound.channelName(),
order != null && Boolean.TRUE.equals(order.getChannelChanged()), inbound.warehouseId(),
inbound.warehouseName(), inbound.deliveryWarehouseId(), inbound.deliveryWarehouseName(),
inbound.id(), inbound.version(), inbound.inboundCode(), null,
inbound.id(), inbound.version(), inbound.inboundCode(), null, inbound.holdFlag(), inbound.currentHoldReason(),
displayedPackageCount(order, cargo, inbound), null, distinctLocationCodes(inbound.locationCodes()),
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
@@ -753,6 +753,8 @@ components:
inboundVersion: {type: integer, nullable: true, description: C04 loading/complete 的 expectedVersion}
inboundCode: {type: string, nullable: true, description: 当前订单片段的权威入仓码;缺失时为 null,不得回退为 orderId 或 orderNo}
expectedShipDate: {type: string, format: date, nullable: true}
holdFlag: {type: boolean, nullable: true, description: Firstleg 入库单当前扣货标识;尚未生成入库单时为 null}
currentHoldReason: {type: string, nullable: true, description: Firstleg 入库单当前扣货原因;未扣货或尚未生成入库单时为 null}
totalPackageCount: {type: integer, nullable: true, description: 无入仓事实时为当前订单有效货物箱数合计;存在入仓业务单元后为该单元履约总箱数,拆分后不以父单原始箱数覆盖}
recommendedPalletType: {type: string, nullable: true, description: 当前无列表级权威事实时返回 null,不得返回固定托盘类型}
locationCodes: {type: string, nullable: true, description: "库位编码汇总;按首次出现顺序去重并使用中文顿号分隔"}
@@ -54,6 +54,7 @@ class UnifiedOrderApiContractTest {
assertThat(stringMap(row.get("properties"))).containsKeys(
"orderId", "mainStatus", "channelCode", "inboundId", "inboundVersion", "measurement", "preload",
"loading", "delivery", "transport", "customerRemark", "latestInternalNote",
"holdFlag", "currentHoldReason",
"expectedArrivalAt", "requestedPickupAt", "feeReviewStatus", "companyName", "businessType", "pickupMethod",
"taxMode", "containerMode", "customsInspection", "destinationType", "destinationCode",
"totalPalletCount", "chargeableWeightKg", "chargeableVolumeCbm", "volumetricWeightKg", "totalNetWeightKg",
@@ -64,6 +65,8 @@ class UnifiedOrderApiContractTest {
assertThat(inboundCode.get("description").toString()).contains("不得回退");
Map<String, Object> channelCode = stringMap(stringMap(row.get("properties")).get("channelCode"));
assertThat(channelCode.get("description").toString()).contains("不由 channelName 推导");
assertThat(stringMap(row.get("properties")))
.doesNotContainKeys("holdCargo", "holdReason");
Map<String, Object> coLoadGroup = stringMap(schemas.get("UnifiedCoLoadGroupSummary"));
assertThat(stringMap(coLoadGroup.get("properties")))
.containsOnlyKeys("status", "memberCount", "memberInboundCodes");
@@ -82,7 +85,9 @@ class UnifiedOrderApiContractTest {
"page", String.class, FirstlegUnifiedOrderClient.Query.class);
assertThat(page.getAnnotation(PostMapping.class).value()).containsExactly("/query");
assertThat(List.of(FirstlegUnifiedOrderClient.Inbound.class.getRecordComponents()).stream()
.map(component -> component.getName())).contains("channelCode", "channelName");
.map(component -> component.getName()))
.contains("channelCode", "channelName", "holdFlag", "currentHoldReason")
.doesNotContain("holdCargo", "holdReason");
}
@Test
@@ -74,6 +74,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.inboundCode()).isEqualTo("RK-77");
assertThat(row.channelCode()).isEqualTo("MATSON");
assertThat(row.inboundId()).isNull();
assertThat(row.holdFlag()).isNull();
assertThat(row.currentHoldReason()).isNull();
assertThat(row.customerRemark()).isEqualTo("客户要求提货前联系");
assertThat(row.latestInternalNote()).isEqualTo("客服已确认提货时间");
assertThat(row.relation()).isNull();
@@ -98,6 +100,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(row.nextAction()).isEqualTo("APPROVE_ORDER");
assertThat(row.inboundId()).isEqualTo(88L);
assertThat(row.internalOrderStatus()).isEqualTo(OrderStatus.SUBMITTED);
assertThat(row.holdFlag()).isTrue();
assertThat(row.currentHoldReason()).isEqualTo("等待客户补充资料");
});
}
@@ -592,6 +596,8 @@ class UnifiedOrderQueryServiceTest {
assertThat(result.summary().mainStatus()).isEqualTo("LOADED");
assertThat(result.summary().internalOrderStatus()).isEqualTo(OrderStatus.APPROVED);
assertThat(result.summary().inboundId()).isEqualTo(88L);
assertThat(result.summary().holdFlag()).isTrue();
assertThat(result.summary().currentHoldReason()).isEqualTo("等待客户补充资料");
ArgumentCaptor<FirstlegUnifiedOrderClient.DetailRequest> captor =
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.DetailRequest.class);
verify(firstleg).detail(eq("internal-token"), captor.capture());
@@ -701,6 +707,8 @@ class UnifiedOrderQueryServiceTest {
when(inbound.deliveryWarehouseName()).thenReturn("ONT8");
when(inbound.channelName()).thenReturn("美森");
when(inbound.channelCode()).thenReturn("MATSON");
when(inbound.holdFlag()).thenReturn(true);
when(inbound.currentHoldReason()).thenReturn("等待客户补充资料");
when(inbound.totalPackageCount()).thenReturn(10);
when(inbound.locationCodes()).thenReturn("A0-01, B2-03");
when(inbound.companyName()).thenReturn("测试客户有限公司");