2026-08-19 16:24:02 +08:00
package com.baiyue.lastleg.service ;
2026-08-26 13:58:33 +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.entity.InboundPlanCarton ; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper ; 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-31 12:13:20 +08:00
private final Map < String , String > frozenContainerNos = new HashMap <> ();
2026-08-27 15:56:14 +08:00
@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 ;
2026-09-09 21:30:39 +08:00
@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 (), anyLong (), anyLong ())). thenAnswer ( i -> { String key = i . getArgument ( 0 ); String value = i . getArgument ( 2 ); 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 ; }); }
2026-09-07 15:07:34 +08:00
@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}" )); }
2026-09-01 14:47:43 +08:00
@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 )); }
2026-09-01 14:46:36 +08:00
@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 )); }
2026-08-28 13:54:32 +08:00
@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 ()))); }
2026-09-09 21:30:39 +08:00
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion (){ service . consume ( event ()); verify ( shippingGates ). initialize ( "book-1" , "" , "MSCU6639870" , 0L , 1L ); }
2026-08-31 12:13:20 +08:00
@Test void rejectsUnnormalizedActualContainerNumberInsteadOfUsingCabinetNumber (){ EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy (); (( com . fasterxml . jackson . databind . node . ObjectNode ) node ). put ( "containerNo" , " mscu6639870 " ). put ( "cabinetNo" , "FALLBACK-CABINET" ); 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 ); assertThrows ( IllegalArgumentException . class ,() -> service . consume ( changed )); verify ( inbox , never ()). insert ( any ( LastlegEventInbox . class )); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); }
@Test void rejectsBlankActualContainerNumberEvenWhenCabinetNumberExists (){ EventEnvelope source = event (); JsonNode node = source . payload (). deepCopy (); (( com . fasterxml . jackson . databind . node . ObjectNode ) node ). put ( "containerNo" , " " ). put ( "cabinetNo" , "CABINET-ONLY" ); 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 ); assertThrows ( IllegalArgumentException . class ,() -> service . consume ( changed )); verify ( inbox , never ()). insert ( any ( LastlegEventInbox . class )); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); }
@Test void rejectsDifferentContainerNumberForSameSourceBookingBeforeAnyBusinessWrite (){ EventEnvelope source = event (); frozenContainerNos . put ( "book-1" , "CSQU3054383" ); IllegalStateException error = assertThrows ( IllegalStateException . class ,() -> service . consume ( source )); assertTrue ( error . getMessage (). contains ( "sourceContainerBookingId" )); verify ( inbox , never ()). insert ( any ( LastlegEventInbox . class )); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); }
@Test void rejectsExistingOrderContainerDriftForSameSourceBooking (){ when ( orders . lockContainerNumbers ( "book-1" )). thenReturn ( List . of ( "MSCU6639870" , "CSQU3054383" )); assertThrows ( IllegalStateException . class ,() -> service . consume ( event ())); verify ( inbox , never ()). insert ( any ( LastlegEventInbox . class )); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); }
2026-09-09 21:30:39 +08:00
@Test void acceptsSeparateCustomerEventsForTheSameFrozenContainerIdentity (){ EventEnvelope first = event (); assertEquals ( 2 , service . consume ( first )); com . fasterxml . jackson . databind . node . ObjectNode node = first . payload (). deepCopy (); int index = 0 ; for ( JsonNode order : node . path ( "orders" )){ (( com . fasterxml . jackson . databind . node . ObjectNode ) order ). put ( "customerId" , "200" ). put ( "customerName" , "Customer 200" ). put ( "sourceInboundId" , "customer-200-in-" + ( ++ index )); } String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node )); EventEnvelope second = new EventEnvelope ( "123e4567-e89b-12d3-a456-426614174200" , first . eventType (), first . eventVersion (), first . occurredAt (), first . sourceRegion (), first . targetRegion (), "200" , first . aggregateType (), first . aggregateId (), first . aggregateVersion (), first . handoverVersion (), first . traceId (), first . correlationId (), first . causationId (), node , hash ); assertEquals ( 2 , service . consume ( second )); verify ( shippingGates , times ( 2 )). initialize ( eq ( "book-1" ), eq ( "" ), eq ( "MSCU6639870" ), eq ( 0L ), eq ( 1L )); ArgumentCaptor < LastlegOrder > captured = ArgumentCaptor . forClass ( LastlegOrder . class ); verify ( orders , times ( 4 )). insert ( captured . capture ()); assertEquals ( Set . of ( "100" , "200" ), captured . getAllValues (). stream (). map ( LastlegOrder :: getCustomerId ). collect ( java . util . stream . Collectors . toSet ())); assertTrue ( captured . getAllValues (). stream (). allMatch ( order -> "MSCU6639870" . equals ( order . getContainerNo ()))); }
2026-09-01 11:06:26 +08:00
@Test void replayPreservesExistingLmoNumberAndAddsNothing (){ EventEnvelope event = event (); LastlegEventInbox row = new LastlegEventInbox (); row . setPayloadHash ( event . payloadHash ()); when ( inbox . selectById ( event . eventId ())). thenReturn ( row ); when ( orders . countContainer ( "100" , "MSCU6639870" )). thenReturn ( 2L ); assertEquals ( 2 , service . consume ( event )); verify ( orders ). countContainer ( "100" , "MSCU6639870" ); verify ( sequences , never ()). nextValue ( any ( java . time . LocalDate . class )); verify ( orders , never ()). insert ( any ( LastlegOrder . class )); verify ( details , never ()). insert ( any ( LastlegOrderDetail . class )); verify ( histories , never ()). insert ( any ( LastlegOrderStatusHistory . class )); }
2026-08-19 16:24:02 +08:00
@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-09-02 15:04:15 +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" , "Customer 100" , "ONE_CLICK_DROPSHIP" , List . of ( pallet ), "FBA" ); var payload = new FirstLegLoadingPlanCompletedV1 ( "plan-d" , "LP-D" , "book-d" , "MSCU6639870" , "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 ( inventory , never ()). registerInTransit ( anyString (), anyString (), any ()); assertEquals ( "100" , captor . getValue (). customerId ()); assertEquals ( "123e4567-e89b-12d3-a456-426614174010:DROPSHIP:in-d" , captor . getValue (). idempotencyKey ()); 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-09-08 16:16:20 +08:00
@Test void freezesReceivingWarehouseInInitialInTransitInventory (){
service . consume ( event ());
ArgumentCaptor < InventoryCommandRequests . InTransit > captured = ArgumentCaptor . forClass ( InventoryCommandRequests . InTransit . class );
verify ( inventory , times ( 2 )). registerInTransit ( eq ( "100" ), eq ( "SYSTEM" ), captured . capture ());
assertTrue ( captured . getAllValues (). stream (). allMatch ( value -> "193000000000200003" . equals ( value . overseasWarehouseId ()) && "US-LAX-01" . equals ( value . overseasWarehouseCode ()) && "洛杉矶仓" . equals ( value . overseasWarehouseName ())));
}
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-27 15:56:14 +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" , "Customer 100" , "ONE_CLICK_DROPSHIP" , List . of ( pallet ), "FBA" );
2026-08-31 12:13:20 +08:00
EventEnvelope event = eventForPayload ( new FirstLegLoadingPlanCompletedV1 ( "plan-multi" , "LP-MULTI" , "book-multi" , "MSCU6639870" , "CAB-MULTI" , "92" , "Warehouse" , List . of ( order )), 2 );
2026-08-21 14:42:40 +08:00
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-31 12:13:20 +08:00
JsonNode node = v2Payload ( source );
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 ));
2026-09-09 01:29:38 +08:00
ArgumentCaptor < LastlegOrder > orderCaptor = ArgumentCaptor . forClass ( LastlegOrder . class );
verify ( orders , times ( 2 )). insert ( orderCaptor . capture ());
assertTrue ( orderCaptor . getAllValues (). stream (). allMatch ( order -> order . getFbaWarehouseCode () == null ));
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 (){
2026-08-31 12:13:20 +08:00
EventEnvelope source = event (); JsonNode node = v2Payload ( source );
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 (){
2026-08-31 12:13:20 +08:00
EventEnvelope source = event (); JsonNode node = v2Payload ( source );
2026-08-24 17:15:48 +08:00
(( 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 (){
2026-08-31 12:13:20 +08:00
EventEnvelope source = event (); JsonNode node = v2Payload ( source );
2026-08-24 17:15:48 +08:00
(( 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 (){
2026-08-31 12:13:20 +08:00
EventEnvelope source = event (); JsonNode node = v2Payload ( source );
2026-08-24 17:15:48 +08:00
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-09-09 13:20:11 +08:00
}
@Test void projectsOverseasDropshipFrozenCartonsIntoReceivingLabelPool () {
service . consume ( overseasDropshipEvent ( false ));
var captured = ArgumentCaptor . forClass ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton . class );
verify ( cartons , times ( 5 )). insert ( captured . capture ());
assertEquals ( List . of ( "RK1#5-1" , "RK1#5-2" , "RK1#5-3" , "RK1#5-4" , "RK1#5-5" ), captured . getAllValues (). stream (). map ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton :: getCartonMark ). toList ());
for ( var carton : captured . getAllValues ()) {
assertEquals ( "100" , carton . getCustomerId ()); assertEquals ( "d-1" , carton . getSourceInboundDetailId ());
assertEquals ( "SKU-1" , carton . getSkuCode ()); assertEquals ( "UNASSIGNED" , carton . getStatus ());
assertNull ( carton . getPlanId ()); assertNull ( carton . getLineId ());
}
verify ( dropshipOrders ). receive ( any ());
}
@Test void rejectsAmbiguousDropshipSkuSourceBeforeWritingReceivingLabels () {
var failure = assertThrows ( IllegalArgumentException . class ,() -> service . consume ( overseasDropshipEvent ( true )));
assertTrue ( failure . getMessage (). contains ( "来源明细" ));
verify ( cartons , never ()). insert ( any ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton . class ));
verify ( orders , never ()). insert ( any ( LastlegOrder . class ));
}
@Test void mapsSameSkuDifferentSourceDetailsWithoutGuessing () {
EventEnvelope original = overseasDropshipEvent ( false );
var node = ( com . fasterxml . jackson . databind . node . ObjectNode ) original . payload (). deepCopy ();
var order = ( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 );
var second = ( com . fasterxml . jackson . databind . node . ObjectNode ) order . path ( "items" ). get ( 0 ). deepCopy ();
second . put ( "sourceInboundDetailId" , "d-2" );
(( com . fasterxml . jackson . databind . node . ArrayNode ) order . path ( "items" )). add ( second );
order . put ( "packageCount" , 10 );
var skus = ( com . fasterxml . jackson . databind . node . ArrayNode ) order . path ( "pallets" ). get ( 0 ). path ( "skuPackages" );
var secondSku = ( com . fasterxml . jackson . databind . node . ObjectNode ) skus . get ( 0 ). deepCopy ();
secondSku . put ( "sourceInboundDetailId" , "d-2" );
var marks = secondSku . putArray ( "cartonMarks" ); for ( int n = 6 ; n <= 10 ; n ++ ) marks . add ( "RK1#10-" + n );
skus . add ( secondSku );
service . consume ( rehash ( original , node ));
var captured = ArgumentCaptor . forClass ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton . class );
verify ( cartons , times ( 10 )). insert ( captured . capture ());
assertEquals ( 5 , captured . getAllValues (). stream (). filter ( c -> "d-1" . equals ( c . getSourceInboundDetailId ())). count ());
assertEquals ( 5 , captured . getAllValues (). stream (). filter ( c -> "d-2" . equals ( c . getSourceInboundDetailId ())). count ());
assertTrue ( captured . getAllValues (). stream (). filter ( c -> c . getCartonMark (). contains ( "#10-" )). allMatch ( c -> "d-2" . equals ( c . getSourceInboundDetailId ())));
var received = ArgumentCaptor . forClass ( DropshipRequests . Receive . class );
verify ( dropshipOrders ). receive ( received . capture ());
var inventoryRows = received . getValue (). pallets (). get ( 0 ). skuPackages ();
assertEquals ( 1 , inventoryRows . size ()); assertEquals ( 10 , inventoryRows . get ( 0 ). packageCount ());
assertEquals ( 100 , inventoryRows . get ( 0 ). productQuantity ()); assertEquals ( 10 , inventoryRows . get ( 0 ). cartonMarks (). size ());
}
@Test void rejectsMissingOrForeignPalletSourceBeforeAnyOrderWrite () {
for ( String detailId : List . of ( "" , "foreign-detail" )) {
EventEnvelope original = overseasDropshipEvent ( false );
var node = ( com . fasterxml . jackson . databind . node . ObjectNode ) original . payload (). deepCopy ();
(( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 ). path ( "pallets" ). get ( 0 ). path ( "skuPackages" ). get ( 0 )). put ( "sourceInboundDetailId" , detailId );
assertThrows ( IllegalArgumentException . class ,() -> service . consume ( rehash ( original , node )));
}
verify ( orders , never ()). insert ( any ( LastlegOrder . class ));
verify ( cartons , never ()). insert ( any ( com . baiyue . lastleg . inboundplan . entity . InboundPlanCarton . class ));
}
private EventEnvelope rehash ( EventEnvelope original , JsonNode node ) {
return new EventEnvelope ( original . eventId (), original . eventType (), original . eventVersion (), original . occurredAt (), original . sourceRegion (), original . targetRegion (), original . tenantId (), original . aggregateType (), original . aggregateId (), original . aggregateVersion (), original . handoverVersion (), original . traceId (), original . correlationId (), original . causationId (), node , EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node )));
}
private EventEnvelope overseasDropshipEvent ( boolean duplicateSku ) {
EventEnvelope original = event ();
var node = ( com . fasterxml . jackson . databind . node . ObjectNode ) v2Payload ( original );
node . put ( "warehouseId" , "92" );
var sourceOrder = ( com . fasterxml . jackson . databind . node . ObjectNode ) node . path ( "orders" ). get ( 0 );
sourceOrder . put ( "orderType" , "ONE_CLICK_DROPSHIP" ). put ( "destinationType" , "OVERSEAS" );
var sku = json . createObjectNode (). put ( "sourceInboundDetailId" , "d-1" ). put ( "skuCode" , "SKU-1" ). put ( "productName" , "产品" ). put ( "packageCount" , 5 ). put ( "productQuantity" , 50 );
var marks = sku . putArray ( "cartonMarks" ); for ( int n = 1 ; n <= 5 ; n ++ ) marks . add ( "RK1#5-" + n );
var pallet = json . createObjectNode (). put ( "palletNo" , "DP-260909-0143" ); pallet . putArray ( "skuPackages" ). add ( sku );
sourceOrder . putArray ( "pallets" ). add ( pallet );
if ( duplicateSku ) (( com . fasterxml . jackson . databind . node . ArrayNode ) sourceOrder . path ( "items" )). add ( sourceOrder . path ( "items" ). get ( 0 ). deepCopy ());
node . putArray ( "orders" ). add ( sourceOrder );
String hash = EventPayloadHash . sha256 ( EventPayloadCanonicalizer . canonicalize ( node ));
return new EventEnvelope ( original . eventId (), original . eventType (), 2 , original . occurredAt (), original . sourceRegion (), original . targetRegion (), original . tenantId (), original . aggregateType (), original . aggregateId (), original . aggregateVersion (), 2L , original . traceId (), original . correlationId (), original . causationId (), node , hash );
2026-08-24 17:15:48 +08:00
}
2026-08-31 12:13:20 +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" , "MSCU6639870" , "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 ), "100" , "Customer 100" , "FIRST_LEG" , List . of ()), 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 ), "100" , "Customer 100" , "FIRST_LEG" , List . of ()))); 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 ); }
private JsonNode v2Payload ( EventEnvelope source ){ com . fasterxml . jackson . databind . node . ObjectNode node = source . payload (). deepCopy (); node . set ( "sourceContainerBookingId" , node . remove ( "containerBookingId" )); return node ; }
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
}