优化
This commit is contained in:
+1
-1
@@ -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 {
|
||||
|
||||
+103
@@ -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"));
|
||||
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+88
-2
@@ -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)));
|
||||
|
||||
+80
@@ -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;
|
||||
}
|
||||
}
|
||||
+79
-20
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user