This commit is contained in:
2026-09-07 15:07:34 +08:00
parent 693e10188c
commit 1b47158770
37 changed files with 688 additions and 144 deletions
@@ -35,6 +35,20 @@ class DispatchCandidateMapperContractTest {
assertTrue(sql.contains("pos.available_packages > 0"));
}
@Test
void candidateWarehouseComesFromPhysicalInventoryOrFrozenInboundPlan() throws Exception {
for (Method method : List.of(
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
String.class, String.class, String.class),
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL"));
assertTrue(sql.contains("pos.overseas_warehouse_id"));
assertTrue(sql.contains("CAST(ip.overseas_warehouse_id AS CHAR)"));
assertTrue(sql.contains("ll_inventory_balance"));
}
}
private void assertConstructorColumnOrder(Method method) {
String sql = String.join(" ", method.getAnnotation(Select.class).value());
int previous = -1;
@@ -127,6 +127,21 @@ class DispatchServiceTest {
verify(items).insert(any(DispatchItem.class));
}
@Test
void preArrangeRejectsPalletWithoutResolvedOverseasWarehouseWithPreciseReason() {
DispatchPalletCandidate unresolved = new DispatchPalletCandidate(1L, 10L, 20L, 2L,
null, null, "LMO1", "LMO1", "RK1", "LIP1-P001", "ONT8", "ONT8", "ONT8", "ONT8",
12, new BigDecimal("1.2000"), "RECEIVED");
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(unresolved));
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.preArrange("100", "7",
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
assertEquals("所选托盘中存在尚未关联海外仓的托盘", exception.getReason());
verify(orders, never()).insert(any(DispatchOrder.class));
}
@Test
void removingPalletFromOwnedDraftImmediatelyReleasesLock() {
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
@@ -17,5 +17,21 @@ class InboundPlanOrderMapperContractTest {
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
assertTrue(sql.contains("0 AS destinationMismatchCount"));
assertTrue(sql.contains("o.overseas_warehouse_id AS overseasWarehouseId"));
assertTrue(sql.contains("o.overseas_warehouse_code AS overseasWarehouseCode"));
assertTrue(sql.contains("o.overseas_warehouse_name AS overseasWarehouseName"));
}
@Test
void receivingWarehouseSnapshotIsReadFromTenantScopedOrderFact() throws Exception {
Select select = InboundPlanOrderMapper.class
.getMethod("findReceivingWarehouse", String.class, Long.class)
.getAnnotation(Select.class);
String sql = String.join(" ", select.value());
assertTrue(sql.contains("overseas_warehouse_code AS overseasWarehouseCode"));
assertTrue(sql.contains("overseas_warehouse_name AS overseasWarehouseName"));
assertTrue(sql.contains("customer_id=#{customerId} AND id=#{orderId}"));
assertTrue(!sql.contains("destination_warehouse"));
}
}
@@ -112,9 +112,6 @@ class ReceivingContainerServiceBehaviorTest {
when(sessions.insert(any(ReceivingContainerSession.class))).thenReturn(1);
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
when(plans.claimReceivingWarehouse("100", 2L, 100L)).thenReturn(1);
InboundPlan claimed = plan("CONT-A"); claimed.setOverseasWarehouseId(100L);
when(plans.selectById(2L)).thenReturn(claimed);
var response = service.prepare("7", "receiver", 100L, "CONT-A",
new ReceivingContainerRequests.Action("same-key"));
assertThat(response.overseasWarehouseId()).isEqualTo(100L);
@@ -652,7 +649,7 @@ class ReceivingContainerServiceBehaviorTest {
private static InboundPlan plan(String containerNo) {
InboundPlan value = new InboundPlan();
value.setId(2L); value.setPlanNo("PLAN-1"); value.setCustomerId("100"); value.setContainerNo(containerNo);
value.setStatus("SIGNED");
value.setStatus("SIGNED"); value.setOverseasWarehouseId(100L);
return value;
}
@@ -668,7 +665,9 @@ class ReceivingContainerServiceBehaviorTest {
private static LastlegOrder order(String destinationType) {
LastlegOrder value = new LastlegOrder();
value.setId(1L); value.setCustomerId("100"); value.setDestinationType(destinationType);
value.setOverseasWarehouseId("100"); value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
value.setOverseasWarehouseId("100"); value.setOverseasWarehouseCode("SHARED-LAX");
value.setOverseasWarehouseName("Shared LAX");
value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
return value;
}
@@ -690,7 +689,8 @@ class ReceivingContainerServiceBehaviorTest {
private static ReceivingContainerSession session(String containerNo, String status) {
ReceivingContainerSession value = new ReceivingContainerSession();
value.setId(8L); value.setCustomerId("100"); value.setContainerNo(containerNo); value.setOperatorKey("7");
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseName("Shared LAX");
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseCode("SHARED-LAX");
value.setOverseasWarehouseName("Shared LAX");
value.setStatus(status); value.setVersion(0);
return value;
}
@@ -48,9 +48,9 @@ class InboundPlanPaginationTest {
null, "PENDING_ASSIGNMENT", null, null, "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
LocalDateTime.of(2026, 8, 20, 10, 0));
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null,
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
when(queryMapper.count("100", query)).thenReturn(11L);
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
@@ -59,6 +59,8 @@ class InboundPlanPaginationTest {
assertThat(result.records()).containsExactly(row);
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
assertThat(result.records().get(0).appointmentStatus()).isNull();
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
assertThat(result.pages()).isEqualTo(2);
verify(queryMapper).count("100", query);
verify(queryMapper).rows("100", query, 10, 10);
@@ -71,6 +73,10 @@ class InboundPlanPaginationTest {
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
"o.overseas_warehouse_name overseasWarehouseName",
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
"q.destinationWarehouseCode", "q.warehouse",
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
@@ -64,6 +64,9 @@ class InboundPlanServiceTest {
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(orderMapper.findReceivingWarehouse(any(), any())).thenReturn(
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
}
@Test
@@ -105,12 +108,15 @@ class InboundPlanServiceTest {
verify(orderMapper).lockOrder("100", 2L);
assertEquals(2L, result.sourceOrderId());
assertEquals("MSCU6639870", result.containerNo());
assertEquals("US-LAX-01", result.overseasWarehouseCode());
assertEquals("洛杉矶仓", result.overseasWarehouseName());
}
@Test
void convertsAConsistentDestinationAssignedOrder() {
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10,
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10, 0,
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
"2090000000000000001", "LAX-02", "Los Angeles 02", 1, 0);
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(assigned));
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
@@ -148,6 +154,32 @@ class InboundPlanServiceTest {
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
}
@Test
void detailReturnsReceivingWarehouseFromTenantScopedOrderFact() {
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
when(planMapper.selectOne(any())).thenReturn(plan);
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot("88", "US-NY-01", "纽约仓"));
var result = service.detailByPlanNo("100", "LIP-1");
assertEquals("US-NY-01", result.overseasWarehouseCode());
assertEquals("纽约仓", result.overseasWarehouseName());
verify(orderMapper).findReceivingWarehouse("100", 1L);
}
@Test
void detailFailsClosedWhenReceivingWarehouseFactIsMissing() {
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
when(planMapper.selectOne(any())).thenReturn(plan);
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(null);
ResponseStatusException error = assertThrows(ResponseStatusException.class,
() -> service.detailByPlanNo("100", "LIP-1"));
assertEquals(409, error.getStatusCode().value());
}
@Test
void rejectsReversedInboundPlanDateRange() {
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null, null,
@@ -160,7 +192,9 @@ class InboundPlanServiceTest {
@Test
void rejectsMissingDestinationWarehouse() {
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
null, null, null, 1, 0);
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
@@ -190,7 +224,9 @@ class InboundPlanServiceTest {
@Test
void transferWarehouseOverridesOriginalCargoDestination() {
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, 0,
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
"WH-B", "WH-B", "Warehouse B", 1, 1);
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
.thenReturn(1);
@@ -332,7 +368,9 @@ class InboundPlanServiceTest {
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
null, null, null, 1, 0)));
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
@@ -419,12 +457,14 @@ class InboundPlanServiceTest {
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
return new SourceOrderSnapshot(id, customerId, "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
BigDecimal.ONE, 10, 0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
warehouse, warehouse, warehouse, 1, 0);
}
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
return new SourceOrderSnapshot(id, "100", "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
warehouse, warehouse, warehouse, 1, 0);
}
}
@@ -35,8 +35,14 @@ class MerchantOverseasOpenApiContractTest {
.contains("#/components/schemas/TruckDetailResponse")
.contains("#/components/schemas/LooseboardDetailResponse")
.contains("#/components/schemas/InboundPlanDetailResponse")
.contains("overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }")
.contains("overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }")
.contains("PENDING_REVIEW").contains("PENDING_ACCEPTANCE")
.contains("version 必填").contains("幂等冲突");
assertThat(yaml).contains("/api/platform/v1/last-leg/inbound-plans:")
.contains("/api/platform/v1/last-leg/inbound-plans/{planNo}:")
.contains("overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实")
.contains("客户范围只取认证 Token,不接收 customerId");
}
private static String pathBlock(String yaml, String path) {
@@ -17,7 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class SchemaChineseCommentContractTest {
private static final Set<String> MODULE_BASELINE_ONLY_TABLES = Set.of(
"ll_us_truck_order_operation", "ll_local_looseboard_operation");
"ll_us_truck_order_operation", "ll_local_looseboard_operation",
"ll_receiving_warehouse_operation");
private static final Set<String> MODULE_BASELINE_ONLY_COLUMNS = Set.of(
"ll_order.overseas_warehouse_code", "ll_order.receiving_warehouse_version",
"ll_receiving_container_session.overseas_warehouse_code");
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
private static final Pattern TABLE = Pattern.compile(
@@ -138,6 +142,9 @@ class SchemaChineseCommentContractTest {
continue;
}
for (String column : table.columns()) {
if (MODULE_BASELINE_ONLY_COLUMNS.contains(table.name() + "." + column)) {
continue;
}
boolean repaired = migration.contains("table_name='" + table.name()
+ "' AND column_name='" + column + "'");
boolean addedWithComment = hasCommentedAdd(migration, column)
@@ -23,7 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class ContainerPlanProgressContractTest {
@Test
void responseFieldsAndServerAggregateStayFrozen() throws Exception {
assertEquals(List.of("sourceContainerBookingId", "containerNo", "totalOrderCount", "inboundCodeCount",
assertEquals(List.of("sourceContainerBookingId", "containerNo", "overseasWarehouseId",
"overseasWarehouseCode", "overseasWarehouseName", "assignmentVersion",
"canChangeReceivingWarehouse", "receivingWarehouseLockReason", "totalOrderCount", "inboundCodeCount",
"missingInboundCodeCount", "planCount", "plannedInboundCodeCount", "pendingPlanCount",
"plannedArrivalAt", "allPlansReady", "blockingReason"),
Arrays.stream(ContainerPlanProgressSummary.class.getRecordComponents())
@@ -14,6 +14,8 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -38,6 +40,7 @@ import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.lenient;
@@ -53,6 +56,8 @@ class LastlegOrderServiceTest {
@Mock FbaWarehouseValidator fbaWarehouses;
@Mock InventoryCommandService inventory;
@Mock InboundOrderDetainedOutboxService detainedOutbox;
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
@Mock WarehouseResourceGateway warehouseResourceGateway;
LastlegOrderService service;
@BeforeEach
@@ -71,6 +76,50 @@ class LastlegOrderServiceTest {
"US", "CA"));
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
}
@Test
void changesReceivingWarehouseForWholeContainerBeforePdaSession() {
LastlegOrder first = order(1L, "100", "PENDING_ASSIGNMENT", null);
LastlegOrder second = order(2L, "200", "PENDING_ASSIGNMENT", null);
for (LastlegOrder value : List.of(first, second)) {
value.setReceivingWarehouseVersion(0); value.setOverseasWarehouseId("193000000000200003");
value.setOverseasWarehouseCode("US-LAX-01"); value.setOverseasWarehouseName("洛杉矶仓");
}
var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900",
"US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 0, 0, 2,
null, false, "存在2票尚未生成有效入库计划");
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second));
when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn(
new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE"));
when(orders.updateContainerReceivingWarehouse("BOOKING-1", "900", "US-NYC-01", "纽约仓", 0))
.thenReturn(2);
when(orders.findContainerProgress("BOOKING-1")).thenReturn(updated);
var result = service.changeReceivingWarehouse("BOOKING-1", "change-1",
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, "改派纽约"), "admin");
assertEquals("US-NYC-01", result.overseasWarehouseCode());
verify(orders).updateContainerPlanReceivingWarehouse("BOOKING-1", 900L);
verify(receivingWarehouseOperations).insert(any(com.baiyue.lastleg.entity.ReceivingWarehouseOperation.class));
}
@Test
void permanentlyLocksReceivingWarehouseAfterPdaSessionExists() {
LastlegOrder order = order(1L, "100", "PENDING_ASSIGNMENT", null);
order.setReceivingWarehouseVersion(0); order.setOverseasWarehouseId("193000000000200003");
order.setOverseasWarehouseCode("US-LAX-01"); order.setOverseasWarehouseName("洛杉矶仓");
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(order));
when(orders.countReceivingSessions("BOOKING-1")).thenReturn(1L);
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
service.changeReceivingWarehouse("BOOKING-1", "change-2",
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, null), "admin"));
assertEquals(409, error.getStatusCode().value());
verifyNoInteractions(warehouseResourceGateway);
}
@Test
@@ -283,7 +332,8 @@ class LastlegOrderServiceTest {
}
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", 15, 15, 0,
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003",
"US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0,
15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason);
}
@@ -5,7 +5,7 @@ class LoadingCompletedConsumerTest {
private final Map<String,String> frozenContainerNos=new HashMap<>();
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock LastlegOrderNoDailySequenceMapper sequences; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(3); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&"193000000000200003".equals(o.getOverseasWarehouseId())&&"US-LAX-01".equals(o.getOverseasWarehouseCode())&&"洛杉矶仓".equals(o.getOverseasWarehouseName())&&Integer.valueOf(0).equals(o.getReceivingWarehouseVersion()))); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
@Test void oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); com.fasterxml.jackson.databind.node.ArrayNode rows=json.createArrayNode(); for(int index=1;index<=15;index++){ com.fasterxml.jackson.databind.node.ObjectNode row=node.path("orders").get(0).deepCopy(); row.put("sourceInboundId","in-"+index).put("rootInboundId","root-"+index).put("sourceOrderId","order-"+index).put("sourceOrderNo","FL-"+index).put("rootOrderId","root-order-"+index).put("inboundCode","RK-"+index); rows.add(row); } node.set("orders",rows); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope fifteen=new EventEnvelope("123e4567-e89b-12d3-a456-426614174015",source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(15,service.consume(fifteen)); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(15)).insert(captured.capture()); assertEquals(1,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(captured.getAllValues().get(0).getOrderNo().matches("^FL-WH-\\d{8}-\\d{6}$")); verify(sequences,never()).nextValue(any(java.time.LocalDate.class)); }
@Test void rejectsMissingOwnerSnapshotBeforeAnyLastlegWrite(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node.path("orders").get(0)).put("customerName"," "); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope missing=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(missing)); assertTrue(error.getMessage().contains("尾程事件负载无效")||error.getMessage().contains("货主快照")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
@Test void freezesFormalPlannedWarehouseArrivalFromLoadingEvent(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("estimatedWarehouseArrivalAt","2026-10-03T09:00"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); service.consume(changed); ArgumentCaptor<LastlegOrder> captor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captor.capture()); assertTrue(captor.getAllValues().stream().allMatch(order->java.time.LocalDateTime.of(2026,10,3,9,0).equals(order.getEstimatedWarehouseAt()))); }