fix: merge multi-pallet dropship handover fragments
This commit is contained in:
+37
-8
@@ -131,16 +131,17 @@ public class LoadingCompletedOutboxService {
|
||||
Map<String, PalletManifest> palletManifests) {
|
||||
List<Row> sorted = values.stream().sorted(Comparator.comparing(Row::getSourceInboundDetailId)).toList();
|
||||
Row first = sorted.get(0);
|
||||
List<FirstLegLoadingPlanCompletedV2.Item> items = sorted.stream().map(row ->
|
||||
new FirstLegLoadingPlanCompletedV2.Item(string(row.getSourceInboundDetailId()), row.getSkuCode(),
|
||||
row.getProductNameZh(), row.getProductNameEn(), row.getPackageCount(), row.getProductQuantity(),
|
||||
row.getPalletCount(), row.getGrossWeightKg(), row.getNetWeightKg(), row.getDimensionalWeightKg(),
|
||||
row.getVolumeCbm(), string(row.getDestinationWarehouseId()), row.getDestinationWarehouseCode(),
|
||||
String fulfillmentType = fulfillmentType(sorted);
|
||||
List<FirstLegLoadingPlanCompletedV2.Item> items = dropship(fulfillmentType)
|
||||
? eventItems(sorted, cartonMarks)
|
||||
: sorted.stream().map(row -> new FirstLegLoadingPlanCompletedV2.Item(
|
||||
string(row.getSourceInboundDetailId()), row.getSkuCode(), row.getProductNameZh(),
|
||||
row.getProductNameEn(), row.getPackageCount(), row.getProductQuantity(), row.getPalletCount(),
|
||||
row.getGrossWeightKg(), row.getNetWeightKg(), row.getDimensionalWeightKg(), row.getVolumeCbm(),
|
||||
string(row.getDestinationWarehouseId()), row.getDestinationWarehouseCode(),
|
||||
row.getDestinationWarehouseName(), row.getFbaTrackingNo(), row.getShipmentNo(), row.getShippingMark(),
|
||||
row.getCustomerRemark(), row.getInternalRemark(), row.getWarehouseRemark(), null,
|
||||
cartonMarks.getOrDefault(row.getLoadingPlanItemId(), List.of())))
|
||||
.toList();
|
||||
String fulfillmentType = fulfillmentType(sorted);
|
||||
cartonMarks.getOrDefault(row.getLoadingPlanItemId(), List.of()))).toList();
|
||||
String orderType = dropship(fulfillmentType) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
|
||||
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(fulfillmentType)
|
||||
? palletSnapshots(sorted, palletRows, palletManifests) : standardLabels.stream()
|
||||
@@ -161,6 +162,34 @@ public class LoadingCompletedOutboxService {
|
||||
destinationType(sorted), first.getLxCode());
|
||||
}
|
||||
|
||||
/** 一件代发装柜按托盘产生分配行,跨区订单明细仍须按来源入仓明细保持唯一。 */
|
||||
private List<FirstLegLoadingPlanCompletedV2.Item> eventItems(List<Row> rows,
|
||||
Map<Long, List<String>> cartonMarks) {
|
||||
Map<Long, List<Row>> byDetail = rows.stream().collect(java.util.stream.Collectors.groupingBy(
|
||||
Row::getSourceInboundDetailId, LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||
List<FirstLegLoadingPlanCompletedV2.Item> result = new ArrayList<>();
|
||||
for (List<Row> fragments : byDetail.values()) {
|
||||
Row first = fragments.get(0);
|
||||
if (fragments.stream().anyMatch(row -> !Objects.equals(first.getSkuCode(), row.getSkuCode())
|
||||
|| !Objects.equals(first.getDestinationWarehouseId(), row.getDestinationWarehouseId())
|
||||
|| !Objects.equals(first.getDestinationWarehouseCode(), row.getDestinationWarehouseCode()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "同一入仓明细的装柜分片快照不一致");
|
||||
}
|
||||
List<String> marks = fragments.stream()
|
||||
.flatMap(row -> cartonMarks.getOrDefault(row.getLoadingPlanItemId(), List.of()).stream()).toList();
|
||||
result.add(new FirstLegLoadingPlanCompletedV2.Item(string(first.getSourceInboundDetailId()),
|
||||
first.getSkuCode(), first.getProductNameZh(), first.getProductNameEn(),
|
||||
sumInt(fragments, Row::getPackageCount), sumNullableInt(fragments, Row::getProductQuantity),
|
||||
sumNullableInt(fragments, Row::getPalletCount), sum(fragments, Row::getGrossWeightKg),
|
||||
sum(fragments, Row::getNetWeightKg), sum(fragments, Row::getDimensionalWeightKg),
|
||||
sum(fragments, Row::getVolumeCbm), string(first.getDestinationWarehouseId()),
|
||||
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), first.getFbaTrackingNo(),
|
||||
first.getShipmentNo(), first.getShippingMark(), first.getCustomerRemark(), first.getInternalRemark(),
|
||||
first.getWarehouseRemark(), null, marks));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/** 只按 Warehouse 已分配的物理 palletId 计数;无分配事实时保持 null。 */
|
||||
private static Integer actualPalletCount(List<PalletRow> palletRows) {
|
||||
Set<Long> palletIds = palletRows.stream().map(PalletRow::getPalletId)
|
||||
|
||||
+26
@@ -376,6 +376,32 @@ class LoadingCompletedOutboxServiceTest {
|
||||
.doesNotStartWith("sha256:");
|
||||
}
|
||||
|
||||
@Test void mergesOneDropshipSourceDetailSplitAcrossTwoPalletsBeforePublishing() throws Exception {
|
||||
Row first = row(11, 100, 101); first.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
Row second = row(11, 100, 101); second.setFulfillmentType("ONE_CLICK_DROPSHIP");
|
||||
second.setLoadingPlanItemId(102L);
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(first, second));
|
||||
when(snapshots.workPalletCount(90L)).thenReturn(2);
|
||||
when(snapshots.actualPalletRows(90L)).thenReturn(List.of(
|
||||
actualPallet(11,100,"DP-260914-0021",101,"SKU101","BOX-001",10),
|
||||
actualPallet(11,100,"DP-260914-0022",101,"SKU101","BOX-002",10)));
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captor.capture());
|
||||
EventEnvelope event = mapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.payload().at("/orders/0/items")).hasSize(1);
|
||||
assertThat(event.payload().at("/orders/0/items/0/sourceInboundDetailId").asText()).isEqualTo("101");
|
||||
assertThat(event.payload().at("/orders/0/items/0/packageCount").asInt()).isEqualTo(2);
|
||||
assertThat(event.payload().at("/orders/0/items/0/productQuantity").asInt()).isEqualTo(20);
|
||||
assertThat(event.payload().at("/orders/0/pallets")).hasSize(2);
|
||||
assertThat(event.payload().at("/orders/0/pallets/0/skuPackages/0/sourceInboundDetailId").asText())
|
||||
.isEqualTo("101");
|
||||
assertThat(event.payload().at("/orders/0/pallets/1/skuPackages/0/sourceInboundDetailId").asText())
|
||||
.isEqualTo("101");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pricingBusinessTypeCannotTurnAStandardOrderIntoDropship() throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
|
||||
+52
-8
@@ -234,6 +234,8 @@ public class LoadingCompletedConsumer {
|
||||
String destinationType = destinationType(payload, source);
|
||||
String customerId = owner.customerId();
|
||||
String customerName = owner.customerName();
|
||||
List<FirstLegLoadingPlanCompletedV1.Item> items = "ONE_CLICK_DROPSHIP".equals(orderType)
|
||||
? canonicalItems(source.items()) : source.items();
|
||||
List<DropshipRequests.Pallet> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(orderType)
|
||||
? pallets(source, rawSource) : List.of();
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType) && dropshipPallets.isEmpty()) {
|
||||
@@ -259,7 +261,7 @@ public class LoadingCompletedConsumer {
|
||||
? dropshipPallets.size() : Objects.requireNonNullElse(source.palletCount(), 0));
|
||||
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
|
||||
order.setEstimatedWarehouseAt(parsePlannedArrival(payload.estimatedWarehouseArrivalAt()));
|
||||
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
|
||||
order.setVolumeWeightKg(items.stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
|
||||
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSourceEventId(event.eventId());
|
||||
order.setSourceLoadingPlanId(payload.loadingPlanId());
|
||||
@@ -293,7 +295,7 @@ public class LoadingCompletedConsumer {
|
||||
throw new IllegalArgumentException("散货来源托盘码数量与托盘数不一致");
|
||||
}
|
||||
}
|
||||
for(var item:source.items()) {
|
||||
for(var item:items) {
|
||||
LastlegOrderDetail detail=new LastlegOrderDetail();
|
||||
detail.setCustomerId(customerId);
|
||||
detail.setOrderId(order.getId()); detail.setSourceInboundDetailId(item.sourceInboundDetailId()); detail.setSkuCode(item.skuCode());
|
||||
@@ -377,11 +379,8 @@ public class LoadingCompletedConsumer {
|
||||
|
||||
private Map<String, List<String>> dropshipReceivingLabels(FirstLegLoadingPlanCompletedV1.Order source, JsonNode rawSource) {
|
||||
Map<String, FirstLegLoadingPlanCompletedV1.Item> itemsByDetail = new java.util.LinkedHashMap<>();
|
||||
for (var item : source.items()) {
|
||||
if (item.sourceInboundDetailId() == null || item.sourceInboundDetailId().isBlank()
|
||||
|| itemsByDetail.putIfAbsent(item.sourceInboundDetailId(), item) != null) {
|
||||
throw new IllegalArgumentException("一件代发来源明细标识缺失或重复");
|
||||
}
|
||||
for (var item : canonicalItems(source.items())) {
|
||||
itemsByDetail.put(item.sourceInboundDetailId(), item);
|
||||
}
|
||||
Map<String, List<String>> marksByDetail = new java.util.LinkedHashMap<>();
|
||||
for (JsonNode pallet : rawSource.path("pallets")) {
|
||||
@@ -395,7 +394,7 @@ public class LoadingCompletedConsumer {
|
||||
sku.path("cartonMarks").forEach(mark -> marks.add(mark.asText()));
|
||||
}
|
||||
}
|
||||
for (var item : source.items()) {
|
||||
for (var item : itemsByDetail.values()) {
|
||||
if (marksByDetail.getOrDefault(item.sourceInboundDetailId(), List.of()).size() != item.packageCount()) {
|
||||
throw new IllegalArgumentException("一件代发托盘箱唛与来源明细箱数不一致");
|
||||
}
|
||||
@@ -403,6 +402,51 @@ public class LoadingCompletedConsumer {
|
||||
return marksByDetail;
|
||||
}
|
||||
|
||||
/** 接受同一来源明细因多托装柜形成的多个分片,并在尾程落库前合并为唯一明细。 */
|
||||
private List<FirstLegLoadingPlanCompletedV1.Item> canonicalItems(List<FirstLegLoadingPlanCompletedV1.Item> source) {
|
||||
Map<String, List<FirstLegLoadingPlanCompletedV1.Item>> byDetail = new java.util.LinkedHashMap<>();
|
||||
for (var item : source == null ? List.<FirstLegLoadingPlanCompletedV1.Item>of() : source) {
|
||||
if (item.sourceInboundDetailId() == null || item.sourceInboundDetailId().isBlank()) {
|
||||
throw new IllegalArgumentException("一件代发来源明细标识缺失");
|
||||
}
|
||||
byDetail.computeIfAbsent(item.sourceInboundDetailId(), ignored -> new ArrayList<>()).add(item);
|
||||
}
|
||||
List<FirstLegLoadingPlanCompletedV1.Item> result = new ArrayList<>();
|
||||
for (List<FirstLegLoadingPlanCompletedV1.Item> fragments : byDetail.values()) {
|
||||
var first = fragments.get(0);
|
||||
if (fragments.stream().anyMatch(item -> !Objects.equals(first.skuCode(), item.skuCode())
|
||||
|| !Objects.equals(first.destinationWarehouseId(), item.destinationWarehouseId())
|
||||
|| !Objects.equals(first.destinationWarehouseCode(), item.destinationWarehouseCode()))) {
|
||||
throw new IllegalArgumentException("同一来源明细的尾程事件分片快照不一致");
|
||||
}
|
||||
List<String> marks = fragments.stream().flatMap(item -> item.cartonMarks().stream()).toList();
|
||||
result.add(new FirstLegLoadingPlanCompletedV1.Item(first.sourceInboundDetailId(), first.skuCode(),
|
||||
first.productNameZh(), first.productNameEn(),
|
||||
fragments.stream().mapToInt(FirstLegLoadingPlanCompletedV1.Item::packageCount).sum(),
|
||||
sumNullableInt(fragments, FirstLegLoadingPlanCompletedV1.Item::productQuantity),
|
||||
sumNullableInt(fragments, FirstLegLoadingPlanCompletedV1.Item::palletCount),
|
||||
sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::grossWeightKg),
|
||||
sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::netWeightKg),
|
||||
sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg),
|
||||
sumDecimal(fragments, FirstLegLoadingPlanCompletedV1.Item::volumeCbm),
|
||||
first.destinationWarehouseId(), first.destinationWarehouseCode(), first.destinationWarehouseName(),
|
||||
first.fbaTrackingNo(), first.shipmentNo(), first.shippingMark(), first.palletNo(), marks));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static Integer sumNullableInt(List<FirstLegLoadingPlanCompletedV1.Item> values,
|
||||
java.util.function.Function<FirstLegLoadingPlanCompletedV1.Item, Integer> getter) {
|
||||
List<Integer> present = values.stream().map(getter).filter(Objects::nonNull).toList();
|
||||
return present.isEmpty() ? null : present.stream().mapToInt(Integer::intValue).sum();
|
||||
}
|
||||
|
||||
private static BigDecimal sumDecimal(List<FirstLegLoadingPlanCompletedV1.Item> values,
|
||||
java.util.function.Function<FirstLegLoadingPlanCompletedV1.Item, BigDecimal> getter) {
|
||||
List<BigDecimal> present = values.stream().map(getter).filter(Objects::nonNull).toList();
|
||||
return present.isEmpty() ? null : present.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
}
|
||||
|
||||
private void createCartonLabels(String customerId, Long orderId, String destinationType,
|
||||
FirstLegLoadingPlanCompletedV1.Item item, List<String> sourceMarks) {
|
||||
if (!"OVERSEAS".equals(destinationType)) return;
|
||||
|
||||
+26
@@ -147,6 +147,32 @@ class LoadingCompletedConsumerTest {
|
||||
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);
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user