feat: complete dropship palletization workflow
This commit is contained in:
@@ -48,7 +48,8 @@ public interface FirstlegUnifiedOrderClient {
|
||||
Long deliveryWarehouseId, String deliveryWarehouseName, String channelCode, String channelName,
|
||||
Integer totalPackageCount, String locationCodes,
|
||||
LocalDateTime expectedArrivalAt, LocalDateTime requestedPickupAt, String feeReviewStatus,
|
||||
String companyName, String businessType, String taxMode, String containerMode,
|
||||
String companyName, String businessType, String fulfillmentType, String lxCode,
|
||||
String taxMode, String containerMode,
|
||||
Boolean customsInspection, String destinationType, String destinationCode,
|
||||
Boolean holdFlag, String currentHoldReason,
|
||||
BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg, BigDecimal actualVolumeCbm,
|
||||
|
||||
@@ -72,13 +72,27 @@ public class InternalOrderCargoController {
|
||||
if (tenantId <= 0 || orderId <= 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "租户和订单ID必须大于0");
|
||||
}
|
||||
var order = orders.findOwned(tenantId, orderId);
|
||||
if (order == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "订单不存在或不属于当前客户");
|
||||
}
|
||||
boolean oneClickDropship = "ONE_CLICK_DROPSHIP".equals(order.getFulfillmentType());
|
||||
return ApiResponse.success(orders.findCargoInheritedByOrder(tenantId, orderId).stream()
|
||||
.map(cargo -> new CargoLabelBinding(cargo.getId(), orderId, cargo.getItemNo(),
|
||||
cargo.getCargoName(), cargo.getSkuNo(), cargo.getPackageCount(), cargo.getLengthCm(),
|
||||
cargo.getWidthCm(), cargo.getHeightCm()))
|
||||
cargo.getCargoName(), cargo.getSkuNo(), cargo.getPackageCount(),
|
||||
unitsPerCarton(cargo, oneClickDropship), cargo.getLengthCm(), cargo.getWidthCm(),
|
||||
cargo.getHeightCm()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private Integer unitsPerCarton(OrderCargoEntity cargo, boolean oneClickDropship) {
|
||||
Integer value = cargo.getUnitsPerPackage();
|
||||
if (oneClickDropship && (value == null || value <= 0)) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "一件代发货物每箱件数缺失或无效");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@GetMapping("/{tenantId}/{orderId}/inbound-snapshot")
|
||||
public ApiResponse<FirstlegInboundClient.OrderApprovedInboundCommand> inboundSnapshot(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@@ -155,12 +169,20 @@ public class InternalOrderCargoController {
|
||||
}
|
||||
|
||||
public record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
String skuNo, Integer packageCount, Integer unitsPerCarton,
|
||||
java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, lengthCm, widthCm, heightCm);
|
||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, null,
|
||||
lengthCm, widthCm, heightCm);
|
||||
}
|
||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||
String skuNo, Integer packageCount, java.math.BigDecimal lengthCm,
|
||||
java.math.BigDecimal widthCm, java.math.BigDecimal heightCm) {
|
||||
this(cargoId, orderId, itemNo, cargoName, skuNo, packageCount, null,
|
||||
lengthCm, widthCm, heightCm);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public class PlatformUnifiedOrderController {
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('platform:order:list')")
|
||||
@Operation(summary = "统一订单列表", description = "返回七态产品主状态及测量、预装、装柜、派送摘要。")
|
||||
@Operation(summary = "统一订单列表", description = "返回八态产品主状态及测量、组托、预装、装柜、派送摘要。")
|
||||
public ApiResponse<PageResult<UnifiedOrderResponse.Row>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) Long tenantId,
|
||||
|
||||
@@ -12,8 +12,8 @@ import java.time.LocalDateTime;
|
||||
public record UnifiedOrderQuery(
|
||||
@Size(max = 64) String keyword,
|
||||
@Size(max = 64) String orderNo,
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED",
|
||||
"LOADING", "LOADED", "DELIVERED"})
|
||||
@Schema(allowableValues = {"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED",
|
||||
"WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"})
|
||||
@Size(max = 32) String mainStatus,
|
||||
Long customerId,
|
||||
Long receivingWarehouseId,
|
||||
|
||||
@@ -24,7 +24,7 @@ public final class UnifiedOrderResponse {
|
||||
BigDecimal estimatedVolumeCbm, BigDecimal expectedVolumeCbm,
|
||||
BigDecimal actualGrossWeightKg, BigDecimal actualVolumeCbm,
|
||||
LocalDateTime expectedArrivalAt, String feeReviewStatus,
|
||||
String companyName, String businessType, String pickupMethod,
|
||||
String companyName, String businessType, String fulfillmentType, String lxCode, String pickupMethod,
|
||||
String taxMode, String containerMode, Boolean customsInspection,
|
||||
String destinationType, String destinationCode,
|
||||
Integer totalPalletCount, BigDecimal chargeableWeightKg, BigDecimal chargeableVolumeCbm,
|
||||
|
||||
@@ -38,7 +38,8 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class UnifiedOrderQueryService {
|
||||
private static final Set<String> MAIN_STATUSES = Set.of(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
private static final Set<String> REVIEW_STATUSES = Set.of("UNRECORDED", "PENDING", "APPROVED", "REJECTED");
|
||||
private static final List<OrderStatus> ACTIVE_ORDER_STATUSES = List.copyOf(EnumSet.of(
|
||||
OrderStatus.SUBMITTED, OrderStatus.APPROVED, OrderStatus.ACCEPTED,
|
||||
@@ -311,7 +312,8 @@ public class UnifiedOrderQueryService {
|
||||
order.getInboundCode(),
|
||||
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.getExpectedArrivalAt(), null, null, null, order.getFulfillmentType(), order.getLxCode(),
|
||||
order.getPickupMethod(), order.getTaxMode(), order.getContainerMode(),
|
||||
order.getCustomsInspection(), order.getDestinationType(), order.getAmazonWarehouseCode(),
|
||||
null, null, null, null, null, null, null, order.getExpressCompany(), order.getExpressTrackingNo(),
|
||||
StringUtils.hasText(creatorName) ? creatorName : string(order.getCreatedBy()), skuNos(cargo), totalQuantity(cargo), shipmentId(order, cargo), referenceId(order, cargo),
|
||||
@@ -334,7 +336,8 @@ public class UnifiedOrderQueryService {
|
||||
order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(),
|
||||
inbound.companyName(), inbound.businessType(), order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.companyName(), inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||
order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||
inbound.actualNetWeightKg(),
|
||||
@@ -361,7 +364,8 @@ public class UnifiedOrderQueryService {
|
||||
order == null ? null : order.getEstimatedVolumeCbm(), volumeCbm(cargo),
|
||||
inbound.actualGrossWeightKg(), inbound.actualVolumeCbm(),
|
||||
order != null && order.getExpectedArrivalAt() != null ? order.getExpectedArrivalAt() : inbound.expectedArrivalAt(), inbound.feeReviewStatus(), inbound.companyName(),
|
||||
inbound.businessType(), order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.businessType(), inbound.fulfillmentType(), inbound.lxCode(),
|
||||
order == null ? null : order.getPickupMethod(), inbound.taxMode(), inbound.containerMode(),
|
||||
inbound.customsInspection(), inbound.destinationType(), inbound.destinationCode(), inbound.palletCount(),
|
||||
inbound.chargeableWeightKg(), inbound.chargeableVolumeCbm(), inbound.volumeWeightKg(),
|
||||
inbound.actualNetWeightKg(),
|
||||
@@ -502,7 +506,7 @@ public class UnifiedOrderQueryService {
|
||||
return switch (status) {
|
||||
case "WAIT_MEASURE", "MEASURING", "MEASURE_PENDING", "MEASURE_REJECTED",
|
||||
"WAIT_PRICING", "FEE_CONFIRMING", "INBOUNDING" -> "WAIT_MEASURE";
|
||||
case "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
case "INBOUNDED", "WAIT_PALLETIZE", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED" -> status;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ paths:
|
||||
get:
|
||||
summary: 平台统一订单列表
|
||||
description: >-
|
||||
统一返回七态产品主状态、下一步动作及测量/预装/装柜/派送摘要,需要 platform:order:list。
|
||||
统一返回八态产品主状态、下一步动作及测量/组托/预装/装柜/派送摘要,需要 platform:order:list。
|
||||
入仓码来源由全系统统一模式控制:AUTO 在订单提交时按物流渠道生成,格式为物流前缀+YYMMDD+当天全平台票序;
|
||||
LEGACY_MANUAL 在草稿中录入旧系统入仓码。两种模式下入仓码一经提交均固定;有入库单时同时返回
|
||||
inboundId、inboundVersion、loadingVersion、pickingVersion;inboundVersion
|
||||
@@ -617,7 +617,7 @@ components:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
keyword: {type: string, maxLength: 64}
|
||||
orderNo: {type: string, maxLength: 64}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
mainStatus: {type: string, enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]}
|
||||
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
receivingWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
destinationWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
@@ -693,9 +693,10 @@ components:
|
||||
asOf: {type: string, format: date-time}
|
||||
UnifiedMainStatus:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
description: >-
|
||||
统一订单七态产品主状态。WAIT_SPLIT 为正式待分柜状态,INBOUNDED 仅兼容存量已入库记录。
|
||||
统一订单八态产品主状态。ONE_CLICK_DROPSHIP 测量计费完成后进入 WAIT_PALLETIZE,封托完成后进入 WAIT_SPLIT;
|
||||
STANDARD 保持既有流程,INBOUNDED 仅兼容存量已入库记录。
|
||||
UnifiedOrderPageEnvelope:
|
||||
type: object
|
||||
properties:
|
||||
@@ -766,6 +767,8 @@ components:
|
||||
feeReviewStatus: {type: string, nullable: true, description: Firstleg 入仓费用审核快照;财务状态不在本字段表达}
|
||||
companyName: {type: string, nullable: true}
|
||||
businessType: {type: string, nullable: true}
|
||||
fulfillmentType: {type: string, enum: [STANDARD, ONE_CLICK_DROPSHIP], nullable: true, description: 独立履约类型,不复用计费业务类型}
|
||||
lxCode: {type: string, nullable: true, description: 一件代发可选 LX 编码;普通头程或未填写时为 null}
|
||||
pickupMethod: {type: string, nullable: true}
|
||||
requestedPickupAt: {type: string, format: date-time, nullable: true, description: Customer-requested pickup appointment from the pickup snapshot}
|
||||
taxMode: {type: string, nullable: true}
|
||||
@@ -961,13 +964,13 @@ components:
|
||||
fulfillmentType:
|
||||
type: string
|
||||
enum: [STANDARD, ONE_CLICK_DROPSHIP]
|
||||
default: STANDARD
|
||||
description: 独立履约类型;省略为普通头程,不复用业务/计费 businessType
|
||||
lxCode:
|
||||
type: string
|
||||
nullable: true
|
||||
maxLength: 128
|
||||
description: 一件代发绑定的领星入库单 LX 代码
|
||||
default: STANDARD
|
||||
description: 独立履约类型;省略为普通头程,不复用业务/计费 businessType
|
||||
inboundCode:
|
||||
type: string
|
||||
maxLength: 40
|
||||
@@ -1194,17 +1197,17 @@ components:
|
||||
name: mainStatus
|
||||
in: query
|
||||
required: false
|
||||
description: 统一订单七态主状态筛选;WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询
|
||||
description: 统一订单八态主状态筛选;WAIT_PALLETIZE、WAIT_SPLIT 与存量兼容状态 INBOUNDED 独立查询
|
||||
schema:
|
||||
type: string
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, INBOUNDED, LOADING, LOADED, DELIVERED, WAIT_SPLIT]
|
||||
enum: [PENDING_REVIEW, WAIT_MEASURE, WAIT_PALLETIZE, INBOUNDED, WAIT_SPLIT, LOADING, LOADED, DELIVERED]
|
||||
UnifiedInboundCode:
|
||||
{name: inboundCode, in: query, required: false, description: 订单创建时生成的入仓码,所有订单状态均可查询, schema: {type: string, maxLength: 40}}
|
||||
UnifiedInternalOrderStatus:
|
||||
name: internalOrderStatus
|
||||
in: query
|
||||
required: false
|
||||
description: Order 内部生命周期状态;不是七态产品主状态
|
||||
description: Order 内部生命周期状态;不是八态产品主状态
|
||||
schema: {type: string, enum: [DRAFT, SUBMITTED, APPROVED, ACCEPTED, REJECTED, CANCELLED, PROCESSING, COMPLETED]}
|
||||
UnifiedExpectedArrivalDate:
|
||||
{name: expectedArrivalDate, in: query, required: false, schema: {type: string, format: date}}
|
||||
|
||||
@@ -88,7 +88,10 @@ class InternalOrderCargoControllerTest {
|
||||
@Test
|
||||
void listsCargoInRepositoryOrderForStableCartonAllocation() {
|
||||
OrderCargoEntity first = cargo(31L, 1, 3, "60", "40", "30");
|
||||
first.setUnitsPerPackage(24);
|
||||
OrderCargoEntity second = cargo(32L, 2, 2, "50", "30", "20");
|
||||
OrderEntity order = new OrderEntity(); order.setId(21L); order.setFulfillmentType("STANDARD");
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(first, second));
|
||||
|
||||
var response = controller.listForCartonLabels("internal-token", 11L, 21L);
|
||||
@@ -97,10 +100,68 @@ class InternalOrderCargoControllerTest {
|
||||
assertThat(response.data()).extracting(InternalOrderCargoController.CargoLabelBinding::cargoId)
|
||||
.containsExactly(31L, 32L);
|
||||
assertThat(response.data().get(0).packageCount()).isEqualTo(3);
|
||||
assertThat(response.data().get(0).unitsPerCarton()).isEqualTo(24);
|
||||
assertThat(response.data().get(1).unitsPerCarton()).isNull();
|
||||
assertThat(response.data().get(0).lengthCm()).isEqualByComparingTo("60");
|
||||
verify(orders).findOwned(11L, 21L);
|
||||
verify(orders).findCargoInheritedByOrder(11L, 21L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipReturnsAuthoritativePositiveUnitsPerCarton() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
cargo.setUnitsPerPackage(12);
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(cargo));
|
||||
|
||||
var response = controller.listForCartonLabels("internal-token", 11L, 21L);
|
||||
|
||||
assertThat(response.data()).singleElement()
|
||||
.extracting(InternalOrderCargoController.CargoLabelBinding::unitsPerCarton)
|
||||
.isEqualTo(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipFailsClosedWhenUnitsPerCartonIsMissing() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L))
|
||||
.thenReturn(List.of(cargo(31L, 1, 3, "60", "40", "30")));
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 11L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("每箱件数缺失或无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oneClickDropshipFailsClosedWhenUnitsPerCartonIsNotPositive() {
|
||||
OrderEntity order = new OrderEntity();
|
||||
order.setId(21L); order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
cargo.setUnitsPerPackage(0);
|
||||
when(orders.findOwned(11L, 21L)).thenReturn(order);
|
||||
when(orders.findCargoInheritedByOrder(11L, 21L)).thenReturn(List.of(cargo));
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 11L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("每箱件数缺失或无效");
|
||||
}
|
||||
|
||||
@Test
|
||||
void cartonLabelListDoesNotExposeCargoWhenOrderIsOutsideCustomerScope() {
|
||||
when(orders.findOwned(12L, 21L)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() -> controller.listForCartonLabels("internal-token", 12L, 21L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("订单不存在或不属于当前客户");
|
||||
|
||||
verify(orders).findOwned(12L, 21L);
|
||||
verifyNoMoreInteractions(orders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void childOrderCanInheritRootCargoBindingWithoutCrossCustomerLeak() {
|
||||
OrderCargoEntity cargo = cargo(31L, 1, 3, "60", "40", "30");
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.baiyue.order.controller;
|
||||
|
||||
import com.baiyue.order.client.FirstlegUnifiedOrderClient;
|
||||
import com.baiyue.order.client.FirstlegColoadGroupClient;
|
||||
import com.baiyue.order.dto.UnifiedOrderQuery;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
@@ -26,7 +28,7 @@ class UnifiedOrderApiContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentsSevenProductMainStatuses() throws Exception {
|
||||
void documentsEightProductMainStatuses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/order-v1.yaml")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
Map<String, Object> document = stringMap(new Yaml().load(yaml));
|
||||
@@ -40,22 +42,32 @@ class UnifiedOrderApiContractTest {
|
||||
Map<String, Object> mainStatus = stringMap(parameters.get("UnifiedMainStatus"));
|
||||
Map<String, Object> schema = stringMap(mainStatus.get("schema"));
|
||||
assertThat(stringList(schema.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED", "WAIT_SPLIT");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(mainStatus.get("description").toString())
|
||||
.contains("WAIT_SPLIT", "INBOUNDED");
|
||||
.contains("WAIT_PALLETIZE", "WAIT_SPLIT", "INBOUNDED");
|
||||
|
||||
Map<String, Object> schemas = stringMap(components.get("schemas"));
|
||||
Map<String, Object> responseStatus = stringMap(schemas.get("UnifiedMainStatus"));
|
||||
assertThat(stringList(responseStatus.get("enum"))).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString()).contains("WAIT_SPLIT", "INBOUNDED");
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
assertThat(responseStatus.get("description").toString())
|
||||
.contains("WAIT_PALLETIZE", "WAIT_SPLIT", "INBOUNDED");
|
||||
Schema queryAnnotation = UnifiedOrderQuery.class.getDeclaredField("mainStatus")
|
||||
.getAnnotation(Schema.class);
|
||||
assertThat(queryAnnotation).isNotNull();
|
||||
assertThat(queryAnnotation.allowableValues()).containsExactly(
|
||||
"PENDING_REVIEW", "WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT",
|
||||
"LOADING", "LOADED", "DELIVERED");
|
||||
|
||||
Map<String, Object> row = stringMap(schemas.get("UnifiedOrderRow"));
|
||||
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",
|
||||
"expectedArrivalAt", "requestedPickupAt", "feeReviewStatus", "companyName", "businessType",
|
||||
"fulfillmentType", "lxCode", "pickupMethod",
|
||||
"taxMode", "containerMode", "customsInspection", "destinationType", "destinationCode",
|
||||
"totalPalletCount", "chargeableWeightKg", "chargeableVolumeCbm", "volumetricWeightKg", "totalNetWeightKg",
|
||||
"measurementCompletedAt", "inboundCompletedAt", "expressCompany", "expressTrackingNo",
|
||||
@@ -86,7 +98,7 @@ class UnifiedOrderApiContractTest {
|
||||
assertThat(page.getAnnotation(PostMapping.class).value()).containsExactly("/query");
|
||||
assertThat(List.of(FirstlegUnifiedOrderClient.Inbound.class.getRecordComponents()).stream()
|
||||
.map(component -> component.getName()))
|
||||
.contains("channelCode", "channelName", "holdFlag", "currentHoldReason")
|
||||
.contains("channelCode", "channelName", "holdFlag", "currentHoldReason", "fulfillmentType", "lxCode")
|
||||
.doesNotContain("holdCargo", "holdReason");
|
||||
}
|
||||
|
||||
|
||||
@@ -549,6 +549,70 @@ class UnifiedOrderQueryServiceTest {
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_SPLIT");
|
||||
}
|
||||
|
||||
@Test
|
||||
void waitPalletizeFilterReturnsDropshipProjectionAndForwardsToFirstleg() {
|
||||
UnifiedOrderQuery query = query("WAIT_PALLETIZE");
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(summary), 1, 20, 1, 1)));
|
||||
when(orders.findByIds(eq(10001L), eq(List.of(77L)))).thenReturn(List.of(order(OrderStatus.PROCESSING)));
|
||||
|
||||
var result = service.page(10001L, query);
|
||||
|
||||
assertThat(result.records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
assertThat(row.nextAction()).isEqualTo("PALLETIZE_DROPSHIP");
|
||||
assertThat(row.fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(row.lxCode()).isEqualTo("LX-20260908-01");
|
||||
});
|
||||
ArgumentCaptor<FirstlegUnifiedOrderClient.Query> captor =
|
||||
ArgumentCaptor.forClass(FirstlegUnifiedOrderClient.Query.class);
|
||||
verify(firstleg).page(eq("internal-token"), captor.capture());
|
||||
assertThat(captor.getValue().mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderOnlyProjectionUsesExistingFulfillmentFieldsWithoutCreatingInboundFacts() {
|
||||
UnifiedOrderQuery query = query("PENDING_REVIEW");
|
||||
OrderEntity order = order(OrderStatus.SUBMITTED);
|
||||
order.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
order.setLxCode("LX-ORDER-01");
|
||||
when(orders.unifiedPage(eq(10001L), eq(query), eq(List.of(OrderStatus.SUBMITTED))))
|
||||
.thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1));
|
||||
|
||||
assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> {
|
||||
assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW");
|
||||
assertThat(row.fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(row.lxCode()).isEqualTo("LX-ORDER-01");
|
||||
assertThat(row.inboundId()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailMapsWaitPalletizeSummaryAndTimeline() {
|
||||
OrderEntity order = order(OrderStatus.PROCESSING);
|
||||
when(orders.findOwned(10001L, 77L)).thenReturn(order);
|
||||
when(orders.findCargo(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findParties(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findNotes(10001L, 77L)).thenReturn(List.of());
|
||||
when(orders.findHistory(10001L, 77L)).thenReturn(List.of());
|
||||
var timeline = new FirstlegUnifiedOrderClient.Timeline(
|
||||
"FIRSTLEG", "STATUS", "WAIT_MEASURE", "WAIT_PALLETIZE", "MEASUREMENT_APPROVED",
|
||||
null, 9L, "平台操作员", LocalDateTime.of(2026, 9, 8, 10, 0));
|
||||
var summary = summary("WAIT_PALLETIZE", "PALLETIZE_DROPSHIP");
|
||||
when(firstleg.detail(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new FirstlegUnifiedOrderClient.Detail(summary, List.of(timeline))));
|
||||
|
||||
var result = service.detail(10001L, 77L, false);
|
||||
|
||||
assertThat(result.summary().mainStatus()).isEqualTo("WAIT_PALLETIZE");
|
||||
assertThat(result.summary().fulfillmentType()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||
assertThat(result.summary().lxCode()).isEqualTo("LX-20260908-01");
|
||||
assertThat(result.timeline()).singleElement()
|
||||
.extracting(UnifiedOrderResponse.TimelineEvent::mainStatus)
|
||||
.isEqualTo("WAIT_PALLETIZE");
|
||||
}
|
||||
|
||||
@Test
|
||||
void listKeepsLoadedFulfillmentAheadOfRawApprovedOrderStatus() {
|
||||
UnifiedOrderQuery query = query(null);
|
||||
@@ -728,7 +792,7 @@ class UnifiedOrderQueryServiceTest {
|
||||
@Test
|
||||
void fulfillmentTabsForwardStatusSearchAndPaginationToFirstleg() {
|
||||
List<String> statuses = List.of(
|
||||
"WAIT_MEASURE", "INBOUNDED", "LOADING", "LOADED", "DELIVERED");
|
||||
"WAIT_MEASURE", "WAIT_PALLETIZE", "INBOUNDED", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED");
|
||||
when(firstleg.page(eq("internal-token"), any()))
|
||||
.thenReturn(ApiResponse.success(new PageResult<>(List.of(), 3, 20, 0, 0)));
|
||||
|
||||
@@ -840,6 +904,8 @@ class UnifiedOrderQueryServiceTest {
|
||||
when(inbound.locationCodes()).thenReturn("A0-01, B2-03");
|
||||
when(inbound.companyName()).thenReturn("测试客户有限公司");
|
||||
when(inbound.businessType()).thenReturn("SEA_LCL");
|
||||
when(inbound.fulfillmentType()).thenReturn("ONE_CLICK_DROPSHIP");
|
||||
when(inbound.lxCode()).thenReturn("LX-20260908-01");
|
||||
when(inbound.palletCount()).thenReturn(2);
|
||||
when(inbound.actualGrossWeightKg()).thenReturn(new BigDecimal("1787.500"));
|
||||
when(inbound.actualNetWeightKg()).thenReturn(new BigDecimal("1747.500"));
|
||||
|
||||
Reference in New Issue
Block a user