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
@@ -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"));