This commit is contained in:
2026-09-10 17:11:33 +08:00
parent 1ee32b7a6f
commit 2e6a18c58e
54 changed files with 2094 additions and 167 deletions
@@ -24,7 +24,7 @@ class PdaPalletOperationControllerContractTest {
.map(Method::getDeclaredAnnotations).flatMap(Arrays::stream)
.filter(PostMapping.class::isInstance).map(PostMapping.class::cast)
.flatMap(annotation -> Arrays.stream(annotation.value())).collect(Collectors.toSet());
assertEquals(Set.of("/pallets/resolve", "/assemble", "/disassemble", "/swap", "/unbind"), paths);
assertEquals(Set.of("/pallets/resolve", "/assemble", "/disassemble", "/relabel-disassemble", "/swap", "/unbind"), paths);
}
@Test void palletContentAnnotationUsesNativeGreaterThanOperator() throws Exception {
@@ -13,6 +13,11 @@ import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper;
import com.baiyue.lastleg.palletops.model.PalletInventoryRow;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.service.RelabelPalletMutationService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -43,6 +48,9 @@ class PdaPalletOperationServiceTest {
@Mock private InventoryLedgerMapper ledgers;
@Mock private PalletPhysicalGateway physical;
@Mock private PdaWarehouseService history;
@Mock private RelabelApplicationMapper relabelApplications;
@Mock private RelabelItemMapper relabelItems;
@Mock private RelabelPalletMutationService relabelPalletMutations;
private PdaPalletOperationService service;
@BeforeEach void setUp() {
@@ -53,6 +61,9 @@ class PdaPalletOperationServiceTest {
ReflectionTestUtils.setField(service, "ledgers", ledgers);
ReflectionTestUtils.setField(service, "physical", physical);
ReflectionTestUtils.setField(service, "history", history);
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "relabelPalletMutations", relabelPalletMutations);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
service.configureTransactions(new NoOpTransactionManager());
}
@@ -157,6 +168,83 @@ class PdaPalletOperationServiceTest {
verifyNoInteractions(physical);
}
@Test void relabelDisassembleMovesOnlySelectedHeldCarton() {
var sourcePallet = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L,
"OCCUPIED", null, 1);
var targetPallet = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L,
"IDLE", null, 2);
when(physical.resolve("SOURCE")).thenReturn(sourcePallet);
when(physical.resolve("TARGET")).thenReturn(targetPallet);
when(physical.resolveLocation(eq(88L), anyLong())).thenAnswer(invocation ->
new PalletPhysicalGateway.LocationReference(invocation.getArgument(1), "LOC-" + invocation.getArgument(1),
"STORAGE", "ACTIVE"));
when(physical.apply(eq(88L), any(), anyString(), anyLong(), anyString(), any()))
.thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2)));
RelabelApplication application = new RelabelApplication(); application.setId(600L);
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND");
application.setOverseasWarehouseId("88"); application.setOutboundOrderNo("OUT-1");
when(relabelApplications.findAnyByRelabelNo("RL-1")).thenReturn(application);
when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(application);
RelabelItem selected = relabelItem(701L, "CARTON-A", 100L);
when(relabelItems.selectList(any())).thenReturn(List.of(selected));
InventoryPosition source = position(100L, 20L, "SOURCE", 2L, 3L);
AtomicReference<InventoryPosition> target = new AtomicReference<>();
when(positions.selectById(anyLong())).thenAnswer(invocation -> {
long id = invocation.getArgument(0);
return id == 100L ? source : target.get();
});
doAnswer(invocation -> { target.set(invocation.getArgument(0)); return 1; }).when(positions)
.insert(any(InventoryPosition.class));
PalletInventoryRow held = row(100L, 20L, 3L, 0L); held.setPalletNo("SOURCE"); held.setHoldPackages(2L);
when(mapper.content("88", 20L)).thenReturn(List.of(held));
when(mapper.content("88", 10L)).thenReturn(List.of());
when(mapper.lockPosition("88", 100L)).thenReturn(held);
when(mapper.palletPackages("88", 20L)).thenReturn(2L, 1L);
when(mapper.palletPackages("88", 10L)).thenReturn(0L, 1L);
InventoryBalance balance = new InventoryBalance(); balance.setId(200L); balance.setCustomerId("100");
balance.setOverseasWarehouseId("88"); when(balances.selectById(200L)).thenReturn(balance);
AtomicReference<PdaPalletOperation> operation = new AtomicReference<>();
doAnswer(invocation -> { operation.set(invocation.getArgument(0)); return 1; }).when(mapper)
.insert(any(PdaPalletOperation.class));
when(mapper.selectById(anyLong())).thenAnswer(invocation -> operation.get());
var result = service.relabelDisassemble(identity(), new PalletOperationRequests.RelabelDisassemble(
"relabel-move-1", "RL-1", new PalletOperationRequests.PalletExpectation("SOURCE", 1),
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("CARTON-A"), "split"));
assertEquals("COMPLETED", result.status());
assertEquals(1L, source.getHoldPackages());
assertEquals(0L, source.getAvailablePackages());
assertNotNull(target.get()); assertEquals(1L, target.get().getHoldPackages());
verify(relabelPalletMutations).moveSelectedItems(eq(application), anyMap(), argThat(targets ->
targets.size() == 1 && targets.get(100L) == target.get()));
verify(balances, never()).updateById(any(InventoryBalance.class));
assertEquals("RELABEL_PALLET_DISASSEMBLE", operation.get().getOperationType());
}
@Test void relabelDisassembleReplayDoesNotRevalidateChangedInventoryOrPallets() throws Exception {
var request = new PalletOperationRequests.RelabelDisassemble("relabel-replay", "RL-1",
new PalletOperationRequests.PalletExpectation("SOURCE", 1),
new PalletOperationRequests.PalletExpectation("TARGET", 2), List.of("CARTON-A"), "split");
PdaPalletOperation existing = new PdaPalletOperation(); existing.setId(900L);
existing.setOperationType("RELABEL_PALLET_DISASSEMBLE"); existing.setStatus("COMPLETED");
existing.setRequestHash(ReflectionTestUtils.invokeMethod(service, "hash", request));
var stored = new com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation(
900L, "RELABEL_PALLET_DISASSEMBLE", "relabel-replay", "COMPLETED", false,
LocalDateTime.now(), List.of());
existing.setResultJson(new ObjectMapper().findAndRegisterModules().writeValueAsString(stored));
when(mapper.findOperation("7", "relabel-replay")).thenReturn(existing);
var replay = service.relabelDisassemble(identity(), request);
assertTrue(replay.replayed());
verifyNoInteractions(physical, relabelApplications, relabelItems, positions, balances, ledgers,
relabelPalletMutations);
}
private static PdaWarehouseIdentity identity() { return new PdaWarehouseIdentity("7", "picker", 88L); }
private static PalletPhysicalGateway.PalletReference pallet(long id, String code, long warehouse, int version) {
return new PalletPhysicalGateway.PalletReference(id, code, warehouse, 1000L + id,
@@ -178,6 +266,21 @@ class PdaPalletOperationServiceTest {
value.setPickedPackages(0L); value.setReceivingPackages(0L); value.setVersion(version); return value;
}
private static InventoryPosition position(long id, long palletId, String palletNo, long hold, long version) {
InventoryPosition value = new InventoryPosition(); value.setId(id); value.setCustomerId("100");
value.setBalanceId(200L); value.setOverseasWarehouseId("88"); value.setPalletId(palletId);
value.setPalletNo(palletNo); value.setBatchNo("DEFAULT"); value.setWarehouseLocationId(1020L);
value.setWarehouseLocationCode("LOC-1020"); value.setAvailablePackages(0L); value.setHoldPackages(hold);
value.setAllocatedPackages(0L); value.setPickedPackages(0L); value.setReceivingPackages(0L);
value.setStatus("OCCUPIED"); value.setVersion(version); return value;
}
private static RelabelItem relabelItem(long id, String cartonMark, long positionId) {
RelabelItem value = new RelabelItem(); value.setId(id); value.setApplicationId(600L); value.setCustomerId("100");
value.setCartonMark(cartonMark); value.setNotifiedPackageCount(1); value.setSourceInventoryPositionId(positionId);
value.setDestinationFbaWarehouseCode("ONT8"); return value;
}
private static final class NoOpTransactionManager extends AbstractPlatformTransactionManager {
@Override protected Object doGetTransaction() { return new Object(); }
@Override protected void doBegin(Object transaction, TransactionDefinition definition) { }
@@ -50,6 +50,13 @@ class RelabelContractTest {
"source_warehouse_code", "source_warehouse_name", "idx_ll_relabel_item_source_position" }) {
assertTrue(deployment.contains(field), () -> "deployment SQL must include " + field);
}
for (String field : new String[] { "delivery_method", "print_shipping_label", "shipping_label_file_id",
"shipping_label_file_name", "shipping_label_file_url", "fulfillment_status", "outbound_order_no",
"customer_confirmation_key", "customer_confirmed_at" }) {
assertTrue(baseline.contains(field), () -> "baseline SQL must include " + field);
assertTrue(migration.contains(field), () -> "migration SQL must include " + field);
assertTrue(deployment.contains(field), () -> "deployment SQL must include " + field);
}
}
@Test
@@ -63,6 +70,19 @@ class RelabelContractTest {
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:list"));
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:create"));
assertTrue(openApi.contains("platform:overseas-warehouse:relabel-order:status"));
for (String path : new String[] {
"/api/merchant/v1/last-leg/relabel-applications/inventory-candidates:",
"/api/merchant/v1/last-leg/relabel-applications:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/shipping-label/{fileId}/download:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/completion-photos/{fileId}/download:",
"/api/merchant/v1/last-leg/relabel-applications/{relabelNo}/confirm-completion:" }) {
assertTrue(openApi.contains(path), () -> "merchant relabel path missing: " + path);
}
assertTrue(openApi.contains("RelabelMerchantPageResponse"));
assertTrue(openApi.contains("RelabelCustomerConfirm"));
assertTrue(openApi.contains("merchant:warehouse:relabel"));
String relabelPaths = openApi.substring(openApi.indexOf("/api/platform/v1/last-leg/relabel-applications:"),
openApi.indexOf("components:"));
assertTrue(relabelPaths.contains("name: customerId, in: query, required: true"));
@@ -182,7 +182,7 @@ class PdaRelabelServiceTest {
new PdaRelabelRequests.Review("APPROVE", 2, "review-1", "ok"));
assertEquals("COMPLETED", result.taskStatus());
assertEquals("COMPLETED", result.applicationStatus());
assertEquals(RelabelApplicationService.CUSTOMER_CONFIRM_PENDING, result.applicationStatus());
verify(applicationService).changeStatus(eq("100"), eq("FBR-1"), eq("8"), any(RelabelRequests.ChangeStatus.class));
}
@@ -22,6 +22,12 @@ import com.baiyue.lastleg.inventory.entity.InventoryBalance;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.entity.OutboundPdaTask;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPdaTaskMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -60,6 +66,9 @@ class RelabelApplicationServiceTest {
@Mock InventoryCommandService inventoryCommands;
@Mock InventoryBalanceMapper inventoryBalances;
@Mock InventoryPositionMapper inventoryPositions;
@Mock OutboundOrderMapper outboundOrders;
@Mock OutboundPalletItemMapper outboundItems;
@Mock OutboundPdaTaskMapper outboundTasks;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
private RelabelApplicationService service;
@@ -77,6 +86,9 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
ReflectionTestUtils.setField(service, "inventoryBalances", inventoryBalances);
ReflectionTestUtils.setField(service, "inventoryPositions", inventoryPositions);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
ReflectionTestUtils.setField(service, "outboundTasks", outboundTasks);
ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(walletCommands.publish(any(), any(Long.class), any(), any(), any(), any(), any(Long.class))).thenReturn("wallet-event-1");
lenient().when(inventoryPositions.findTenantPosition(any(), any())).thenAnswer(invocation -> {
@@ -344,7 +356,7 @@ class RelabelApplicationServiceTest {
@Test
void completedReturnRelabelOrderEntersPendingAssignment() {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", "PROCESSING");
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-1", "PROCESSING");
order.setOrderType("RETURN_RELABEL");
@@ -362,7 +374,7 @@ class RelabelApplicationServiceTest {
@Test
void completingPartialRelabelReleasesOnlyNotifiedPackages() {
RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", "PROCESSING");
RelabelApplication application = application(11L, "FBR-PARTIAL", "10001", "key-partial", "hash", RelabelApplicationService.READY_FOR_OUTBOUND);
application.setLastlegOrderId(21L);
LastlegOrder order = order(21L, "10001", "LMO-RELABEL-PARTIAL", "PROCESSING");
when(applications.lockNo("10001", "FBR-PARTIAL")).thenReturn(application);
@@ -380,6 +392,80 @@ class RelabelApplicationServiceTest {
assertEquals(6L, move.getValue().packages());
}
@Test
void truckCustomerConfirmationIsIdempotentAndWaitsForPreDispatch() {
RelabelApplication application = confirmationApplication("TRUCK", false);
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
stubDetail(application);
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-1", "photos accepted"));
assertEquals(RelabelApplicationService.READY_FOR_OUTBOUND, application.getStatus());
assertEquals("WAIT_PRE_DISPATCH", application.getFulfillmentStatus());
assertNull(application.getOutboundOrderNo());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-1", "photos accepted"));
verify(applications, times(1)).updateById(application);
}
@Test
void expressCustomerConfirmationRequiresShippingLabelWhenWarehousePrintingIsSelected() {
RelabelApplication application = confirmationApplication("EXPRESS", true);
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-express", null)));
assertEquals(409, exception.getStatusCode().value());
verify(outboundOrders, never()).insert(any(OutboundOrder.class));
verify(applications, never()).updateById(application);
}
@Test
void pickupCustomerConfirmationCreatesOneExactCartonOutbound() {
RelabelApplication application = confirmationApplication("PICKUP", false);
RelabelItem source = item(31L, application.getId(), "SKU-A", 1, 1);
source.setSourceInventoryPositionId(201L);
source.setSourceInboundCode("CODE-1");
source.setCartonMark("BOX-1");
source.setDestinationFbaWarehouseCode("ONT8");
InventoryPosition position = new InventoryPosition();
position.setId(201L); position.setCustomerId("10001"); position.setPalletId(501L); position.setPalletNo("PALLET-1");
when(applications.lockNo("10001", "FBR-1")).thenReturn(application);
when(applications.findNo("10001", "FBR-1")).thenReturn(application);
when(items.selectList(any())).thenReturn(List.of(source));
when(histories.selectList(any())).thenReturn(List.of());
when(orders.selectById(21L)).thenReturn(order(21L, "10001", "LMO-RELABEL-1", "PENDING_ASSIGNMENT"));
when(inventoryPositions.selectById(201L)).thenReturn(position);
service.confirmCompletion("10001", "FBR-1", "customer-1",
new RelabelRequests.CustomerConfirm("confirm-pickup", null));
assertEquals("OUTBOUND_CREATED", application.getFulfillmentStatus());
assertNotNull(application.getOutboundOrderNo());
ArgumentCaptor<OutboundOrder> outbound = ArgumentCaptor.forClass(OutboundOrder.class);
verify(outboundOrders, times(1)).insert(outbound.capture());
assertEquals("RELABEL_OUTBOUND", outbound.getValue().getOutboundType());
ArgumentCaptor<OutboundPalletItem> outboundItem = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundItems, times(1)).insert(outboundItem.capture());
assertEquals("BOX-1", outboundItem.getValue().getCartonMarks());
assertEquals("PALLET-1", outboundItem.getValue().getPalletNo());
verify(outboundTasks, times(1)).insert(any(OutboundPdaTask.class));
}
private RelabelApplication confirmationApplication(String deliveryMethod, boolean printShippingLabel) {
RelabelApplication application = application(11L, "FBR-1", "10001", "key-1", "hash",
RelabelApplicationService.CUSTOMER_CONFIRM_PENDING);
application.setLastlegOrderId(21L);
application.setDeliveryMethod(deliveryMethod);
application.setPrintShippingLabel(printShippingLabel);
return application;
}
private void stubDetail(RelabelApplication application) {
when(applications.findNo(application.getCustomerId(), application.getRelabelNo())).thenReturn(application);
when(items.selectList(any())).thenReturn(List.of(item(31L, application.getId(), "SKU-A", 10, 6)));
@@ -0,0 +1,80 @@
package com.baiyue.lastleg.relabel.service;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RelabelPalletMutationServiceTest {
@Mock private RelabelItemMapper relabelItems;
@Mock private InventoryPositionMapper positions;
@Mock private OutboundOrderMapper outboundOrders;
@Mock private OutboundPalletItemMapper outboundItems;
private RelabelPalletMutationService service;
@BeforeEach void setUp() {
service = new RelabelPalletMutationService();
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "positions", positions);
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundItems", outboundItems);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
}
@Test void partialCartonMoveSplitsOutboundSnapshotAndKeepsUnselectedCartonOnSource() {
RelabelApplication application = new RelabelApplication(); application.setId(10L);
application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setOutboundOrderNo("OUT-1");
RelabelItem selected = item(20L, "CARTON-A", 101L);
RelabelItem remaining = item(21L, "CARTON-B", 100L);
InventoryPosition target = new InventoryPosition(); target.setId(101L); target.setPalletId(11L); target.setPalletNo("TARGET");
OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("100");
outbound.setOutboundNo("OUT-1"); outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK");
OutboundPalletItem source = new OutboundPalletItem(); source.setId(31L); source.setCustomerId("100");
source.setOutboundOrderId(30L); source.setWarehousePalletId(10L); source.setPalletNo("SOURCE");
source.setPackageCount(2); source.setPickedPackageCount(0); source.setStatus("WAIT_PICK");
source.setCartonMarks("CARTON-A,CARTON-B");
when(outboundOrders.lockByNo("100", "OUT-1")).thenReturn(outbound);
when(outboundItems.findByOutbound("100", 30L)).thenReturn(List.of(source));
when(relabelItems.selectList(any())).thenReturn(List.of(remaining));
service.moveSelectedItems(application, Map.of(100L, List.of(selected)), Map.of(100L, target));
assertEquals(101L, selected.getSourceInventoryPositionId());
verify(relabelItems).updateById(selected);
assertEquals("CARTON-B", source.getCartonMarks()); assertEquals(1, source.getPackageCount());
ArgumentCaptor<OutboundPalletItem> split = ArgumentCaptor.forClass(OutboundPalletItem.class);
verify(outboundItems).insert(split.capture());
assertEquals("CARTON-A", split.getValue().getCartonMarks());
assertEquals(11L, split.getValue().getWarehousePalletId());
assertEquals(1, split.getValue().getPackageCount());
assertEquals(2, outbound.getPalletCount());
}
private static RelabelItem item(long id, String mark, long positionId) {
RelabelItem value = new RelabelItem(); value.setId(id); value.setApplicationId(10L); value.setCustomerId("100");
value.setCartonMark(mark); value.setNotifiedPackageCount(1); value.setSourceInventoryPositionId(positionId);
value.setDestinationFbaWarehouseCode("ONT8"); return value;
}
}
@@ -1,9 +1,8 @@
package com.baiyue.lastleg.transfer.service;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegInboundOrder;
import com.baiyue.lastleg.relabel.entity.RelabelApplication;
import com.baiyue.lastleg.relabel.entity.RelabelItem;
import com.baiyue.lastleg.relabel.entity.RelabelStatusHistory;
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
@@ -12,16 +11,17 @@ import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper;
import com.baiyue.lastleg.relabel.mapper.RelabelStatusHistoryMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferOrder;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferInventoryAllocation;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferSourceOrder;
import com.baiyue.lastleg.transfer.mapper.InboundSnapshotMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferItemMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferInventoryAllocationMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferReceiptMapper;
@@ -58,21 +58,22 @@ import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
@ExtendWith(MockitoExtension.class)
class WarehouseTransferServiceTest {
@Mock WarehouseTransferOrderMapper transfers;
@Mock WarehouseTransferItemMapper transferItems;
@Mock WarehouseTransferCartonMapper transferCartons;
@Mock WarehouseTransferSourceOrderMapper sourceOrders;
@Mock WarehouseTransferInventoryAllocationMapper inventoryAllocations;
@Mock WarehouseTransferReceiptMapper transferReceipts;
@Mock InboundSnapshotMapper inboundSnapshots;
@Mock LastlegInboundOrderMapper inboundOrders;
@Mock LastlegOrderMapper orders;
@Mock LastlegOrderDetailMapper orderDetails;
@Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock RelabelApplicationMapper relabelApplications;
@Mock RelabelItemMapper relabelItems;
@Mock RelabelStatusHistoryMapper relabelHistories;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
@Mock InventoryCommandService inventory;
@Mock InventoryCommandMapper inventoryCommands;
@@ -83,16 +84,16 @@ class WarehouseTransferServiceTest {
service = new WarehouseTransferService();
ReflectionTestUtils.setField(service, "transfers", transfers);
ReflectionTestUtils.setField(service, "transferItems", transferItems);
ReflectionTestUtils.setField(service, "transferCartons", transferCartons);
ReflectionTestUtils.setField(service, "sourceOrders", sourceOrders);
ReflectionTestUtils.setField(service, "inventoryAllocations", inventoryAllocations);
ReflectionTestUtils.setField(service, "transferReceipts", transferReceipts);
ReflectionTestUtils.setField(service, "inboundSnapshots", inboundSnapshots);
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
ReflectionTestUtils.setField(service, "relabelItems", relabelItems);
ReflectionTestUtils.setField(service, "relabelHistories", relabelHistories);
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
@@ -101,10 +102,11 @@ class WarehouseTransferServiceTest {
.thenReturn("WT-WH-20260901-000001");
lenient().when(inventoryAllocations.selectList(any())).thenReturn(List.of());
lenient().when(transferReceipts.selectList(any())).thenReturn(List.of());
lenient().when(transferCartons.listOrder(anyString(), anyLong())).thenReturn(List.of());
}
@Test
void relabelTransferLinksHistoricalOrderAndInboundAndCreatesOnePendingReviewTailOrder() {
void relabelTransferOnlyMarksPendingBindingUntilInboundIsComplete() {
WarehouseTransferRequests.Create request = request(true);
LastlegOrder historical = historicalOrder();
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
@@ -117,18 +119,15 @@ class WarehouseTransferServiceTest {
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
verify(orders).insert(tailOrder.capture());
assertEquals(WarehouseTransferService.RETURN_RELABEL_ORDER_TYPE, tailOrder.getValue().getOrderType());
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals(WarehouseTransferService.ORDER_TYPE, tailOrder.getValue().getOrderType());
assertEquals(WarehouseTransferService.PENDING_REVIEW, tailOrder.getValue().getCurrentStatus());
assertEquals("WAREHOUSE_TRANSFER:PENDING_REVIEW;RELABEL:PENDING_BINDING", tailOrder.getValue().getSubOrderStatus());
assertTrue(tailOrder.getValue().getOrderNo().matches("WT-WH-\\d{8}-\\d{6}"));
assertNull(tailOrder.getValue().getTransferWarehouseId());
verify(sourceOrders).insert(any(WarehouseTransferSourceOrder.class));
verify(relabelApplications).insert(any(RelabelApplication.class));
verify(relabelItems).insert(any(RelabelItem.class));
verify(relabelHistories).insert(any(RelabelStatusHistory.class));
verify(orders).updateById(tailOrder.getValue());
assertNotNull(tailOrder.getValue().getRelabelOrderNo());
assertEquals("PENDING_PROCESSING", tailOrder.getValue().getRelabelStatus());
verify(relabelApplications, never()).insert(any(RelabelApplication.class));
assertNull(tailOrder.getValue().getRelabelOrderNo());
assertEquals("PENDING_BINDING", tailOrder.getValue().getRelabelStatus());
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.status());
assertEquals(WarehouseTransferService.PENDING_REVIEW, result.orderStatus());
}
@@ -144,11 +143,71 @@ class WarehouseTransferServiceTest {
var result = service.create("TENANT-A", "USER-1", request(false));
verify(relabelApplications, never()).insert(any(RelabelApplication.class));
verify(relabelItems, never()).insert(any(RelabelItem.class));
verify(relabelHistories, never()).insert(any(RelabelStatusHistory.class));
assertNull(result.relabelNo());
}
@Test
void transferCanEnterTheTransitWarehouseBeforeFbaDestinationIsKnown() {
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
"IN-1", List.of(11L), "WH-1", null, "123 Pickup Road", 5,
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
null, null, null, "FBA destination will be supplied later", false, "key-no-fba",
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product", 5, "MARK-1")));
when(inboundSnapshots.findId("TENANT-A", "IN-1")).thenReturn(101L);
when(orders.selectList(any())).thenReturn(List.of(historicalOrder()));
when(transfers.findNo(eq("TENANT-A"), org.mockito.ArgumentMatchers.anyString())).thenAnswer(invocation -> persistedTransfer());
when(transferItems.selectList(any())).thenReturn(List.of());
when(sourceOrders.selectList(any())).thenReturn(List.of());
service.create("TENANT-A", "USER-1", request);
ArgumentCaptor<WarehouseTransferOrder> transfer = ArgumentCaptor.forClass(WarehouseTransferOrder.class);
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
verify(transfers).insert(transfer.capture());
verify(orders).insert(tailOrder.capture());
assertNull(transfer.getValue().getFbaWarehouseCode());
assertNull(transfer.getValue().getFbaShipmentNo());
assertNull(tailOrder.getValue().getFbaWarehouseCode());
assertEquals("WAREHOUSE_PICKUP", transfer.getValue().getDeliveryMethod());
}
@Test
void newTransitInboundCreatesSnapshotAndImmutableCartonMarksWithoutHistoricalInventory() {
WarehouseTransferRequests.Create request = new WarehouseTransferRequests.Create(
null, null, "WH-1", null, "123 Pickup Road", 5,
new BigDecimal("0.2500"), "WAREHOUSE_PICKUP", LocalDateTime.of(2026, 9, 12, 10, 0),
null, "TRACK-MUST-NOT-BE-SAVED", null, "Incoming transit cargo", true, "key-incoming",
List.of(new WarehouseTransferRequests.Item("SKU-1", "Product A", 2, null),
new WarehouseTransferRequests.Item("SKU-2", "Product B", 3, null)));
when(transfers.findNo(eq("TENANT-A"), anyString())).thenAnswer(invocation -> persistedTransfer());
when(transferItems.selectList(any())).thenReturn(List.of());
when(sourceOrders.selectList(any())).thenReturn(List.of());
service.create("TENANT-A", "USER-1", request);
ArgumentCaptor<LastlegInboundOrder> inbound = ArgumentCaptor.forClass(LastlegInboundOrder.class);
ArgumentCaptor<WarehouseTransferOrder> transfer = ArgumentCaptor.forClass(WarehouseTransferOrder.class);
ArgumentCaptor<WarehouseTransferCarton> cartons = ArgumentCaptor.forClass(WarehouseTransferCarton.class);
ArgumentCaptor<LastlegOrder> tailOrder = ArgumentCaptor.forClass(LastlegOrder.class);
verify(inboundOrders).insert(inbound.capture());
verify(transfers).insert(transfer.capture());
verify(transferCartons, times(5)).insert(cartons.capture());
verify(orders).insert(tailOrder.capture());
verify(sourceOrders, never()).insert(any(WarehouseTransferSourceOrder.class));
verify(relabelApplications, never()).insert(any(RelabelApplication.class));
assertEquals(transfer.getValue().getTransferNo(), transfer.getValue().getInboundCode());
assertEquals(transfer.getValue().getTransferNo(), inbound.getValue().getInboundCode());
assertEquals("OVERSEAS", inbound.getValue().getDestinationType());
assertEquals(WarehouseTransferService.PENDING_REVIEW, inbound.getValue().getStatus());
assertNull(transfer.getValue().getTrackingNo());
assertEquals("PENDING_BINDING", tailOrder.getValue().getRelabelStatus());
assertEquals(List.of(1, 2, 3, 4, 5), cartons.getAllValues().stream()
.map(WarehouseTransferCarton::getCartonIndex).toList());
assertTrue(cartons.getAllValues().stream().allMatch(value -> value.getCartonMark()
.matches("WT-WH-\\d{8}-\\d{6}#5-[1-5]")));
}
@Test
void approvedTransferWithoutRelabelEntersPendingAssignment() {
WarehouseTransferOrder transfer = persistedTransfer();