2026-08-19 16:24:02 +08:00
package com.baiyue.lastleg.service ;
2026-08-24 17:43:03 +08:00
import com.baiyue.common.event.* ; import com.baiyue.lastleg.entity.* ; import com.baiyue.lastleg.mapper.* ; import com.baiyue.lastleg.dropship.dto.DropshipRequests ; import com.baiyue.lastleg.dropship.service.DropshipOrderService ; import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse ; import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton ; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper ; import com.baiyue.lastleg.inboundplan.service.InboundPlanService ; import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests ; import com.baiyue.lastleg.inventory.command.service.InventoryCommandService ; import com.fasterxml.jackson.databind.* ; import org.junit.jupiter.api.* ; import org.junit.jupiter.api.extension.ExtendWith ; import org.mockito.* ; import org.springframework.test.util.ReflectionTestUtils ; import java.math.BigDecimal ; import java.time.Instant ; import java.util.* ; import static org.junit.jupiter.api.Assertions.* ; import static org.mockito.ArgumentMatchers.* ; import static org.mockito.Mockito.* ;
2026-08-19 16:24:02 +08:00
@ExtendWith ( org . mockito . junit . jupiter . MockitoExtension . class )
class LoadingCompletedConsumerTest {
2026-08-25 10:31:04 +08:00
@Mock LastlegEventInboxMapper inbox ; @Mock LastlegOrderMapper orders ; @Mock LastlegInboundOrderMapper inboundOrders ; @Mock LastlegOrderDetailMapper details ; @Mock LastlegOrderStatusHistoryMapper histories ; @Mock InboundPlanCartonMapper cartons ; @Mock DropshipOrderService dropshipOrders ; @Mock InboundPlanService inboundPlans ; @Mock InventoryCommandService inventory ; @Mock com . baiyue . lastleg . shipping . mapper . ContainerShippingGateMapper shippingGates ; 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 , "inboundPlans" , inboundPlans ); ReflectionTestUtils . setField ( service , "inventory" , inventory ); ReflectionTestUtils . setField ( service , "shippingGates" , shippingGates ); ReflectionTestUtils . setField ( service , "objectMapper" , json ); ReflectionTestUtils . setField ( service , "orderNoGenerator" , new LastlegOrderNoGenerator ()); 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 ( inboundPlans . convert ( anyString (), anyLong ())). thenReturn ( new InboundPlanResponse ( 1L , "LIP-1" , "100" , 1L , "MSCU1" , "PENDING_ASSIGNMENT" , 1 , 1 , BigDecimal . ONE , List . of (), List . of (), List . of ())); }
2026-08-24 21:46:19 +08:00
@Test void createsOneOrderAndOneInboundPlanPerInbound (){ EventEnvelope event = event (); assertEquals ( 2 , service . consume ( event )); verify ( orders , times ( 2 )). insert ( any ( LastlegOrder . class )); verify ( inboundPlans , times ( 2 )). convert ( eq ( "100" ), anyLong ()); verify ( inventory , times ( 2 )). registerInTransit ( eq ( "100" ), eq ( "SYSTEM" ), any ( InventoryCommandRequests . InTransit . class )); 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 . getId () != null )); assertTrue ( c . getAllValues (). stream (). allMatch ( o -> "MSCU1" . equals ( o . getContainerNo ()))); assertTrue ( c . getAllValues (). stream (). allMatch ( o -> "FBA" . equals ( o . getDestinationType ()))); 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 ()); assertEquals ( new LastlegOrderNoGenerator (). forContainerNo ( "MSCU1" ), c . getAllValues (). get ( 0 ). getOrderNo ()); }
2026-08-25 13:23:05 +08:00
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion (){ service . consume ( event ()); verify ( shippingGates ). initialize ( "book-1" , "100" , "" , "MSCU1" , 0L , 1L ); }
2026-08-19 16:24:02 +08:00
@Test void repeatedEventReturnsExistingEffect (){ EventEnvelope event = event (); LastlegEventInbox row = new LastlegEventInbox (); row . setPayloadHash ( event . payloadHash ()); when ( inbox . selectById ( event . eventId ())). thenReturn ( row ); when ( orders . countContainer ( "100" , "MSCU1" )). thenReturn ( 2L ); assertEquals ( 2 , service . consume ( event )); verify ( orders ). countContainer ( "100" , "MSCU1" ); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); verify ( details , never ()). insert ( any ( LastlegOrderDetail . class )); }
@Test void sameEventWithDifferentHashIsRejected (){ EventEnvelope event = event (); LastlegEventInbox row = new LastlegEventInbox (); row . setPayloadHash ( "sha256:" + "0" . repeat ( 64 )); when ( inbox . selectById ( event . eventId ())). thenReturn ( row ); assertThrows ( IllegalArgumentException . class ,() -> service . consume ( event )); }
2026-08-25 14:46:06 +08:00
@Test void tamperedPayloadHashIsRejectedBeforeInboxMutation (){ EventEnvelope source = event (); EventEnvelope tampered = 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 (), source . payload (), "sha256:" + "0" . repeat ( 64 )); IllegalArgumentException error = assertThrows ( IllegalArgumentException . class ,() -> service . consume ( tampered )); assertTrue ( error . getMessage (). contains ( "payloadHash" )); verify ( inbox , never ()). insert ( any ( LastlegEventInbox . class )); }
@Test void concurrentDuplicateKeyWithDifferentHashIsRejected (){ EventEnvelope event = event (); LastlegEventInbox raced = new LastlegEventInbox (); raced . setPayloadHash ( "sha256:" + "0" . repeat ( 64 )); when ( inbox . selectById ( event . eventId ())). thenReturn ( null , raced ); doThrow ( new org . springframework . dao . DuplicateKeyException ( "race" )). when ( inbox ). insert ( any ( LastlegEventInbox . class )); IllegalArgumentException error = assertThrows ( IllegalArgumentException . class ,() -> service . consume ( event )); assertTrue ( error . getMessage (). contains ( "摘要冲突" )); verify ( orders , never ()). countContainer ( anyString (), anyString ()); }
2026-08-24 17:22:18 +08:00
@Test void inboundSnapshotUsesCustomerIdAndHasNoPhantomTenantColumn () throws Exception {
assertThrows ( NoSuchFieldException . class ,() -> LastlegInboundOrder . class . getDeclaredField ( "tenantId" ));
String schema = java . nio . file . Files . readString ( java . nio . file . Path . of ( "src/main/resources/db/lastleg.sql" ));
String table = schema . substring ( schema . indexOf ( "CREATE TABLE IF NOT EXISTS ll_inbound_order (" ), schema . indexOf ( "CREATE TABLE IF NOT EXISTS ll_inbound_order_item (" ));
assertTrue ( table . contains ( "customer_id" )); assertFalse ( table . contains ( "tenant_id" ));
}
2026-08-19 16:24:02 +08:00
@Test void rejectsEventFromWrongRegion (){ EventEnvelope source = event (); EventEnvelope invalid = new EventEnvelope ( source . eventId (), source . eventType (), source . eventVersion (), source . occurredAt (), RegionCode . OVERSEAS , RegionCode . CN , source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (), source . handoverVersion (), source . traceId (), source . correlationId (), source . causationId (), source . payload (), source . payloadHash ()); assertThrows ( IllegalArgumentException . class ,() -> service . consume ( invalid )); }
2026-08-25 08:40:27 +08:00
@Test void consumesFormalDropshipPalletSnapshots (){ var sku = new FirstLegLoadingPlanCompletedV1 . SkuPackageSnapshot ( "SKU-A" , "Product A" , 2 , 20 , List . of ( "BOX-1" , "BOX-2" )); var pallet = new FirstLegLoadingPlanCompletedV1 . PalletSnapshot ( "PLT-1" , List . of ( sku )); var item = new FirstLegLoadingPlanCompletedV1 . Item ( "d-1" , "SKU-A" , "Product A" , "Product A" , 2 , 20 , 1 , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , "wh-1" , "ONT8" , "ONT8" , null , null , "MARK" , null , List . of ()); var order = new FirstLegLoadingPlanCompletedV1 . Order ( "in-d" , "root-d" , "ord-d" , "FL-D" , "root-od" , "RK-D" , "wh-1" , "ONT8" , "ONT8" , 2 , 20 , 1 , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , List . of ( item ), "100" , "ONE_CLICK_DROPSHIP" , List . of ( pallet ), "FBA" ); var payload = new FirstLegLoadingPlanCompletedV1 ( "plan-d" , "LP-D" , "book-d" , "CONT-D" , "CAB-D" , "92" , "Warehouse" , List . of ( order )); JsonNode node = json . valueToTree ( payload ); String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node )); var event = new EventEnvelope ( "123e4567-e89b-12d3-a456-426614174010" , EventTypes . FIRST_LEG_LOADING_PLAN_COMPLETED , 1 , Instant . now (), RegionCode . CN , RegionCode . OVERSEAS , "100" , "LOADING_PLAN" , "plan-d" , 2 , 1L , "123e4567-e89b-12d3-a456-426614174011" , "FIRSTLEG_LOADING_COMPLETED:plan-d:100:1" , null , node , hash ); assertEquals ( 1 , service . consume ( event )); ArgumentCaptor < DropshipRequests . Receive > captor = ArgumentCaptor . forClass ( DropshipRequests . Receive . class ); verify ( dropshipOrders ). receive ( captor . capture ()); verify ( inboundPlans ). convert ( eq ( "100" ), anyLong ()); verify ( inventory , never ()). registerInTransit ( anyString (), anyString (), any ()); assertEquals ( "100" , captor . getValue (). customerId ()); assertEquals ( 1 , captor . getValue (). pallets (). size ()); assertEquals ( "PLT-1" , captor . getValue (). pallets (). get ( 0 ). palletNo ()); assertEquals ( List . of ( "BOX-1" , "BOX-2" ), captor . getValue (). pallets (). get ( 0 ). skuPackages (). get ( 0 ). cartonMarks ()); }
2026-08-21 14:42:40 +08:00
@Test void oneDropshipInboundWithMultipleSkusCreatesOneOrderAndMultipleSkuDetails (){
var itemA = new FirstLegLoadingPlanCompletedV1 . Item ( "d-a" , "SKU-A" , "产品A" , "Product A" , 2 , 20 , 1 , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , "wh-1" , "ONT8" , "ONT8" , null , "SHIP-A" , "MARK-A" , null , List . of ( "BOX-A1" , "BOX-A2" ));
var itemB = new FirstLegLoadingPlanCompletedV1 . Item ( "d-b" , "SKU-B" , "产品B" , "Product B" , 3 , 15 , 1 , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , BigDecimal . ONE , "wh-1" , "ONT8" , "ONT8" , null , "SHIP-B" , "MARK-B" , null , List . of ( "BOX-B1" , "BOX-B2" , "BOX-B3" ));
var pallet = new FirstLegLoadingPlanCompletedV1 . PalletSnapshot ( "PLT-1" , List . of (
new FirstLegLoadingPlanCompletedV1 . SkuPackageSnapshot ( "SKU-A" , "产品A" , 2 , 20 , List . of ( "BOX-A1" , "BOX-A2" )),
new FirstLegLoadingPlanCompletedV1 . SkuPackageSnapshot ( "SKU-B" , "产品B" , 3 , 15 , List . of ( "BOX-B1" , "BOX-B2" , "BOX-B3" ))));
2026-08-24 17:15:48 +08:00
var order = new FirstLegLoadingPlanCompletedV1 . Order ( "in-multi" , "root-multi" , "ord-multi" , "FL-MULTI" , "root-ord-multi" , "RK-MULTI" , "wh-1" , "ONT8" , "ONT8" , 5 , 35 , 1 , BigDecimal . valueOf ( 2 ), BigDecimal . valueOf ( 2 ), BigDecimal . valueOf ( 2 ), List . of ( itemA , itemB ), "100" , "ONE_CLICK_DROPSHIP" , List . of ( pallet ), "FBA" );
2026-08-21 14:42:40 +08:00
EventEnvelope event = eventForPayload ( new FirstLegLoadingPlanCompletedV1 ( "plan-multi" , "LP-MULTI" , "book-multi" , "CONT-MULTI" , "CAB-MULTI" , "92" , "Warehouse" , List . of ( order )), 2 );
assertEquals ( 1 , service . consume ( event ));
verify ( orders , times ( 1 )). insert ( any ( LastlegOrder . class ));
ArgumentCaptor < LastlegOrderDetail > detailCaptor = ArgumentCaptor . forClass ( LastlegOrderDetail . class );
verify ( details , times ( 2 )). insert ( detailCaptor . capture ());
assertEquals ( Map . of ( "SKU-A" , 2 , "SKU-B" , 3 ), detailCaptor . getAllValues (). stream (). collect ( java . util . stream . Collectors . toMap ( LastlegOrderDetail :: getSkuCode , LastlegOrderDetail :: getPackageCount )));
ArgumentCaptor < DropshipRequests . Receive > receiveCaptor = ArgumentCaptor . forClass ( DropshipRequests . Receive . class );
verify ( dropshipOrders ). receive ( receiveCaptor . capture ());
assertEquals ( 2 , receiveCaptor . getValue (). pallets (). get ( 0 ). skuPackages (). size ());
assertEquals ( Map . of ( "SKU-A" , 2L , "SKU-B" , 3L ), receiveCaptor . getValue (). pallets (). get ( 0 ). skuPackages (). stream (). collect ( java . util . stream . Collectors . toMap ( DropshipRequests . SkuPackages :: skuCode , DropshipRequests . SkuPackages :: packageCount )));
}
2026-08-20 13:34:34 +08:00
@Test void consumesV2LoadingEventAndKeepsDropshipSnapshot (){
2026-08-20 16:45:47 +08:00
EventEnvelope source = event ();
2026-08-20 13:34:34 +08:00
JsonNode node = source . payload (). deepCopy ();
2026-08-24 17:15:48 +08:00
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 )). put ( "destinationType" , "OVERSEAS" );
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 )). put ( "destinationType" , "OVERSEAS" );
2026-08-20 13:34:34 +08:00
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 ). path ( "items" ). get ( 0 )). put ( "customerRemark" , "fragile" );
2026-08-20 16:45:47 +08:00
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 ). path ( "items" ). get ( 0 ))
. set ( "cartonMarks" , json . valueToTree ( List . of ( "BOX-1" , "BOX-2" , "BOX-3" , "BOX-4" , "BOX-5" )));
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 ). path ( "items" ). get ( 0 ))
. set ( "cartonMarks" , json . valueToTree ( List . of ( "BOX-6" , "BOX-7" , "BOX-8" )));
2026-08-20 13:34:34 +08:00
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
EventEnvelope v2 = new EventEnvelope ( source . eventId (), source . eventType (), 2 , source . occurredAt (), source . sourceRegion (),
source . targetRegion (), source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (),
2L , source . traceId (), source . correlationId (), source . causationId (), node , hash );
assertEquals ( 2 , service . consume ( v2 ));
verify ( orders , times ( 2 )). insert ( any ( LastlegOrder . class ));
2026-08-20 16:45:47 +08:00
ArgumentCaptor < com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton > cartonCaptor =
ArgumentCaptor . forClass ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton . class );
verify ( cartons , times ( 8 )). insert ( cartonCaptor . capture ());
assertEquals ( "SKU-1" , cartonCaptor . getAllValues (). get ( 0 ). getSkuCode ());
assertEquals ( "BOX-1" , cartonCaptor . getAllValues (). get ( 0 ). getCartonMark ());
2026-08-22 10:16:45 +08:00
assertNotNull ( cartonCaptor . getAllValues (). get ( 0 ). getOrderId ());
2026-08-20 16:45:47 +08:00
}
@Test void rejectsV2EventWithoutSkuCartonMapping (){
EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy ();
2026-08-24 17:15:48 +08:00
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 )). put ( "destinationType" , "OVERSEAS" );
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 )). put ( "destinationType" , "OVERSEAS" );
2026-08-20 16:45:47 +08:00
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
EventEnvelope v2 = new EventEnvelope ( source . eventId (), source . eventType (), 2 , source . occurredAt (), source . sourceRegion (),
source . targetRegion (), source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (),
2L , source . traceId (), source . correlationId (), source . causationId (), node , hash );
IllegalArgumentException error = assertThrows ( IllegalArgumentException . class ,() -> service . consume ( v2 ));
2026-08-22 10:16:45 +08:00
assertTrue ( error . getMessage (). contains ( "箱唛数量必须等于实装箱数" ));
2026-08-20 16:45:47 +08:00
verify ( orders , never ()). insert ( any ( LastlegOrder . class ));
2026-08-20 13:34:34 +08:00
}
2026-08-24 17:15:48 +08:00
@Test void legacyV2FbaWithCompleteFrozenCartonsPassesTheMappingGate (){
EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy ();
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 ). path ( "items" ). get ( 0 ))
. set ( "cartonMarks" , json . valueToTree ( List . of ( "ABE3-BOX-1" , "ABE3-BOX-2" , "ABE3-BOX-3" , "ABE3-BOX-4" , "ABE3-BOX-5" )));
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 ). path ( "items" ). get ( 0 ))
. set ( "cartonMarks" , json . valueToTree ( List . of ( "LAX9-BOX-1" , "LAX9-BOX-2" , "LAX9-BOX-3" )));
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
EventEnvelope v2 = new EventEnvelope ( source . eventId (), source . eventType (), 2 , source . occurredAt (), source . sourceRegion (),
source . targetRegion (), source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (),
2L , source . traceId (), source . correlationId (), source . causationId (), node , hash );
assertEquals ( 2 , service . consume ( v2 ));
2026-08-24 17:43:03 +08:00
verify ( cartons , never ()). insert ( any ( InboundPlanCarton . class ));
2026-08-24 17:15:48 +08:00
}
@Test void acceptsV2FbaByPackageCountWithoutCartonFacts (){
EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy ();
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 )). put ( "destinationType" , "FBA" );
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 )). put ( "destinationType" , "FBA" );
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
EventEnvelope v2 = new EventEnvelope ( source . eventId (), source . eventType (), 2 , source . occurredAt (), source . sourceRegion (),
source . targetRegion (), source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (),
2L , source . traceId (), source . correlationId (), source . causationId (), node , hash );
assertEquals ( 2 , service . consume ( v2 ));
2026-08-24 17:43:03 +08:00
verify ( cartons , never ()). insert ( any ( InboundPlanCarton . class ));
2026-08-24 17:15:48 +08:00
}
@Test void safelyInfersLegacyOverseasDestinationFromReceivingWarehouseIdentity (){
EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy ();
for ( JsonNode order : node . path ( "orders" )){
(( com . fasterxml . jackson . databind . node . ObjectNode ) order ). put ( "destinationWarehouseId" , "ow-1" );
JsonNode item = order . path ( "items" ). get ( 0 );
(( com . fasterxml . jackson . databind . node . ObjectNode ) item ). putNull ( "fbaTrackingNo" ). putNull ( "shipmentNo" ). put ( "destinationWarehouseName" , "洛杉矶海外仓" );
}
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 ). path ( "items" ). get ( 0 )). set ( "cartonMarks" , json . valueToTree ( List . of ( "BOX-1" , "BOX-2" , "BOX-3" , "BOX-4" , "BOX-5" )));
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 1 ). path ( "items" ). get ( 0 )). set ( "cartonMarks" , json . valueToTree ( List . of ( "BOX-6" , "BOX-7" , "BOX-8" )));
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
EventEnvelope v2 = new EventEnvelope ( source . eventId (), source . eventType (), 2 , source . occurredAt (), source . sourceRegion (), source . targetRegion (), source . tenantId (), source . aggregateType (), source . aggregateId (), source . aggregateVersion (), 2L , source . traceId (), source . correlationId (), source . causationId (), node , hash );
assertEquals ( 2 , service . consume ( v2 ));
ArgumentCaptor < LastlegOrder > captured = ArgumentCaptor . forClass ( LastlegOrder . class ); verify ( orders , times ( 2 )). insert ( captured . capture ());
assertTrue ( captured . getAllValues (). stream (). allMatch ( order -> "OVERSEAS" . equals ( order . getDestinationType ())));
2026-08-24 17:43:03 +08:00
verify ( cartons , times ( 8 )). insert ( any ( InboundPlanCarton . class ));
2026-08-24 17:15:48 +08:00
}
2026-08-19 16:24:02 +08:00
private EventEnvelope event (){ var item1 = new FirstLegLoadingPlanCompletedV1 . Item ( "d-1" , "SKU-1" , "产品" , "Product" , 5 , 50 , 1 , new BigDecimal ( "10" ), new BigDecimal ( "9" ), new BigDecimal ( "12" ), new BigDecimal ( "1.2" ), "wh-1" , "ONT8" , "FBA ONT8" , "FBA-1" , "SHIP-1" , "MARK-1" ); var item2 = new FirstLegLoadingPlanCompletedV1 . Item ( "d-2" , "SKU-2" , "产品2" , "Product2" , 3 , 30 , 2 , new BigDecimal ( "8" ), new BigDecimal ( "7" ), new BigDecimal ( "9" ), new BigDecimal ( "0.8" ), "wh-2" , "LAX9" , "FBA LAX9" , "FBA-2" , "SHIP-2" , "MARK-2" ); var p = new FirstLegLoadingPlanCompletedV1 ( "plan-1" , "LP1" , "book-1" , "MSCU1" , "CAB1" , "ow-1" , "洛杉矶海外仓" , List . of ( new FirstLegLoadingPlanCompletedV1 . Order ( "in-1" , "root-1" , "ord-1" , "FL1" , "root-o1" , "RK1" , "wh-1" , "ONT8" , "FBA ONT8" , 5 , 50 , 1 , new BigDecimal ( "10" ), new BigDecimal ( "9" ), new BigDecimal ( "1.2" ), List . of ( item1 )), new FirstLegLoadingPlanCompletedV1 . Order ( "in-2" , "root-2" , "ord-2" , "FL2" , "root-o2" , "RK2" , "wh-2" , "LAX9" , "FBA LAX9" , 3 , 30 , 2 , new BigDecimal ( "8" ), new BigDecimal ( "7" ), new BigDecimal ( "0.8" ), List . of ( item2 )))); JsonNode node = json . valueToTree ( p ); String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node )); return new EventEnvelope ( "123e4567-e89b-12d3-a456-426614174000" , EventTypes . FIRST_LEG_LOADING_PLAN_COMPLETED , 1 , Instant . now (), RegionCode . CN , RegionCode . OVERSEAS , "100" , "LOADING_PLAN" , "plan-1" , 2 , 1L , "123e4567-e89b-12d3-a456-426614174001" , "FIRSTLEG_LOADING_COMPLETED:plan-1:100:1" , null , node , hash ); }
2026-08-21 14:42:40 +08:00
private EventEnvelope eventForPayload ( FirstLegLoadingPlanCompletedV1 payload , long version ){ JsonNode node = json . valueToTree ( payload ); String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node )); return new EventEnvelope ( "123e4567-e89b-12d3-a456-426614174099" , EventTypes . FIRST_LEG_LOADING_PLAN_COMPLETED , 1 , Instant . now (), RegionCode . CN , RegionCode . OVERSEAS , "100" , "LOADING_PLAN" , payload . loadingPlanId (), 2 , version , "123e4567-e89b-12d3-a456-426614174098" , "FIRSTLEG_LOADING_COMPLETED:" + payload . loadingPlanId () + ":100:1" , null , node , hash ); }
2026-08-19 16:24:02 +08:00
}