feat: complete dropship palletization workflow

This commit is contained in:
2026-09-08 17:24:48 +08:00
parent e9182e8cfe
commit 6c2d169798
61 changed files with 1242 additions and 1358 deletions
@@ -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;
};
}
+12 -9
View File
@@ -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"));