fix(lastleg): preserve generated pallet FBA destination

This commit is contained in:
2026-09-09 01:29:38 +08:00
parent b797af706a
commit 3cfd45f4bb
8 changed files with 82 additions and 8 deletions
@@ -48,6 +48,7 @@ public record ContainerInboundPlanPreviewResponse(
String destinationWarehouseId, String destinationWarehouseId,
String destinationWarehouseCode, String destinationWarehouseCode,
String destinationWarehouseName, String destinationWarehouseName,
String fbaWarehouseCode,
int packageCount, int packageCount,
BigDecimal volumeCbm, BigDecimal volumeCbm,
Integer suggestedPalletCount, Integer suggestedPalletCount,
@@ -210,7 +210,7 @@ public class ContainerInboundPlanConversionCoordinator {
for (int localSequence = 1; localSequence <= order.suggestedPalletCount(); localSequence++) { for (int localSequence = 1; localSequence <= order.suggestedPalletCount(); localSequence++) {
items.add(new PreparedItem(order.customerId(), Long.parseLong(order.orderId()), planId, planNo, items.add(new PreparedItem(order.customerId(), Long.parseLong(order.orderId()), planId, planNo,
localSequence, ++globalSequence, "CIP:" + generationId + ":" + globalSequence, localSequence, ++globalSequence, "CIP:" + generationId + ":" + globalSequence,
"FBA".equals(order.destinationType()) ? order.destinationWarehouseCode() : null)); trimToNull(order.fbaWarehouseCode())));
} }
} }
Preparation preparation = new Preparation(warehouseId, List.copyOf(items)); Preparation preparation = new Preparation(warehouseId, List.copyOf(items));
@@ -224,7 +224,7 @@ public class InboundPlanService {
if (warehouseId == null || !Objects.equals(warehouseId, trimToNull(row.overseasWarehouseId()))) { if (warehouseId == null || !Objects.equals(warehouseId, trimToNull(row.overseasWarehouseId()))) {
blockers.add("RECEIVING_WAREHOUSE_INCONSISTENT"); blockers.add("RECEIVING_WAREHOUSE_INCONSISTENT");
} }
boolean fba = "FBA".equals(destinationType(row)); boolean fba = isFbaDestination(row);
if (fba ? trimToNull(row.fbaWarehouseCode()) == null if (fba ? trimToNull(row.fbaWarehouseCode()) == null
: row.destinationCount() != 1 || trimToNull(row.destinationWarehouseId()) == null : row.destinationCount() != 1 || trimToNull(row.destinationWarehouseId()) == null
&& trimToNull(row.destinationWarehouseCode()) == null) blockers.add("DESTINATION_NOT_READY"); && trimToNull(row.destinationWarehouseCode()) == null) blockers.add("DESTINATION_NOT_READY");
@@ -234,13 +234,14 @@ public class InboundPlanService {
Integer suggested = suggestedPalletCount(row, blockers); Integer suggested = suggestedPalletCount(row, blockers);
if (!planned && (suggested == null || suggested <= 0)) blockers.add("PALLET_COUNT_NOT_READY"); if (!planned && (suggested == null || suggested <= 0)) blockers.add("PALLET_COUNT_NOT_READY");
String destinationType = destinationType(row); String destinationType = destinationType(row);
String destinationCode = "FBA".equals(destinationType) && trimToNull(row.fbaWarehouseCode()) != null String fbaWarehouseCode = isFbaDestination(row) ? trimToNull(row.fbaWarehouseCode()) : null;
String destinationCode = fbaWarehouseCode != null
? row.fbaWarehouseCode().trim() : row.destinationWarehouseCode(); ? row.fbaWarehouseCode().trim() : row.destinationWarehouseCode();
String readiness = planned ? "PLANNED" : blockers.isEmpty() ? "READY" : "BLOCKED"; String readiness = planned ? "PLANNED" : blockers.isEmpty() ? "READY" : "BLOCKED";
return new ContainerInboundPlanPreviewResponse.Order(row.customerId(), row.customerName(), return new ContainerInboundPlanPreviewResponse.Order(row.customerId(), row.customerName(),
String.valueOf(row.orderId()), trimToNull(row.orderNo()) == null ? row.sourceOrderNo() : row.orderNo(), String.valueOf(row.orderId()), trimToNull(row.orderNo()) == null ? row.sourceOrderNo() : row.orderNo(),
row.inboundCode(), row.orderType(), destinationType, row.destinationWarehouseId(), row.inboundCode(), row.orderType(), destinationType, row.destinationWarehouseId(),
destinationCode, row.destinationWarehouseName(), row.packageCount(), row.volumeCbm(), destinationCode, row.destinationWarehouseName(), fbaWarehouseCode, row.packageCount(), row.volumeCbm(),
suggested, row.plannedPalletCount(), row.planNo(), row.planStatus(), row.version(), readiness, suggested, row.plannedPalletCount(), row.planNo(), row.planStatus(), row.version(), readiness,
List.copyOf(blockers)); List.copyOf(blockers));
} }
@@ -258,7 +259,7 @@ public class InboundPlanService {
|| row.packageCount() > 0 && row.volumeCbm().signum() == 0) return null; || row.packageCount() > 0 && row.volumeCbm().signum() == 0) return null;
try { try {
return PalletAllocator.allocate(List.of(new PalletAllocator.LineInput(row.orderId(), return PalletAllocator.allocate(List.of(new PalletAllocator.LineInput(row.orderId(),
nullSafe(row.destinationWarehouseId()) + "|" + nullSafe("FBA".equals(destinationType(row)) nullSafe(row.destinationWarehouseId()) + "|" + nullSafe(isFbaDestination(row)
? row.fbaWarehouseCode() : row.destinationWarehouseCode()), ? row.fbaWarehouseCode() : row.destinationWarehouseCode()),
row.volumeCbm(), row.packageCount(), row.recommendedPalletCount()))).pallets().size(); row.volumeCbm(), row.packageCount(), row.recommendedPalletCount()))).pallets().size();
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
@@ -268,10 +269,16 @@ public class InboundPlanService {
private static String destinationType(InboundPlanOrderMapper.ContainerPreviewOrderRow row) { private static String destinationType(InboundPlanOrderMapper.ContainerPreviewOrderRow row) {
if ("ONE_CLICK_DROPSHIP".equals(row.orderType())) return "ONE_CLICK_DROPSHIP"; if ("ONE_CLICK_DROPSHIP".equals(row.orderType())) return "ONE_CLICK_DROPSHIP";
if ("FBA".equals(row.destinationType()) || trimToNull(row.fbaWarehouseCode()) != null) return "FBA"; if (isFbaDestination(row)) return "FBA";
return "PRIVATE_ADDRESS"; return "PRIVATE_ADDRESS";
} }
private static boolean isFbaDestination(InboundPlanOrderMapper.ContainerPreviewOrderRow row) {
String explicit = trimToNull(row.destinationType());
if (explicit != null) return "FBA".equalsIgnoreCase(explicit);
return trimToNull(row.fbaWarehouseCode()) != null;
}
private static int typeRank(String type) { private static int typeRank(String type) {
return switch (type) { case "FBA" -> 0; case "ONE_CLICK_DROPSHIP" -> 1; default -> 2; }; return switch (type) { case "FBA" -> 0; case "ONE_CLICK_DROPSHIP" -> 1; default -> 2; };
} }
@@ -247,7 +247,8 @@ public class LoadingCompletedConsumer {
order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE); order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE);
order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME); order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME);
order.setReceivingWarehouseVersion(0); order.setReceivingWarehouseVersion(0);
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(containerNo); order.setFbaWarehouseCode("FBA".equals(destinationType) ? source.destinationWarehouseCode() : null);
order.setContainerNo(containerNo);
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false); order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount()); order.setPackageCount(source.packageCount());
order.setPalletCount("ONE_CLICK_DROPSHIP".equals(orderType) order.setPalletCount("ONE_CLICK_DROPSHIP".equals(orderType)
@@ -2358,6 +2358,7 @@ components:
destinationWarehouseId: { type: string, nullable: true } destinationWarehouseId: { type: string, nullable: true }
destinationWarehouseCode: { type: string, nullable: true } destinationWarehouseCode: { type: string, nullable: true }
destinationWarehouseName: { type: string, nullable: true } destinationWarehouseName: { type: string, nullable: true }
fbaWarehouseCode: { type: string, nullable: true, description: 底层目的类型为 FBA 时的目的仓编码;一件代发展示分类不改变该事实 }
packageCount: { type: integer } packageCount: { type: integer }
volumeCbm: { type: number } volumeCbm: { type: number }
suggestedPalletCount: { type: integer, nullable: true } suggestedPalletCount: { type: integer, nullable: true }
@@ -225,6 +225,47 @@ class ContainerInboundPlanConversionCoordinatorTest {
verify(warehouse).confirmPalletBatch(any()); verify(warehouse).confirmPalletBatch(any());
} }
@Test
void preparationCarriesUnderlyingFbaCodeForDropshipWithoutClassifyingOverseasDropshipAsFba() {
ConvertContainerInboundPlanRequest request = request();
var fba = previewOrder("100", "11", "RK-FBA", "ABE3", "ABE3");
var overseas = previewOrder("200", "12", "RK-OVERSEAS", "US-LAX-01", null);
var preview = new ContainerInboundPlanPreviewResponse(BOOKING_ID, "MSCU6639870", "31", "US-LAX-01",
"Los Angeles", 7L, SNAPSHOT_HASH, 2, 2, 0, 2, 2, 0, 2, 0, true,
List.of(new ContainerInboundPlanPreviewResponse.Group("ONE_CLICK_DROPSHIP", null, "ABE3", "Amazon ABE3",
1, 1, 1, 0, 1, 0, List.of(fba)),
new ContainerInboundPlanPreviewResponse.Group("ONE_CLICK_DROPSHIP", "31", "US-LAX-01", "Los Angeles",
1, 1, 1, 0, 1, 0, List.of(overseas))));
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class)))
.thenReturn("IP-FBA", "IP-OVERSEAS");
ContainerPlanConversionCommand command = ReflectionTestUtils.invokeMethod(coordinator, "prepareCommand",
1L, 2L, BOOKING_ID, "FIRST-KEY", request,
ReflectionTestUtils.invokeMethod(coordinator, "hash",
List.of(BOOKING_ID, request.snapshotVersion(), request.snapshotHash())));
ContainerInboundPlanConversionCoordinator.Preparation preparation;
try {
preparation = objectMapper.readValue(command.getPreparationJson(),
ContainerInboundPlanConversionCoordinator.Preparation.class);
} catch (Exception exception) {
throw new AssertionError(exception);
}
assertEquals(java.util.Arrays.asList("ABE3", null), preparation.items().stream()
.map(ContainerInboundPlanConversionCoordinator.PreparedItem::fbaWarehouseCode).toList());
}
private ContainerInboundPlanPreviewResponse.Order previewOrder(String customerId, String orderId,
String inboundCode, String destinationCode,
String fbaWarehouseCode) {
return new ContainerInboundPlanPreviewResponse.Order(customerId, "Customer " + customerId, orderId,
"LM-" + orderId, inboundCode, "ONE_CLICK_DROPSHIP", "ONE_CLICK_DROPSHIP",
fbaWarehouseCode == null ? "31" : null, destinationCode,
fbaWarehouseCode == null ? "Los Angeles" : "Amazon " + destinationCode,
fbaWarehouseCode, 10, BigDecimal.ONE, 1, null, null, null, 0, "READY", List.of());
}
private ConvertContainerInboundPlanRequest request() { private ConvertContainerInboundPlanRequest request() {
return new ConvertContainerInboundPlanRequest(BOOKING_ID, 7L, SNAPSHOT_HASH); return new ConvertContainerInboundPlanRequest(BOOKING_ID, 7L, SNAPSHOT_HASH);
} }
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.service;
import com.baiyue.lastleg.dropship.service.DropshipOrderService; import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot; import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import com.baiyue.lastleg.inboundplan.dto.ContainerInboundPlanPreviewResponse;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper;
import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -82,6 +83,26 @@ class ContainerInboundPlanPreviewTest {
assertFalse(result.ready()); assertFalse(result.ready());
} }
@Test
void dropshipPreviewKeepsUnderlyingFbaFactSeparateFromItsDisplayCategory() {
when(orderMapper.containerPreviewOrders("BOOKING-DROPSHIP")).thenReturn(List.of(
row(1L, "LM-FBA", "ONE_CLICK_DROPSHIP", "FBA", "ABE3", "Amazon ABE3", 1, null, null, 1),
row(2L, "LM-OVERSEAS", "ONE_CLICK_DROPSHIP", "OVERSEAS", "US-LAX-01", "洛杉矶仓", 1, null, null, 2)
));
when(dropshipOrders.palletSnapshots("100", 1L)).thenReturn(List.of(
new DropshipPalletSnapshot("SRC-P1", 10, List.of())));
when(dropshipOrders.palletSnapshots("100", 2L)).thenReturn(List.of(
new DropshipPalletSnapshot("SRC-P2", 10, List.of())));
var result = service.previewContainer("BOOKING-DROPSHIP");
assertEquals(java.util.Arrays.asList("ABE3", null), result.groups().stream()
.flatMap(group -> group.orders().stream())
.map(ContainerInboundPlanPreviewResponse.Order::fbaWarehouseCode).toList());
assertEquals(List.of("ONE_CLICK_DROPSHIP", "ONE_CLICK_DROPSHIP"), result.groups().stream()
.map(ContainerInboundPlanPreviewResponse.Group::destinationType).toList());
}
private InboundPlanOrderMapper.ContainerPreviewOrderRow row( private InboundPlanOrderMapper.ContainerPreviewOrderRow row(
long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName, long id, String orderNo, String orderType, String destinationType, String destinationCode, String destinationName,
int recommendedPallets, String planNo, Integer plannedPallets, int version) { int recommendedPallets, String planNo, Integer plannedPallets, int version) {
@@ -67,7 +67,9 @@ class LoadingCompletedConsumerTest {
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)); ArgumentCaptor<LastlegOrder> orderCaptor=ArgumentCaptor.forClass(LastlegOrder.class);
verify(orders,times(2)).insert(orderCaptor.capture());
assertTrue(orderCaptor.getAllValues().stream().allMatch(order->order.getFbaWarehouseCode()==null));
ArgumentCaptor<com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton> cartonCaptor= ArgumentCaptor<com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton> cartonCaptor=
ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton.class); ArgumentCaptor.forClass(com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton.class);
verify(cartons,times(8)).insert(cartonCaptor.capture()); verify(cartons,times(8)).insert(cartonCaptor.capture());