package com.baiyue.lastleg.service; 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.*; @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class) class LoadingCompletedConsumerTest { /** 两个来源柜复用同一实箱时仍各自取号,不能把历史箱号当本次柜身份。 */ @Test void reusedPhysicalContainerKeepsDistinctSourceBookingNumbers() { when(sequences.next("WC")).thenReturn("WC202610210001","WC202610210002"); EventEnvelope first=event(); service.consume(first); com.fasterxml.jackson.databind.node.ObjectNode payload=first.payload().deepCopy(); payload.put("containerBookingId","book-2"); EventEnvelope second=new EventEnvelope("123e4567-e89b-12d3-a456-426614174099",first.eventType(),first.eventVersion(), first.occurredAt(),first.tenantId(),first.aggregateType(),first.aggregateId(),first.aggregateVersion(), first.handoverVersion(),first.traceId(),first.correlationId(),null,payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))); service.consume(second); ArgumentCaptor captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(4)).insert(captured.capture()); assertEquals(2,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); verify(orders).containerOrderNumbers("book-1"); verify(orders).containerOrderNumbers("book-2"); } /** 同柜后续客户交接沿用首票编号,不再次占用日流水。 */ @Test void reusesTheExistingContainerNumberWithoutAllocatingAgain() { when(orders.containerOrderNumbers(anyString())).thenReturn(List.of("WC202610200001")); service.consume(event()); ArgumentCaptor captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captured.capture()); assertTrue(captured.getAllValues().stream().allMatch(order->"WC202610200001".equals(order.getOrderNo()))); verify(sequences,never()).next(anyString()); } /** 同柜身份不一致必须停止,不能挑选任一旧号继续生成订单。 */ @Test void rejectsConflictingContainerBusinessNumbers() { when(orders.containerOrderNumbers(anyString())).thenReturn(List.of("WC202610200001","WC202610200002")); assertThrows(IllegalStateException.class,()->service.consume(event())); verify(orders,never()).insert(any(LastlegOrder.class)); } private final Map 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 com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper loadingEvidence; @Mock LastlegDocumentNumberGenerator 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,"orderNumbers",new com.baiyue.lastleg.repository.LastlegOrderNumberRepository(orders)); lenient().when(sequences.next("WC")).thenReturn("WC202610210001"); 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,"loadingEvidence",loadingEvidence); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); 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; }); } @Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(event).outcome()); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor 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 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 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("WC[0-9]{8}[0-9]{4}")); } @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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(fifteen).outcome()); ArgumentCaptor 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("^WC\\d{8}\\d{4}$")); verify(sequences).next("WC"); } @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.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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); service.consume(changed); ArgumentCaptor 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()))); } @Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion(){ service.consume(event()); verify(shippingGates).initialize("book-1","","MSCU6639870",0L,1L); } @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.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.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)); } @Test void acceptsSeparateCustomerEventsForTheSameFrozenContainerIdentity(){ EventEnvelope first=event(); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(first).outcome()); 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(),"200",first.aggregateType(),first.aggregateId(),first.aggregateVersion(),first.handoverVersion(),first.traceId(),first.correlationId(),first.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(second).outcome()); verify(shippingGates,times(2)).initialize(eq("book-1"),eq(""),eq("MSCU6639870"),eq(0L),eq(1L)); ArgumentCaptor 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()))); } @Test void replayPreservesExistingLmoNumberAndAddsNothing(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); row.setStatus("PROCESSED"); when(inbox.selectById(event.eventId())).thenReturn(row); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.DUPLICATE, service.consume(event).outcome()); verifyNoInteractions(sequences); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); verify(histories,never()).insert(any(LastlegOrderStatusHistory.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)); } @Test void tamperedPayloadHashIsRejectedBeforeInboxMutation(){ EventEnvelope source=event(); EventEnvelope tampered=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),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()); } @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")); } @Test void rejectsUnsupportedEventType(){ EventEnvelope source=event(); EventEnvelope invalid=new EventEnvelope(source.eventId(),"UnsupportedEvent",source.eventVersion(),source.occurredAt(),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)); } @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); addManifest(node.path("orders").get(0).path("pallets").get(0),2); 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(),"100","LOADING_PLAN","plan-d",2,1L,"123e4567-e89b-12d3-a456-426614174011","FIRSTLEG_LOADING_COMPLETED:plan-d:100:1",null,node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(event).outcome()); ArgumentCaptor 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()); } @Test void freezesReceivingWarehouseInInitialInTransitInventory(){ service.consume(event()); ArgumentCaptor 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()))); } @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",2,10,1,BigDecimal.ZERO,BigDecimal.ZERO,BigDecimal.ZERO,BigDecimal.ZERO,"wh-1","ONT8","ONT8",null,"SHIP-B","MARK-B",null,List.of("BOX-A1","BOX-A2")); 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",2,10,List.of("BOX-A1","BOX-A2")))); var order=new FirstLegLoadingPlanCompletedV1.Order("in-multi","root-multi","ord-multi","FL-MULTI","root-ord-multi","RK-MULTI","wh-1","ONT8","ONT8",2,30,1,BigDecimal.ONE,BigDecimal.ONE,BigDecimal.ONE,List.of(itemA,itemB),"100","Customer 100","ONE_CLICK_DROPSHIP",List.of(pallet),"FBA"); EventEnvelope event=eventForPayload(new FirstLegLoadingPlanCompletedV1("plan-multi","LP-MULTI","book-multi","MSCU6639870","CAB-MULTI","92","Warehouse",List.of(order)),2); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(event).outcome()); verify(orders,times(1)).insert(any(LastlegOrder.class)); ArgumentCaptor detailCaptor=ArgumentCaptor.forClass(LastlegOrderDetail.class); verify(details,times(2)).insert(detailCaptor.capture()); assertEquals(Map.of("SKU-A",2,"SKU-B",2),detailCaptor.getAllValues().stream().collect(java.util.stream.Collectors.toMap(LastlegOrderDetail::getSkuCode,LastlegOrderDetail::getPackageCount))); ArgumentCaptor receiveCaptor=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(receiveCaptor.capture()); assertEquals(2,receiveCaptor.getValue().pallets().get(0).skuPackages().size()); assertEquals(2,receiveCaptor.getValue().pallets().get(0).manifestPackageCount()); assertEquals(Map.of("SKU-A",2L,"SKU-B",2L),receiveCaptor.getValue().pallets().get(0).skuPackages().stream().collect(java.util.stream.Collectors.toMap(DropshipRequests.SkuPackages::skuCode,DropshipRequests.SkuPackages::packageCount))); } @Test void consumesV2LoadingEventAndKeepsDropshipSnapshot(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); ((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"); ((com.fasterxml.jackson.databind.node.ObjectNode) node.path("orders").get(0).path("items").get(0)).put("customerRemark","fragile"); ((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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(), 2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(v2).outcome()); ArgumentCaptor orderCaptor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(orderCaptor.capture()); assertTrue(orderCaptor.getAllValues().stream().allMatch(order->order.getFbaWarehouseCode()==null)); ArgumentCaptor 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()); assertNotNull(cartonCaptor.getAllValues().get(0).getOrderId()); } @Test void rejectsV2EventWithoutSkuCartonMapping(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); ((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"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope v2=new EventEnvelope(source.eventId(),source.eventType(),2,source.occurredAt(),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)); assertTrue(error.getMessage().contains("箱唛数量必须等于实装箱数")); verify(orders,never()).insert(any(LastlegOrder.class)); } @Test void legacyV2FbaWithCompleteFrozenCartonsPassesTheMappingGate(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); ((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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(), 2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(v2).outcome()); verify(cartons,never()).insert(any(InboundPlanCarton.class)); } @Test void acceptsV2FbaByPackageCountWithoutCartonFacts(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); ((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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(), 2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(v2).outcome()); verify(cartons,never()).insert(any(InboundPlanCarton.class)); } @Test void safelyInfersLegacyOverseasDestinationFromReceivingWarehouseIdentity(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); 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.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(v2).outcome()); ArgumentCaptor captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captured.capture()); assertTrue(captured.getAllValues().stream().allMatch(order->"OVERSEAS".equals(order.getDestinationType()))); verify(cartons,times(8)).insert(any(InboundPlanCarton.class)); } @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 preservesNonUniformCartonQuantitiesAcrossTheEventBoundary() { EventEnvelope original=overseasDropshipEvent(false); var node=(com.fasterxml.jackson.databind.node.ObjectNode)original.payload().deepCopy(); var sku=(com.fasterxml.jackson.databind.node.ObjectNode)node.path("orders").get(0).path("pallets").get(0).path("skuPackages").get(0); var quantities=sku.putObject("cartonQuantities"); quantities.put("RK1#5-1",6).put("RK1#5-2",7).put("RK1#5-3",8).put("RK1#5-4",9).put("RK1#5-5",20); service.consume(rehash(original,node)); var captured=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(captured.capture()); assertEquals(Map.of("RK1#5-1",6L,"RK1#5-2",7L,"RK1#5-3",8L,"RK1#5-4",9L,"RK1#5-5",20L), captured.getValue().pallets().get(0).skuPackages().get(0).cartonQuantities()); } @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 consumesOneDropshipSourceDetailSplitAcrossTwoPallets() { 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 secondItem=(com.fasterxml.jackson.databind.node.ObjectNode)order.path("items").get(0).deepCopy(); ((com.fasterxml.jackson.databind.node.ArrayNode)order.path("items")).add(secondItem); order.put("packageCount",10).put("productQuantity",100).put("palletCount",2); var secondPallet=(com.fasterxml.jackson.databind.node.ObjectNode)order.path("pallets").get(0).deepCopy(); secondPallet.put("palletNo","DP-260914-0022"); var secondSku=(com.fasterxml.jackson.databind.node.ObjectNode)secondPallet.path("skuPackages").get(0); var marks=secondSku.putArray("cartonMarks"); for(int n=6;n<=10;n++)marks.add("RK1#10-"+n); ((com.fasterxml.jackson.databind.node.ArrayNode)order.path("pallets")).add(secondPallet); addManifest(secondPallet, 5); service.consume(rehash(original,node)); var detailCaptor=ArgumentCaptor.forClass(LastlegOrderDetail.class); verify(details).insert(detailCaptor.capture()); assertEquals("d-1",detailCaptor.getValue().getSourceInboundDetailId()); assertEquals(10,detailCaptor.getValue().getPackageCount()); assertEquals(100,detailCaptor.getValue().getProductQuantity()); verify(cartons,times(10)).insert(any(com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton.class)); var received=ArgumentCaptor.forClass(DropshipRequests.Receive.class); verify(dropshipOrders).receive(received.capture()); assertEquals(List.of("DP-260909-0143","DP-260914-0022"), received.getValue().pallets().stream().map(DropshipRequests.Pallet::palletNo).toList()); } @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); addManifest(order.path("pallets").get(0),10); 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.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); addManifest(pallet,5); 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.tenantId(),original.aggregateType(),original.aggregateId(),original.aggregateVersion(),2L,original.traceId(),original.correlationId(),original.causationId(),node,hash); } 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(),"100","LOADING_PLAN","plan-1",2,1L,"123e4567-e89b-12d3-a456-426614174001","FIRSTLEG_LOADING_COMPLETED:plan-1:100:1",null,node,hash); } @Test void projectsFiveDomesticLoadingPhotosOncePerPhysicalContainer(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=(com.fasterxml.jackson.databind.node.ObjectNode)v2Payload(source); var values=node.putArray("loadingEvidence"); int fileId=10; for(String type:List.of("PHOTO_STEP_1","PHOTO_STEP_2","PHOTO_STEP_3","PHOTO_STEP_4","SEAL")) values.addObject().put("evidenceType",type).put("fileId",fileId++).put("fileName",type+".jpg"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope event=new EventEnvelope(source.eventId(),source.eventType(),3,source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),3L,source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(event).outcome()); ArgumentCaptor captured=ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence.class); verify(loadingEvidence,times(5)).insert(captured.capture()); assertEquals(Set.of("PHOTO_STEP_1","PHOTO_STEP_2","PHOTO_STEP_3","PHOTO_STEP_4","SEAL"),captured.getAllValues().stream().map(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence::getEvidenceType).collect(java.util.stream.Collectors.toSet())); assertTrue(captured.getAllValues().stream().allMatch(value->"book-1".equals(value.getSourceContainerBookingId()))); } @Test void rejectsV3EventWithoutTheFiveDomesticLoadingPhotos(){ EventEnvelope source=event(); JsonNode node=v2Payload(source); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope event=new EventEnvelope(source.eventId(),source.eventType(),3,source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),3L,source.traceId(),source.correlationId(),source.causationId(),node,hash); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(event)); assertTrue(error.getMessage().contains("五类国内装柜照片")); verify(loadingEvidence,never()).insert(any(com.baiyue.lastleg.inboundplan.pda.entity.ContainerLoadingEvidence.class)); } private JsonNode v2Payload(EventEnvelope source){ com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); node.set("sourceContainerBookingId",node.remove("containerBookingId")); return node; } private EventEnvelope eventForPayload(FirstLegLoadingPlanCompletedV1 payload, long version){ JsonNode node=json.valueToTree(payload); for(JsonNode order:node.path("orders")) for(JsonNode pallet:order.path("pallets")) addManifest(pallet,order.path("packageCount").asInt()); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); return new EventEnvelope("123e4567-e89b-12d3-a456-426614174099",EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,1,Instant.now(),"100","LOADING_PLAN",payload.loadingPlanId(),2,version,"123e4567-e89b-12d3-a456-426614174098","FIRSTLEG_LOADING_COMPLETED:"+payload.loadingPlanId()+":100:1",null,node,hash); } private void addManifest(JsonNode pallet,int packageCount){ var object=(com.fasterxml.jackson.databind.node.ObjectNode)pallet; object.put("manifestVersion",1).put("manifestPackageCount",packageCount).put("manifestFragmentCount",1).put("manifestHash","a".repeat(64)); // These fixtures explicitly represent uniform cartons; production must never infer this ratio. for(JsonNode sku:object.path("skuPackages")) { int count=sku.path("packageCount").asInt(), quantity=sku.path("productQuantity").asInt(); assertTrue(count>0 && quantity%count==0); var quantities=((com.fasterxml.jackson.databind.node.ObjectNode)sku).putObject("cartonQuantities"); for(JsonNode mark:sku.path("cartonMarks")) quantities.put(mark.asText(),quantity/count); } } }