桥接事件

This commit is contained in:
2026-08-20 16:45:47 +08:00
parent 1f0c0f7751
commit c9a05b108b
6 changed files with 100 additions and 6 deletions
@@ -46,7 +46,9 @@ public record FirstLegLoadingPlanCompletedV2(
String destinationWarehouseName, String fbaTrackingNo, String destinationWarehouseName, String fbaTrackingNo,
String shipmentNo, String shippingMark, String shipmentNo, String shippingMark,
String customerRemark, String internalRemark, String warehouseRemark, String customerRemark, String internalRemark, String warehouseRemark,
String palletNo, List<String> cartonMarks) { String palletNo,
/** 头程箱唛二维码中的箱号;这些箱号均对应本明细的 skuCode。 */
List<String> cartonMarks) {
public Item { public Item {
cartonMarks = cartonMarks == null ? List.of() : cartonMarks; cartonMarks = cartonMarks == null ? List.of() : cartonMarks;
} }
@@ -72,7 +74,7 @@ public record FirstLegLoadingPlanCompletedV2(
/** 一键代发入仓单的托盘冻结快照。 */ /** 一键代发入仓单的托盘冻结快照。 */
public record PalletSnapshot(String palletNo, List<SkuPackageSnapshot> skuPackages) {} public record PalletSnapshot(String palletNo, List<SkuPackageSnapshot> skuPackages) {}
/** 托盘下 SKU、箱数、数量及箱唛冻结快照。 */ /** 托盘下 SKU、箱数、数量及箱号冻结快照;cartonMarks 中每个箱号都对应 skuCode。 */
public record SkuPackageSnapshot(String skuCode, String productName, public record SkuPackageSnapshot(String skuCode, String productName,
int packageCount, int productQuantity, int packageCount, int productQuantity,
List<String> cartonMarks) {} List<String> cartonMarks) {}
@@ -39,4 +39,19 @@ class FirstLegLoadingPlanCompletedV2Test {
assertThat(json.at("/orders/0/orderType").asText()).isEqualTo("ONE_CLICK_DROPSHIP"); assertThat(json.at("/orders/0/orderType").asText()).isEqualTo("ONE_CLICK_DROPSHIP");
assertThat(json.at("/orders/0/pallets/0/skuPackages/0/cartonMarks")).hasSize(2); assertThat(json.at("/orders/0/pallets/0/skuPackages/0/cartonMarks")).hasSize(2);
} }
@Test
void mapsEachOrdinaryCartonNumberToItsSku() {
var item = new FirstLegLoadingPlanCompletedV2.Item("1", "SKU-PSC2", "产品", "Product", 2,
20, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, null, List.of("HDHK260711126-01", "HDHK260711126-02"));
var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK",
null, null, null, 2, 20, null, null, null, null, List.of(item));
var json = new ObjectMapper().valueToTree(new FirstLegLoadingPlanCompletedV2(
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)));
assertThat(json.at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU-PSC2");
assertThat(json.at("/orders/0/items/0/cartonMarks")).hasSize(2);
}
} }
@@ -109,9 +109,19 @@ public class LoadingCompletedOutboxService {
Map<Long, List<String>> result = new java.util.LinkedHashMap<>(); Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
for (Row row : rows) { for (Row row : rows) {
if (dropship(row.getBusinessType())) continue; if (dropship(row.getBusinessType())) continue;
if (row.getSkuCode() == null || row.getSkuCode().isBlank()) {
throw new BizException(ResultCode.CONFLICT, "装柜明细缺少 SKU,不能建立箱唛与 SKU 的对应关系");
}
List<String> labels = Objects.requireNonNullElse(cartonManifestMapper.labelPool(row.getAccountId(), List<String> labels = Objects.requireNonNullElse(cartonManifestMapper.labelPool(row.getAccountId(),
row.getSourceInboundId(), row.getSourceInboundDetailId()), List.of()); row.getSourceInboundId(), row.getSourceInboundDetailId()), List.of());
if (labels.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜明细没有已打印的可用箱唛池,不能生成海外交接事件"); if (labels.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜明细没有已打印的可用箱唛池,不能生成海外交接事件");
if (labels.stream().anyMatch(label -> label == null || label.isBlank())
|| labels.stream().distinct().count() != labels.size()) {
throw new BizException(ResultCode.CONFLICT, "装柜明细箱唛池包含空值或重复箱号");
}
if (labels.size() < Objects.requireNonNullElse(row.getPackageCount(), 0)) {
throw new BizException(ResultCode.CONFLICT, "装柜明细箱唛数量少于实装箱数,不能生成海外交接事件");
}
result.put(row.getLoadingPlanItemId(), labels); result.put(row.getLoadingPlanItemId(), labels);
} }
return result; return result;
@@ -19,6 +19,7 @@ import java.math.BigDecimal;
import java.util.List; import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@@ -52,10 +53,21 @@ class LoadingCompletedOutboxServiceTest {
assertThat(first.payload().get("orders")).hasSize(2); assertThat(first.payload().get("orders")).hasSize(2);
assertThat(first.payload().at("/orders/0/sourceInboundId").asText()).isEqualTo("100"); assertThat(first.payload().at("/orders/0/sourceInboundId").asText()).isEqualTo("100");
assertThat(first.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101"); assertThat(first.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101");
assertThat(first.payload().at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU101");
assertThat(first.payload().at("/orders/0/items/0/cartonMarks/0").asText()).isEqualTo("BOX-POOL-1");
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2"); assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
assertThat(first.eventVersion()).isEqualTo(2); assertThat(first.eventVersion()).isEqualTo(2);
} }
@Test void rejectsOrdinaryItemWhenSkuToCartonMappingCannotBeBuilt() {
Row value = row(11, 100, 101);
value.setSkuCode(null);
when(snapshots.rows(90L)).thenReturn(List.of(value));
assertThatThrownBy(() -> service.publish(90L))
.hasMessageContaining("缺少 SKU");
}
@Test void snapshotOnlyPublishesFinalizedLoadedInbounds() throws Exception { @Test void snapshotOnlyPublishesFinalizedLoadedInbounds() throws Exception {
String sql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("rows", long.class) String sql = String.join(" ", LoadingCompletedEventMapper.class.getMethod("rows", long.class)
.getAnnotation(Select.class).value()).replaceAll("\\s+", " "); .getAnnotation(Select.class).value()).replaceAll("\\s+", " ");
@@ -19,7 +19,9 @@ import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Objects; import java.util.Objects;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Set;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@Service @Service
@@ -48,6 +50,7 @@ public class LoadingCompletedConsumer {
if(payload.containerNo()==null||payload.containerNo().isBlank()) if(payload.containerNo()==null||payload.containerNo().isBlank())
throw new IllegalArgumentException("事件没有实际集装箱号 containerNo"); throw new IllegalArgumentException("事件没有实际集装箱号 containerNo");
if(payload.orders()==null||payload.orders().isEmpty()) throw new IllegalArgumentException("事件没有入仓单"); if(payload.orders()==null||payload.orders().isEmpty()) throw new IllegalArgumentException("事件没有入仓单");
if (envelope.eventVersion() == 2) validateCartonSkuMappings(payload);
LastlegEventInbox existing=inboxMapper.selectById(envelope.eventId()); LastlegEventInbox existing=inboxMapper.selectById(envelope.eventId());
if(existing!=null) { if(existing!=null) {
if(!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) throw new IllegalArgumentException("事件摘要冲突"); if(!existing.getPayloadHash().equalsIgnoreCase(envelope.payloadHash())) throw new IllegalArgumentException("事件摘要冲突");
@@ -88,6 +91,39 @@ public class LoadingCompletedConsumer {
return Math.toIntExact(orderMapper.countContainer(envelope.tenantId(),containerNo)); return Math.toIntExact(orderMapper.countContainer(envelope.tenantId(),containerNo));
} }
private void validateCartonSkuMappings(FirstLegLoadingPlanCompletedV1 payload) {
for (FirstLegLoadingPlanCompletedV1.Order order : payload.orders()) {
Set<String> orderCartonMarks = new HashSet<>();
if ("ONE_CLICK_DROPSHIP".equals(order.orderType())) {
for (FirstLegLoadingPlanCompletedV1.PalletSnapshot pallet : order.pallets()) {
for (FirstLegLoadingPlanCompletedV1.SkuPackageSnapshot sku : pallet.skuPackages()) {
requireSkuCartonMapping(sku.skuCode(), sku.packageCount(), sku.cartonMarks(), orderCartonMarks);
}
}
continue;
}
for (FirstLegLoadingPlanCompletedV1.Item item : order.items()) {
requireSkuCartonMapping(item.skuCode(), item.packageCount(), item.cartonMarks(), orderCartonMarks);
}
}
}
private void requireSkuCartonMapping(String skuCode, int loadedPackages, List<String> cartonMarks,
Set<String> orderCartonMarks) {
if (skuCode == null || skuCode.isBlank()) {
throw new IllegalArgumentException("V2 尾程交接事件的箱唛映射缺少 SKU");
}
List<String> marks = cartonMarks == null ? List.of() : cartonMarks;
if (marks.size() < loadedPackages) {
throw new IllegalArgumentException("V2 尾程交接事件的 SKU 箱唛数量少于实装箱数: " + skuCode);
}
for (String mark : marks) {
if (mark == null || mark.isBlank() || !orderCartonMarks.add(mark)) {
throw new IllegalArgumentException("V2 尾程交接事件包含空值或重复箱唛: " + skuCode);
}
}
}
private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload, private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload,
FirstLegLoadingPlanCompletedV1.Order source,String orderNo, JsonNode rawSource) { FirstLegLoadingPlanCompletedV1.Order source,String orderNo, JsonNode rawSource) {
String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(rawSource)); String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(rawSource));
@@ -1,24 +1,43 @@
package com.baiyue.lastleg.service; 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.service.InboundPlanService; 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.*; 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.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.service.InboundPlanService; 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) @ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
class LoadingCompletedConsumerTest { class LoadingCompletedConsumerTest {
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service; @Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InboundPlanService inboundPlans; 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,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inboundPlans",inboundPlans); 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)); } @BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); 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,"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)); }
@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(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()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); } @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(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()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertEquals(new LastlegOrderNoGenerator().forContainerNo("MSCU1"),c.getAllValues().get(0).getOrderNo()); }
@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 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)); } @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 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)); } @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)); }
@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)); 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()); 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()); } @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)); 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()); 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()); }
@Test void consumesV2LoadingEventAndKeepsDropshipSnapshot(){ @Test void consumesV2LoadingEventAndKeepsDropshipSnapshot(){
EventEnvelope source=event(); EventEnvelope source=event();
JsonNode node=source.payload().deepCopy(); JsonNode node=source.payload().deepCopy();
((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)).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)); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
EventEnvelope v2=new EventEnvelope(source.eventId(),source.eventType(),2,source.occurredAt(),source.sourceRegion(), EventEnvelope v2=new EventEnvelope(source.eventId(),source.eventType(),2,source.occurredAt(),source.sourceRegion(),
source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(), source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),
2L,source.traceId(),source.correlationId(),source.causationId(),node,hash); 2L,source.traceId(),source.correlationId(),source.causationId(),node,hash);
assertEquals(2,service.consume(v2)); assertEquals(2,service.consume(v2));
verify(orders,times(2)).insert(any(LastlegOrder.class)); verify(orders,times(2)).insert(any(LastlegOrder.class));
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());
}
@Test void rejectsV2EventWithoutSkuCartonMapping(){
EventEnvelope source=event(); JsonNode node=source.payload().deepCopy();
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));
assertTrue(error.getMessage().contains("箱唛数量少于实装箱数"));
verify(orders,never()).insert(any(LastlegOrder.class));
} }
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); } 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); }
} }