This commit is contained in:
2026-09-01 11:09:14 +08:00
191 changed files with 6486 additions and 403 deletions
@@ -18,6 +18,8 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
@@ -31,7 +33,7 @@ class LastlegOrderControllerTest {
private final LastlegOrderService service = mock(LastlegOrderService.class);
private final LastlegOrderController controller = controller();
private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query(
1, 20, null, null, null, null, null, null, null, null, null, null, null, null, null);
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null);
@Test
void platformEndpointRejectsMerchantIdentity() {
@@ -76,6 +78,32 @@ class LastlegOrderControllerTest {
verify(service).containerProgressDetail("BOOKING-1");
}
@Test
void exportBuildsCsvBeforeReturningTheResponse() {
byte[] csv = "csv".getBytes(java.nio.charset.StandardCharsets.UTF_8);
when(service.exportCsv("200", query)).thenReturn(csv);
var response = controller.export(user(0L, "PLATFORM"), "200", query);
assertArrayEquals(csv, response.getBody());
verify(service).exportCsv("200", query);
}
@Test
void platformOperatorCanExportAllCustomersWithoutFilters() {
byte[] csv = "all customers".getBytes(java.nio.charset.StandardCharsets.UTF_8);
LastlegOrderRequests.Query emptyQuery = new LastlegOrderRequests.Query(
null, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
when(service.exportCsv(null, emptyQuery)).thenReturn(csv);
var response = controller.export(user(0L, "PLATFORM"), null, emptyQuery);
assertEquals(Long.valueOf(1), emptyQuery.page());
assertEquals(Long.valueOf(20), emptyQuery.size());
assertArrayEquals(csv, response.getBody());
verify(service).exportCsv(isNull(), eq(emptyQuery));
}
@Test
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
EventEnvelope event = mock(EventEnvelope.class);
@@ -18,7 +18,7 @@ class MerchantLastlegOrderControllerTest {
private final LastlegOrderService service = mock(LastlegOrderService.class);
private final MerchantLastlegOrderController controller = controller();
private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query(
1, 20, null, null, null, null, null, null, null, null, null, null, null, null, null);
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null);
@Test
void merchantListUsesOnlyAuthenticatedTenant() {
@@ -1,10 +1,13 @@
package com.baiyue.lastleg.controller;
import com.baiyue.lastleg.delivery.controller.DeliveryTransferController;
import com.baiyue.lastleg.delivery.controller.OutboundPdaController;
import com.baiyue.lastleg.dispatch.controller.DispatchController;
import com.baiyue.lastleg.dispatch.controller.DispatchQueryController;
import com.baiyue.lastleg.dispatch.controller.SmartDispatchController;
import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController;
import com.baiyue.lastleg.customervisit.controller.CustomerVisitController;
import com.baiyue.lastleg.exceptionorder.controller.ExceptionOrderController;
import com.baiyue.lastleg.inboundplan.pda.controller.ReceivingContainerController;
import com.baiyue.lastleg.inventory.command.controller.InventoryCommandController;
import org.junit.jupiter.api.Test;
@@ -39,14 +42,12 @@ class Phase4BrowserApiBoundaryTest {
}
@Test
void excludedOutboundControllersAreDisabledUnlessExplicitlyEnabledLater() {
void outboundControllersAreNotPropertyGated() {
for (Class<?> type : new Class<?>[] {
DispatchController.class, DeliveryTransferController.class, DispatchPdaController.class
DispatchController.class, DeliveryTransferController.class, DispatchPdaController.class,
OutboundPdaController.class, CustomerVisitController.class, ExceptionOrderController.class
}) {
ConditionalOnProperty condition = type.getAnnotation(ConditionalOnProperty.class);
assertTrue(condition != null, type.getSimpleName());
assertTrue(Arrays.asList(condition.name()).contains("lastleg.future-outbound-api.enabled"));
assertFalse(condition.matchIfMissing());
assertFalse(type.isAnnotationPresent(ConditionalOnProperty.class), type.getSimpleName());
}
}
@@ -65,11 +66,10 @@ class Phase4BrowserApiBoundaryTest {
}
@Test
void dispatchQueryIsEnabledByDefaultWithoutEnablingOutboundCommands() throws Exception {
void dispatchQueryAndSmartDispatchAreEnabledByDefault() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/application.yml"));
assertTrue(yaml.contains("enabled: ${LASTLEG_DISPATCH_QUERY_API_ENABLED:true}"));
assertTrue(yaml.contains("enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}"));
assertFalse(yaml.contains("future-outbound-api:"));
}
@Test
@@ -0,0 +1,71 @@
package com.baiyue.lastleg.delivery;
import com.baiyue.common.web.LongIdJacksonConfiguration;
import com.baiyue.lastleg.delivery.dto.DeliveryRequests;
import com.baiyue.lastleg.delivery.dto.DeliveryResponses;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("派送单长整型 ID JSON 契约")
class DeliveryLongIdJsonContractTest {
private static final long DELIVERY_ID = 19_000_000_000_000_001L;
private static final long FEE_ID = 19_000_000_000_000_002L;
private static final long OPERATION_ID = 19_000_000_000_000_003L;
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
JacksonAutoConfiguration.class, LongIdJacksonConfiguration.class));
@Test
void deliveryResponseIdsShouldBeSerializedAsJsonStrings() {
contextRunner.run(context -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
DeliveryResponses.DeliveryDetail detail = new DeliveryResponses.DeliveryDetail(
deliveryRow(),
List.of(),
List.of(new DeliveryResponses.DeliveryFeeRow(
FEE_ID, "RECEIVABLE", "DELIVERY_FEE", "USD", null, null)),
List.of(new DeliveryResponses.DeliveryOperationRow(
OPERATION_ID, null, "CREATED", "tester", "success")));
JsonNode json = objectMapper.readTree(objectMapper.writeValueAsString(detail));
assertThat(json.at("/order/id").isTextual()).isTrue();
assertThat(json.at("/order/id").textValue()).isEqualTo(Long.toString(DELIVERY_ID));
assertThat(json.at("/fees/0/id").textValue()).isEqualTo(Long.toString(FEE_ID));
assertThat(json.at("/operations/0/id").textValue()).isEqualTo(Long.toString(OPERATION_ID));
});
}
@Test
void quotedRequestIdShouldBeDeserializedToLong() {
contextRunner.run(context -> {
ObjectMapper objectMapper = context.getBean(ObjectMapper.class);
DeliveryRequests.OneClickItem item = objectMapper.readValue(
"{\"inventoryPositionId\":\"19000000000000001\",\"packages\":\"2\",\"sku\":\"SKU-1\"}",
DeliveryRequests.OneClickItem.class);
assertThat(item.inventoryPositionId()).isEqualTo(DELIVERY_ID);
assertThat(item.packages()).isEqualTo(2L);
});
}
private DeliveryResponses.DeliveryRow deliveryRow() {
return new DeliveryResponses.DeliveryRow(
DELIVERY_ID,
null, null, "PS-1", null, null, null, null, "CREATED", null,
null, null, null, null, null, null, null, null, null, null, null,
0, 0, null, null, null, null, null, null, null, null);
}
}
@@ -7,6 +7,8 @@ import com.baiyue.lastleg.delivery.entity.DeliveryOrder;
import com.baiyue.lastleg.delivery.entity.OutboundOrder;
import com.baiyue.lastleg.delivery.entity.OutboundPalletItem;
import com.baiyue.lastleg.delivery.mapper.DeliveryOrderMapper;
import com.baiyue.lastleg.delivery.mapper.DeliveryFeeMapper;
import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
@@ -19,6 +21,8 @@ import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -28,6 +32,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@@ -47,10 +52,13 @@ class DeliveryTransferServiceTest {
@Mock OutboundOrderMapper outboundOrders;
@Mock OutboundPalletItemMapper outboundPallets;
@Mock DeliveryOrderMapper deliveryOrders;
@Mock DeliveryFeeMapper deliveryFees;
@Mock DeliveryOperationMapper deliveryOperations;
@Mock InboundPlanPalletAllocationMapper planAllocations;
@Mock InboundPlanLineMapper planLines;
@Mock LastlegOrderMapper tailOrders;
@Mock LastlegOrderStatusHistoryMapper histories;
@Mock LastlegOrderDetailMapper tailOrderDetails;
private DeliveryTransferService service;
@@ -62,10 +70,14 @@ class DeliveryTransferServiceTest {
ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders);
ReflectionTestUtils.setField(service, "outboundPallets", outboundPallets);
ReflectionTestUtils.setField(service, "deliveryOrders", deliveryOrders);
ReflectionTestUtils.setField(service, "deliveryFees", deliveryFees);
ReflectionTestUtils.setField(service, "deliveryOperations", deliveryOperations);
ReflectionTestUtils.setField(service, "planAllocations", planAllocations);
ReflectionTestUtils.setField(service, "planLines", planLines);
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "tailOrderDetails", tailOrderDetails);
}
@Test
@@ -79,7 +91,7 @@ class DeliveryTransferServiceTest {
));
DeliveryResponses.TransferResult result = service.transfer("TENANT-A", "DSP-101", "U-1",
new DeliveryRequests.Transfer("transfer-101", "ready"));
transferRequest("transfer-101", "ready"));
ArgumentCaptor<OutboundOrder> outbound = ArgumentCaptor.forClass(OutboundOrder.class);
ArgumentCaptor<DeliveryOrder> delivery = ArgumentCaptor.forClass(DeliveryOrder.class);
@@ -92,10 +104,10 @@ class DeliveryTransferServiceTest {
assertEquals(2, outbound.getValue().getPalletCount());
assertEquals(20, outbound.getValue().getPackageCount());
assertEquals("transfer-101", outbound.getValue().getIdempotencyKey());
assertTrue(outbound.getValue().getOutboundNo().startsWith("OUT"));
assertTrue(outbound.getValue().getOutboundNo().matches("OB-WH-\\d{8}-\\d{6}"));
assertEquals(outbound.getValue().getId(), delivery.getValue().getOutboundOrderId());
assertEquals("WAIT_DELIVERY", delivery.getValue().getStatus());
assertTrue(delivery.getValue().getDeliveryNo().startsWith("DEL"));
assertTrue(delivery.getValue().getDeliveryNo().matches("DL-WH-\\d{8}-\\d{6}"));
assertEquals(List.of("ONT8", "LAX9"), pallets.getAllValues().stream()
.map(OutboundPalletItem::getDestinationWarehouseCode).toList());
assertEquals(List.of("IN-A", "IN-B"), pallets.getAllValues().stream()
@@ -116,7 +128,7 @@ class DeliveryTransferServiceTest {
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
DeliveryResponses.TransferResult result = service.transfer("TENANT-A", "DSP-101", "U-1",
new DeliveryRequests.Transfer(" transfer-101 ", null));
transferRequest(" transfer-101 ", null));
assertTrue(result.replayed());
assertEquals("OUT-301", result.outboundNo());
@@ -135,7 +147,7 @@ class DeliveryTransferServiceTest {
when(outboundOrders.findByIdempotency("TENANT-A", "shared-key")).thenReturn(other);
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.transfer("TENANT-A", "DSP-102", "U-1", new DeliveryRequests.Transfer("shared-key", null)));
service.transfer("TENANT-A", "DSP-102", "U-1", transferRequest("shared-key", null)));
assertEquals(409, exception.getStatusCode().value());
verify(outboundOrders, never()).insert((OutboundOrder) any());
@@ -155,7 +167,7 @@ class DeliveryTransferServiceTest {
when(deliveryOrders.lockByOutbound("TENANT-A", 301L)).thenReturn(delivery);
DeliveryResponses.TransferResult result = service.transfer("TENANT-A", "DSP-101", "U-1",
new DeliveryRequests.Transfer("transfer-101", null));
transferRequest("transfer-101", null));
assertTrue(result.replayed());
assertEquals("OUT-301", result.outboundNo());
@@ -176,13 +188,15 @@ class DeliveryTransferServiceTest {
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(source));
when(tailOrders.selectList(any())).thenReturn(List.of(tailOrder));
service.transfer("TENANT-A", "DSP-101", "U-1", new DeliveryRequests.Transfer("direct-order", null));
service.transfer("TENANT-A", "DSP-101", "U-1", transferRequest("direct-order", null));
assertEquals("WAIT_OUTBOUND", tailOrder.getCurrentStatus());
assertNotNull(tailOrder.getOutboundOrderNo());
assertNotNull(tailOrder.getDeliveryOrderNo());
verify(tailOrders).updateById(tailOrder);
verify(histories).insert(any(LastlegOrderStatusHistory.class));
ArgumentCaptor<LastlegOrderStatusHistory> history = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class);
verify(histories).insert(history.capture());
assertEquals("TENANT-A", history.getValue().getCustomerId());
}
@Test
@@ -195,7 +209,7 @@ class DeliveryTransferServiceTest {
when(dispatchItems.findByDispatchOrder("TENANT-A", 101L)).thenReturn(List.of(planned));
ResponseStatusException exception = assertThrows(ResponseStatusException.class, () ->
service.transfer("TENANT-A", "DSP-101", "U-1", new DeliveryRequests.Transfer("transfer-101", null)));
service.transfer("TENANT-A", "DSP-101", "U-1", transferRequest("transfer-101", null)));
assertEquals(409, exception.getStatusCode().value());
assertTrue(exception.getReason().contains("received"));
@@ -239,6 +253,12 @@ class DeliveryTransferServiceTest {
return value;
}
private static DeliveryRequests.Transfer transferRequest(String idempotencyKey, String remark) {
return new DeliveryRequests.Transfer(idempotencyKey, LocalDateTime.now(), "SELF_DELIVERY", null, null,
"ONE_PICKUP_ONE_DROP", "PALLET", null, null, null, null, null, null, null, remark,
List.of(new DeliveryRequests.FeeItem("DELIVERY_FEE", "USD", BigDecimal.ZERO, null)));
}
private static DispatchItem item(Long id, Long warehousePalletId, String palletNo, String destinationCode,
String destinationName, String inboundCode, int packages) {
DispatchItem value = new DispatchItem();
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -63,6 +64,7 @@ class CargoDiscardServiceTest {
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "adjustments", adjustments);
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
@@ -2,11 +2,17 @@ package com.baiyue.lastleg.dispatch.pda.service;
import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaOperation;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaPick;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaOperationMapper;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickMapper;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -15,7 +21,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.when;
@@ -24,6 +33,16 @@ class DispatchPdaWorkflowServiceTest {
@Mock private DispatchOrderMapper orders;
@Mock private DispatchPdaOperationMapper operations;
@Mock private OutboundOrderMapper outboundOrders;
@Mock private DispatchItemMapper items;
@Mock private DispatchPdaTaskMapper tasks;
@Mock private DispatchPdaPickMapper picks;
@Test
void labelsTransferredPalletsWithOriginalAndCurrentFbaWarehouses() {
assertEquals("ONT8 -> ONT", DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ONT"));
assertNull(DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ont8"));
assertNull(DispatchPdaWorkflowService.warehouseRouteLabel(null, "ONT"));
}
@Test
void pushRequiresVehiclePlateBeforeCreatingPdaTask() {
@@ -118,4 +137,34 @@ class DispatchPdaWorkflowServiceTest {
assertEquals("DEPARTED", result.status());
assertEquals(true, result.replayed());
}
@Test
void completingAnAutomaticallyCompletedTaskReturnsIdempotentSuccess() {
DispatchPdaWorkflowService service = new DispatchPdaWorkflowService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "operations", operations);
ReflectionTestUtils.setField(service, "items", items);
ReflectionTestUtils.setField(service, "tasks", tasks);
ReflectionTestUtils.setField(service, "picks", picks);
DispatchOrder order = new DispatchOrder();
order.setId(1L); order.setCustomerId("100"); order.setDispatchNo("PC1");
order.setStatus("READY_TO_DEPART"); order.setVehiclePlate("CA-100");
DispatchPdaTask task = new DispatchPdaTask();
task.setId(2L); task.setStatus("COMPLETED");
DispatchItem item = new DispatchItem(); item.setPackageCount(6);
DispatchPdaPick pick = new DispatchPdaPick(); pick.setPackages(6L);
DispatchPdaRequests.CompletePicking request = new DispatchPdaRequests.CompletePicking("complete-1", "normal");
when(orders.findByDispatchNo("100", "PC1")).thenReturn(order);
when(orders.lockById("100", 1L)).thenReturn(order);
when(operations.findKey("100", "complete-1")).thenReturn(null);
when(tasks.lockByDispatch("100", 1L)).thenReturn(task);
when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item));
when(picks.listTask("100", 2L)).thenReturn(List.of(pick));
DispatchPdaResponses.PickingCompleted result = service.completePicking("100", "PC1", request);
assertEquals("READY_TO_DEPART", result.status());
assertEquals(6, result.pickedPackages());
assertEquals(true, result.replayed());
}
}
@@ -75,8 +75,8 @@ class DispatchPickOrderAttributionServiceTest {
"t1", "OUT1", "D1", List.of(pick), LocalDateTime.of(2026, 8, 17, 15, 0));
assertEquals(List.of(new DispatchPickOrderAttributionService.OrderPackages(101L, 4L)), result);
verify(history).insert(argThat((LastlegOrderStatusHistory value) -> "PARTIALLY_SHIPPED".equals(value.getToStatus())
&& value.getRemark().contains("6/10")));
verify(history).insert(argThat((LastlegOrderStatusHistory value) -> "t1".equals(value.getCustomerId())
&& "PARTIALLY_SHIPPED".equals(value.getToStatus()) && value.getRemark().contains("6/10")));
}
private DispatchPickOrderAttributionService service() {
@@ -56,6 +56,7 @@ class DispatchServiceTest {
ReflectionTestUtils.setField(service, "candidates", candidates);
ReflectionTestUtils.setField(service, "drafts", drafts);
ReflectionTestUtils.setField(service, "draftContainers", draftContainers);
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
}
@@ -97,7 +97,7 @@ class DropshipOrderServiceTest {
when(cartons.listProfile("C-1", 2L)).thenReturn(List.of(secondCarton));
service.activateAfterStockIn("C-1", 10L, 50L, List.of(
receipt(101L, "WH-P-1", 201L, 1), receipt(102L, "WH-P-2", 202L, 1)));
receipt(101L, "SOURCE-P-1", 201L, 1), receipt(102L, "SOURCE-P-2", 202L, 1)));
assertThat(first.getStatus()).isEqualTo("ACTIVE");
assertThat(first.getWarehousePalletId()).isEqualTo(101L);
@@ -0,0 +1,74 @@
package com.baiyue.lastleg.exceptionorder.service;
import com.baiyue.lastleg.entity.LastlegInboundOrder;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper;
import com.baiyue.lastleg.mapper.LastlegInboundOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ExceptionOrderServiceTest {
@Mock private ExceptionOrderMapper exceptionOrders;
@Mock private LastlegOrderMapper lastMileOrders;
@Mock private LastlegInboundOrderMapper inboundOrders;
private ExceptionOrderService service;
@BeforeEach
void setUp() {
service = new ExceptionOrderService();
ReflectionTestUtils.setField(service, "exceptionOrders", exceptionOrders);
ReflectionTestUtils.setField(service, "lastMileOrders", lastMileOrders);
ReflectionTestUtils.setField(service, "inboundOrders", inboundOrders);
}
@Test
void resolvesTheOneToOneInboundAndLastMileRelation() {
LastlegOrder tail = tail("LM-1", "IN-1");
LastlegInboundOrder inbound = inbound("IN-1");
when(lastMileOrders.selectList(any())).thenReturn(List.of(tail));
when(inboundOrders.selectList(any())).thenReturn(List.of(inbound));
var relation = service.resolve(null, null, "IN-1", "LM-1");
assertThat(relation.lastMileOrderNo()).isEqualTo("LM-1");
assertThat(relation.inboundCode()).isEqualTo("IN-1");
assertThat(relation.customerId()).isEqualTo("1001");
}
@Test
void rejectsInputsThatExistButDoNotBelongTogether() {
when(lastMileOrders.selectList(any())).thenReturn(List.of(tail("LM-1", "IN-1")));
when(inboundOrders.selectList(any())).thenReturn(List.of(inbound("IN-2")));
assertThatThrownBy(() -> service.resolve(null, null, "IN-2", "LM-1"))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("不是同一关联关系");
}
private LastlegOrder tail(String orderNo, String inboundCode) {
LastlegOrder value = new LastlegOrder();
value.setId(12L); value.setCustomerId("1001"); value.setOrderNo(orderNo); value.setInboundCode(inboundCode);
value.setCustomerName("测试客户"); value.setPackageCount(10);
return value;
}
private LastlegInboundOrder inbound(String inboundCode) {
LastlegInboundOrder value = new LastlegInboundOrder();
value.setId(11L); value.setCustomerId("1001"); value.setInboundCode(inboundCode); value.setPackageCount(10);
return value;
}
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.inboundplan.mapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class InboundPlanOrderMapperContractTest {
@Test
void transferWarehouseIsTheEffectivePalletDestination() throws Exception {
Select select = InboundPlanOrderMapper.class
.getMethod("lockOrder", String.class, Long.class)
.getAnnotation(Select.class);
String sql = String.join(" ", select.value());
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,'')))"));
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
assertTrue(sql.contains("0 AS destinationMismatchCount"));
}
}
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.inboundplan.mapper;
import org.apache.ibatis.annotations.Select;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
class InboundPlanQueryMapperContractTest {
@Test
void platformListReadsVersionAndDoesNotRequireANullDispatchCustomer() throws Exception {
Select select = InboundPlanQueryMapper.class
.getMethod("rows", String.class,
com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests.Query.class,
long.class, long.class)
.getAnnotation(Select.class);
String sql = String.join(" ", select.value());
assertTrue(sql.contains("p.version version"));
assertTrue(sql.contains("WHERE <if test='customerId != null and customerId != \"\"'>di.customer_id=#{customerId} AND </if>di.order_id=l.order_id"));
}
}
@@ -12,6 +12,8 @@ import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
import com.baiyue.lastleg.inboundplan.pda.repository.PdaWorkflowRepository;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -51,6 +53,7 @@ class InboundPdaWorkflowServiceTest {
@Mock InboundUnloadCompletedOutboxService unloadCompletedOutbox;
@Mock LastlegOrderMapper orders;
@Mock UnloadPhotoFileGateway unloadPhotoFiles;
@Mock DropshipOrderService dropshipOrders;
private InboundPdaWorkflowService service;
private InboundPlan plan;
@@ -73,6 +76,7 @@ class InboundPdaWorkflowServiceTest {
ReflectionTestUtils.setField(service, "unloadCompletedOutbox", unloadCompletedOutbox);
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "unloadPhotoFiles", unloadPhotoFiles);
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
ReflectionTestUtils.setField(service, "sourceObjectMapper", new ObjectMapper().findAndRegisterModules());
service.initializeObjectMapper();
plan = plan();
@@ -189,6 +193,20 @@ class InboundPdaWorkflowServiceTest {
assertEquals(3, targetPallet.getActualPackageCount());
}
@Test
void cartonScanRejectsBatchQuantityAndRequiresOneRealCartonPerScan() {
InboundPlanCarton first = carton("BOX-BATCH-1", "UNASSIGNED", targetLine.getId(), "WH-A");
InboundPlanCarton second = carton("BOX-BATCH-2", "UNASSIGNED", targetLine.getId(), "WH-A");
second.setId(42L);
activeTask();
when(repository.lockCarton("100", "BOX-BATCH-1")).thenReturn(first);
assertThrows(ResponseStatusException.class, () -> service.scanCarton("100", "LIP1",
new PdaRequests.ScanCarton("BOX-BATCH-1", null, 2, "scan-batch", "7", "worker")));
assertEquals("UNASSIGNED", first.getStatus());
assertEquals("UNASSIGNED", second.getStatus());
}
@Test
void cartonScanRejectsDuplicateAndWrongDestination() {
InboundPlanCarton repeated = carton("BOX-002", "ON_PALLET", targetLine.getId(), "WH-A");
@@ -225,7 +243,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void countMustPrecedePickAndPickedBoxesDriveActualVolume() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
target.setCountedPackageCount(0);
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
@@ -248,7 +268,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void overPickReturnsConfirmationWithoutChangingAllocationOrPallet() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
candidateQueries();
@@ -459,8 +481,95 @@ class InboundPdaWorkflowServiceTest {
verify(repository, never()).update(task);
}
@Test
void partialCompleteStocksOnlyActualPickedPackagesAndKeepsPartialStatus() {
target.setPickedPackageCount(2);
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
when(repository.cartons(plan.getId())).thenReturn(List.of());
var result = service.partialComplete("100", "LIP1",
new PdaRequests.ForceComplete("partial-1", "7", "worker"));
assertEquals("PARTIAL_INBOUND", result.status());
assertEquals(5, result.expectedPackageCount());
assertEquals(2, result.pickedPackageCount());
assertEquals("PARTIAL_INBOUND", plan.getStatus());
verify(stockReceipts).stockIn(eq("100"), eq(plan), eq(task), eq(List.of(targetLine)),
eq(List.of(target)), eq(List.of(targetPallet)), any(), eq(true));
verify(unloadCompletedOutbox).publish(eq(plan), eq(task), eq(List.of(101L)));
}
@Test
void partialCompleteRequiresUnloadPhotosBeforeStockIn() {
target.setPickedPackageCount(2);
task.setUnloadPhotoFileIds(null);
task.setUnloadPhotoUploadedAt(null);
task.setEstimatedEmptyReturnAt(null);
activeTask();
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.partialComplete("100", "LIP1",
new PdaRequests.ForceComplete("partial-no-photo", "7", "worker")));
assertEquals(409, exception.getStatusCode().value());
verify(stockReceipts, never()).stockIn(any(), any(), any(), any(), any(), any(), any(), eq(true));
verify(unloadCompletedOutbox, never()).publish(any(), any(), any());
}
@Test
void dropshipTaskIsMarkedAndWholePalletPickReceivesThePlannedPalletAtOnce() {
plan.setSourceOrderId(101L);
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
when(repository.order(101L)).thenReturn(order);
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(
new DropshipPalletSnapshot("PLT-21", 3,
List.of(new DropshipPalletSnapshot.Sku("SKU-A", "Product A", 3, 30)))));
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.allocations(plan.getId())).thenReturn(List.of(target));
when(repository.pallets(plan.getId())).thenReturn(List.of(targetPallet));
when(repository.task("100", plan.getId())).thenReturn(task);
activeTask();
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
var taskResult = service.task("100", "LIP1");
var pickResult = service.pickWholePallet("100", "LIP1", target.getId(),
new PdaRequests.WholePalletOperation("whole-1", "7", "worker"));
assertEquals("ONE_CLICK_DROPSHIP", taskResult.orderType());
assertTrue(taskResult.wholePalletInbound());
assertEquals("SKU-A", taskResult.dropshipPallets().get(0).skus().get(0).skuCode());
assertEquals(targetPallet.getId(), taskResult.dropshipPallets().get(0).planPalletId());
assertEquals(target.getId(), taskResult.dropshipPallets().get(0).allocationId());
assertEquals("WHOLE_PALLET_PICKED", pickResult.result());
assertEquals(3, target.getPickedPackageCount());
assertEquals(3, targetPallet.getActualPackageCount());
}
@Test
void dropshipPlanRejectsOrdinaryPackagePicking() {
plan.setSourceOrderId(101L);
LastlegOrder order = new LastlegOrder();
order.setId(101L); order.setOrderType("ONE_CLICK_DROPSHIP");
when(repository.order(101L)).thenReturn(order);
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.pick("100", "LIP1", target.getId(),
new PdaRequests.PackageOperation(1, "wrong-mode", "7", "worker")));
assertEquals(409, exception.getStatusCode().value());
verify(repository, never()).lockAllocation(plan.getId(), target.getId());
}
@Test
void rejectsReuseOfIdempotencyKeyWithDifferentActionOrPayload() {
targetLine.setFbaWarehouseCode("ONT8");
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
InboundPdaOperation existing = new InboundPdaOperation();
existing.setAction("COUNT");
existing.setRequestHash("different");
@@ -472,7 +581,9 @@ class InboundPdaWorkflowServiceTest {
@Test
void completedTaskStillReplaysAnAlreadyPersistedPick() {
targetLine.setFbaWarehouseCode("ONT8");
activeTask();
when(repository.lines(plan.getId())).thenReturn(List.of(targetLine));
when(repository.lockAllocation(plan.getId(), target.getId())).thenReturn(target);
when(repository.line(plan.getId(), targetLine.getId())).thenReturn(targetLine);
when(repository.lockPallet(plan.getId(), targetPallet.getId())).thenReturn(targetPallet);
@@ -19,6 +19,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -157,6 +158,21 @@ class InboundStockReceiptServiceTest {
assertEquals(5L, requests.getAllValues().stream().mapToLong(InventoryCommandRequests.StockIn::packages).sum());
}
@Test
void stocksInToHeldInventoryWhenInboundOrderIsDetained() {
Fixture fixture = fixture(5, 5, 5, 10);
fixture.order.setDetained(true);
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now());
verify(inventory).stockInHeld(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class));
verify(inventory, never()).stockIn(any(), any(), any());
}
@Test
void existingTaskReceiptMakesStockInReplayANoOp() {
Fixture fixture = fixture(10, 5, 5, 10);
@@ -200,6 +216,7 @@ class InboundStockReceiptServiceTest {
void activatesDropshipAfterGenericStockInReceiptIsCreated() {
Fixture fixture = fixture(5, 5, 5, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 5, 0, 0, 0, false, false));
@@ -227,6 +244,45 @@ class InboundStockReceiptServiceTest {
verify(receipts, never()).insert(any(InboundStockReceipt.class));
}
@Test
void partialStockInSynchronizesOrderWithoutActivatingDropship() {
Fixture fixture = fixture(5, 2, 2, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 2)));
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(inventory.stockIn(eq("100"), eq("operator"), any(InventoryCommandRequests.StockIn.class)))
.thenReturn(new InventoryCommandResponse(8L, "STOCK_IN", 9L, 10L, 2, 0, 0, 0, false, false));
service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true);
assertEquals("INBOUND_COMPLETED", fixture.order.getCurrentStatus());
assertEquals("INBOUND_PLAN:PARTIAL_INBOUND", fixture.order.getSubOrderStatus());
verify(orders).updateById(fixture.order);
verify(orderHistories).insert(any(LastlegOrderStatusHistory.class));
verify(dropshipOrders, never()).activateAfterStockIn(any(), any(), any(), any());
}
@Test
void rejectsPartiallyPickedDropshipSourcePallet() {
Fixture fixture = fixture(5, 5, 2, 10);
fixture.order.setOrderType("ONE_CLICK_DROPSHIP");
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
when(dropshipOrders.palletSnapshots("100", 101L)).thenReturn(List.of(dropshipPallet("PLT-21", 5)));
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.stockIn("100", fixture.plan, fixture.task, List.of(fixture.line),
List.of(fixture.allocation), List.of(fixture.pallet), LocalDateTime.now(), true));
assertEquals(409, exception.getStatusCode().value());
verify(inventory, never()).stockIn(any(), any(), any());
}
private DropshipPalletSnapshot dropshipPallet(String palletNo, long packages) {
return new DropshipPalletSnapshot(palletNo, packages,
List.of(new DropshipPalletSnapshot.Sku("SKU-1", "Product", packages, packages * 10)));
}
private Fixture fixture(int required, int planned, int picked, int quantity) {
InboundPlan plan = new InboundPlan(); plan.setId(1L); plan.setPlanNo("IP-1"); plan.setOverseasWarehouseId(900L);
InboundPdaTask task = new InboundPdaTask(); task.setId(2L); task.setOperatorId("operator");
@@ -502,6 +502,20 @@ class ReceivingContainerServiceBehaviorTest {
verifyNoInteractions(workflow, operations);
}
@Test
void cartonResolutionReturnsInboundAndPalletMergeReminderFromTailLegOrder() {
stubCurrent("FBA", "CONT-A");
LastlegOrder marked = order("FBA");
marked.setPalletMergeFlag("REQUIRED");
when(orders.selectById(1L)).thenReturn(marked);
var resolved = service.resolveCarton("7", new ReceivingContainerRequests.CartonResolve("IN-1#2-1"));
assertThat(resolved.inboundCode()).isEqualTo("IN-1");
assertThat(resolved.palletMergeRequired()).isTrue();
assertThat(resolved.palletMergeHint()).contains("合托");
}
@Test
void fbaCartonResolutionRejectsCrossContainerAndInvalidPrefixesAsConflicts() {
stubCurrent("FBA", "CONT-A");
@@ -70,7 +70,7 @@ class InboundPlanPaginationTest {
.getMethod("rows", String.class, InboundPlanRequests.Query.class, long.class, long.class)
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("di.customer_id=#{customerId} AND di.order_id=l.order_id");
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
"q.destinationWarehouseCode", "q.warehouse",
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.service;
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.UpdateInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
@@ -55,6 +56,7 @@ class InboundPlanServiceTest {
ReflectionTestUtils.setField(service, "allocationMapper", allocationMapper);
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
@@ -182,13 +184,93 @@ class InboundPlanServiceTest {
}
@Test
void rejectsTransferWarehouseThatDiffersFromCargoSnapshots() {
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
void transferWarehouseOverridesOriginalCargoDestination() {
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
.thenReturn(1);
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
verify(planMapper, never()).insert(any(InboundPlan.class));
service.convert("100", 1L);
ArgumentCaptor<InboundPlanPallet> pallet = ArgumentCaptor.forClass(InboundPlanPallet.class);
verify(palletMapper).insert(pallet.capture());
assertEquals("WH-B", pallet.getValue().getDestinationWarehouseId());
assertEquals("WH-B", pallet.getValue().getDestinationWarehouseCode());
}
@Test
void convertsDropshipSourcePalletsWithoutRedistributingTheirPackages() {
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(order(2L, "RK-2", "1.0000", "WH-A")));
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
.thenReturn(1);
service.convertDropship("100", 2L, List.of(
new InboundPlanService.WholePalletInput("SOURCE-P-1", 3),
new InboundPlanService.WholePalletInput("SOURCE-P-2", 7)));
ArgumentCaptor<InboundPlanPallet> pallets = ArgumentCaptor.forClass(InboundPlanPallet.class);
verify(palletMapper, times(2)).insert(pallets.capture());
assertEquals(List.of("SOURCE-P-1", "SOURCE-P-2"),
pallets.getAllValues().stream().map(InboundPlanPallet::getPalletNo).toList());
ArgumentCaptor<InboundPlanPalletAllocation> allocations =
ArgumentCaptor.forClass(InboundPlanPalletAllocation.class);
verify(allocationMapper, times(2)).insert(allocations.capture());
assertEquals(List.of(3, 7), allocations.getAllValues().stream()
.map(InboundPlanPalletAllocation::getPlannedPackageCount).toList());
}
@Test
void updateSynchronizesDestinationSnapshotToLinesAndPallets() {
InboundPlan plan = new InboundPlan();
plan.setId(20L);
plan.setPlanNo("IP-20");
plan.setCustomerId("100");
plan.setStatus("PENDING_ASSIGNMENT");
plan.setLineCount(1);
plan.setPalletCount(1);
plan.setVersion(3);
InboundPlanLine line = new InboundPlanLine();
line.setId(21L);
line.setCustomerId("100");
line.setPlanId(20L);
line.setRequiredPackageCount(1);
line.setPlannedPalletCount(1);
line.setDestinationWarehouseId("OLD-WH");
line.setDestinationWarehouseCode("OLD-CODE");
line.setDestinationWarehouseName("Old warehouse");
InboundPlanPallet pallet = new InboundPlanPallet();
pallet.setId(22L);
pallet.setCustomerId("100");
pallet.setPlanId(20L);
pallet.setSequenceNo(1);
pallet.setActualPackageCount(0);
pallet.setDestinationWarehouseId("OLD-WH");
pallet.setDestinationWarehouseCode("OLD-CODE");
pallet.setDestinationWarehouseName("Old warehouse");
when(planMapper.selectOne(any())).thenReturn(plan);
when(lineMapper.selectList(any())).thenReturn(List.of(line), List.of(line));
when(palletMapper.selectList(any())).thenReturn(List.of(pallet), List.of(pallet));
when(allocationMapper.selectList(any())).thenReturn(List.of());
service.update("100", "IP-20",
new UpdateInboundPlanRequest("100", 193000000000200003L,
" US-LAX-01 ", " Los Angeles Warehouse ", 3));
ArgumentCaptor<InboundPlanLine> updatedLine = ArgumentCaptor.forClass(InboundPlanLine.class);
verify(lineMapper).updateById(updatedLine.capture());
assertEquals("193000000000200003", updatedLine.getValue().getDestinationWarehouseId());
assertEquals("US-LAX-01", updatedLine.getValue().getDestinationWarehouseCode());
assertEquals("Los Angeles Warehouse", updatedLine.getValue().getDestinationWarehouseName());
ArgumentCaptor<InboundPlanPallet> updatedPallet = ArgumentCaptor.forClass(InboundPlanPallet.class);
verify(palletMapper).updateById(updatedPallet.capture());
assertEquals("193000000000200003", updatedPallet.getValue().getDestinationWarehouseId());
assertEquals("US-LAX-01", updatedPallet.getValue().getDestinationWarehouseCode());
assertEquals("Los Angeles Warehouse", updatedPallet.getValue().getDestinationWarehouseName());
}
@Test
@@ -49,6 +49,16 @@ class PalletAllocatorTest {
.map(PalletAllocator.AllocationDraft::plannedPackageCount).toList());
}
@Test
void keepsAtLeastTheFirstLegRecommendedPalletCount() {
PalletAllocator.Result result = PalletAllocator.allocate(List.of(
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal("1.5000"), 6, 3)));
assertEquals(3, result.pallets().size());
assertEquals(6, result.allocations().stream()
.mapToInt(PalletAllocator.AllocationDraft::plannedPackageCount).sum());
}
private PalletAllocator.Result allocate(String volume) {
return PalletAllocator.allocate(List.of(
new PalletAllocator.LineInput(1L, "WH-A", new BigDecimal(volume),
@@ -24,6 +24,8 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -57,6 +59,7 @@ class InventoryCommandServiceTest {
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
lenient().when(operations.findIdempotency(any(), any())).thenReturn(null);
lenient().when(commands.lockInboundTransitBalances(any(), any(), any())).thenReturn(List.of());
}
@Test
@@ -117,6 +120,7 @@ class InventoryCommandServiceTest {
balance.setSkuCode("SKU-1");
balance.setInTransitPackages(12L);
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(balance);
when(commands.lockInboundTransitBalances("100", 101L, "IN-CODE-1")).thenReturn(List.of(balance));
InventoryCommandResponse result = service.stockIn("100", "7", stockIn("IN-1", "stock-in-transit", 12, 24));
@@ -125,6 +129,48 @@ class InventoryCommandServiceTest {
assertEquals(12L, balance.getAvailablePackages());
}
@Test
void stockInTransfersInTransitFromTheOriginalOverseasWarehouse() {
InventoryBalance target = balance(200L, 0);
target.setSkuCode("SKU-1");
target.setInTransitPackages(0L);
InventoryBalance source = balance(201L, 0);
source.setOverseasWarehouseId("WH-OLD");
source.setSkuCode("SKU-1");
source.setInTransitPackages(12L);
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(target);
when(commands.lockInboundTransitBalances("100", 101L, "IN-CODE-1")).thenReturn(List.of(source));
InventoryCommandResponse result = service.stockIn("100", "7",
stockIn("IN-1", "stock-in-cross-warehouse", 12, 24));
assertEquals(12, result.availablePackages());
assertEquals(0L, source.getInTransitPackages());
ArgumentCaptor<InventoryLedger> written = ArgumentCaptor.forClass(InventoryLedger.class);
verify(ledgers, times(2)).insert(written.capture());
assertEquals("STOCK_IN_TRANSIT_RELEASE", written.getAllValues().get(0).getBusinessType());
assertEquals(-12L, written.getAllValues().get(0).getDeltaPackages());
assertEquals("STOCK_IN", written.getAllValues().get(1).getBusinessType());
assertEquals("IN_TRANSIT", written.getAllValues().get(1).getStatusFrom());
}
@Test
void stockInKeepsOutsideSourceWhenInTransitIsInsufficient() {
InventoryBalance target = balance(200L, 0);
target.setSkuCode("SKU-1");
target.setInTransitPackages(2L);
when(balances.lockDimension("100", "WH-1", "ONT8", 101L, "IN-CODE-1")).thenReturn(target);
when(commands.lockInboundTransitBalances("100", 101L, "IN-CODE-1")).thenReturn(List.of(target));
service.stockIn("100", "7", stockIn("IN-1", "stock-in-partial-transit", 5, 10));
ArgumentCaptor<InventoryLedger> ledger = ArgumentCaptor.forClass(InventoryLedger.class);
verify(ledgers).insert(ledger.capture());
assertEquals("OUTSIDE", ledger.getValue().getStatusFrom());
assertEquals(0L, target.getInTransitPackages());
assertEquals(5L, target.getAvailablePackages());
}
@Test
void differentSkusShareInboundBalanceAndKeepSkuInLedger() {
InventoryBalance balance = balance(200L, 4);
@@ -16,6 +16,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
@@ -31,6 +32,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
@@ -43,7 +45,7 @@ class LocalLooseboardServiceTest {
@Mock LocalLooseboardOperationMapper operations;
@Mock LastlegOrderMapper lastlegOrders;
@Mock LastlegOrderStatusHistoryMapper lastlegHistories;
@Mock LastlegOrderNoGenerator lastlegOrderNoGenerator;
@Spy LastlegOrderNoGenerator lastlegOrderNoGenerator = new LastlegOrderNoGenerator();
private LocalLooseboardService service;
private final Map<String, LocalLooseboardOperation> operationRows = new HashMap<>();
@@ -57,7 +59,8 @@ class LocalLooseboardServiceTest {
ReflectionTestUtils.setField(service, "lastlegHistories", lastlegHistories);
ReflectionTestUtils.setField(service, "lastlegOrderNoGenerator", lastlegOrderNoGenerator);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
org.mockito.Mockito.lenient().when(lastlegOrderNoGenerator.next()).thenReturn("LMO-LOOSE-1");
org.mockito.Mockito.lenient().when(lastlegOrderNoGenerator.next(anyString(), any(), anyLong()))
.thenReturn("LB-WH-20260901-000001");
org.mockito.Mockito.lenient().when(operations.findKey(anyString(), any(), anyString()))
.thenAnswer(call -> operationRows.get(call.getArgument(2)));
org.mockito.Mockito.lenient().when(operations.insert(any(LocalLooseboardOperation.class))).thenAnswer(call -> {
@@ -23,6 +23,9 @@ class OverseasProcurementInventoryBoundaryTest {
assertFalse(source.contains("availablePackages"));
assertFalse(source.contains("addOutbound"));
assertFalse(source.contains("OverseasProcurementOutbound"));
assertFalse(source.contains("outboundQuantity"));
String responses = Files.readString(Path.of(
"src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementResponses.java"),
StandardCharsets.UTF_8);
assertTrue(responses.contains("outboundQuantity"));
}
}
@@ -20,6 +20,7 @@ class OverseasProcurementSchemaContractTest {
assertTrue(sql.contains("operation_type VARCHAR(32) NOT NULL"));
assertTrue(sql.contains("review_idempotency_key VARCHAR(255) NULL"));
assertTrue(sql.contains("review_request_hash CHAR(64) NULL"));
assertTrue(sql.contains("outbound_details JSON NULL COMMENT '出库明细'"));
assertTrue(sql.contains("idempotency_key VARCHAR(255) NULL COMMENT '操作幂等键'"));
assertTrue(sql.contains("change_detail JSON NULL"));
org.junit.jupiter.api.Assertions.assertFalse(sql.contains("CREATE TABLE IF NOT EXISTS ll_overseas_procurement_outbound"));
@@ -39,6 +40,8 @@ class OverseasProcurementSchemaContractTest {
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN review_idempotency_key VARCHAR(255) NULL"));
assertTrue(sql.contains(
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN review_request_hash CHAR(64) NULL"));
assertTrue(sql.contains(
"ALTER TABLE ll_overseas_procurement_order ADD COLUMN outbound_details JSON NULL"));
assertTrue(sql.contains(
"table_name='ll_overseas_procurement_status_history' AND column_name='idempotency_key'"));
}
@@ -45,6 +45,7 @@ class OverseasProcurementServiceTest {
ReflectionTestUtils.setField(service, "histories", histories);
ReflectionTestUtils.setField(service, "warehouseDirectory", warehouseDirectory);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
}
@Test
@@ -63,9 +64,32 @@ class OverseasProcurementServiceTest {
assertEquals(OverseasProcurementService.PENDING_REVIEW, detail.summary().status());
assertEquals(0, detail.summary().inboundQuantity());
assertEquals(0, detail.summary().outboundQuantity());
assertEquals(0, detail.summary().inventoryQuantity());
}
@Test
void outboundQuantityAndDisplayStatusFollowOutboundDetails() {
OverseasProcurementOrder order = order(OverseasProcurementService.APPROVED, 90, 0);
when(orders.findNo("100", "HC1")).thenReturn(order);
when(histories.listOrder("100", 1L)).thenReturn(List.of());
order.setOutboundDetails("[]");
assertEquals("PENDING_OUTBOUND", service.detail("100", "HC1").summary().status());
order.setOutboundDetails("[{\"id\":\"1\",\"quantity\":1}]");
var partial = service.detail("100", "HC1").summary();
assertEquals(1, partial.outboundQuantity());
assertEquals(89, partial.inventoryQuantity());
assertEquals("PARTIALLY_OUTBOUND", partial.status());
order.setOutboundDetails("[{\"id\":\"1\",\"quantity\":90}]");
var completed = service.detail("100", "HC1").summary();
assertEquals(90, completed.outboundQuantity());
assertEquals(0, completed.inventoryQuantity());
assertEquals("OUTBOUNDED", completed.status());
}
@Test
void pendingOrderCannotReceiveInventory() {
OverseasProcurementOrder order = order(OverseasProcurementService.PENDING_REVIEW, 0, 0);
@@ -19,6 +19,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
@@ -44,7 +45,7 @@ class RelabelApplicationServiceTest {
@Mock RelabelStatusHistoryMapper histories;
@Mock LastlegOrderMapper orders;
@Mock LastlegOrderStatusHistoryMapper orderHistories;
@Mock LastlegOrderNoGenerator orderNoGenerator;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
private RelabelApplicationService service;
@@ -58,7 +59,6 @@ class RelabelApplicationServiceTest {
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
ReflectionTestUtils.setField(service, "objectMapper", mapper());
lenient().when(orderNoGenerator.next()).thenReturn("LMO-RELABEL-1");
}
@Test
@@ -91,10 +91,10 @@ class RelabelApplicationServiceTest {
assertEquals("PENDING_ASSIGNMENT", tailOrder.getValue().getCurrentStatus());
assertEquals("RELABEL:PENDING_PROCESSING", tailOrder.getValue().getSubOrderStatus());
assertEquals("CUSTOMER", tailOrder.getValue().getSourceRegion());
assertEquals("LMO-RELABEL-1", tailOrder.getValue().getOrderNo());
assertTrue(tailOrder.getValue().getOrderNo().matches("RL-WH-\\d{8}-\\d{6}"));
assertEquals(tailOrder.getValue().getInboundCode(), tailOrder.getValue().getSourceInboundId());
assertEquals(tailOrder.getValue().getInboundCode(), application.getValue().getRelabelNo());
assertTrue(tailOrder.getValue().getInboundCode().startsWith("FBR"));
assertTrue(tailOrder.getValue().getInboundCode().startsWith("RL-"));
assertEquals(10, tailOrder.getValue().getPackageCount());
assertEquals(6, application.getValue().getNotifiedPackageCount());
assertEquals("SKU-A", relabelItem.getValue().getSkuCode());
@@ -12,6 +12,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.times;
@@ -53,4 +54,19 @@ class LastlegOrderNoGeneratorTest {
when(sequenceMapper.currentValue()).thenReturn(10_000L);
assertThrows(IllegalStateException.class, () -> generator.next(LocalDate.of(2026, 8, 27)));
}
@Test void actualContainerNumberIsStableAndShared() {
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
assertTrue(generator.forContainerNo("2087379674555461633").matches("FL-WH-\\d{8}-\\d{6}"));
assertEquals(generator.forContainerNo("MSCU1"), new LastlegOrderNoGenerator().forContainerNo("MSCU1"));
assertNotEquals(generator.forContainerNo("MSCU1"), generator.forContainerNo("CAB1"));
assertTrue(generator.forContainerNo("MSCU1234567890").length() <= 32);
assertThrows(IllegalArgumentException.class, () -> generator.forContainerNo(" "));
}
@Test void formatsBusinessWarehouseAndDate() {
String value = new LastlegOrderNoGenerator().next("OUTBOUND", "warehouse-LAX-01", 123L);
assertTrue(value.matches("OB-LAX-\\d{8}-000123"), value);
assertTrue(value.length() <= 24);
}
}
@@ -8,9 +8,12 @@ import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -22,11 +25,16 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.util.List;
import java.time.LocalDateTime;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -38,8 +46,12 @@ class LastlegOrderServiceTest {
@Mock LastlegOrderMapper orders;
@Mock LastlegOrderDetailMapper details;
@Mock LastlegOrderStatusHistoryMapper histories;
@Mock LastlegOrderFeeMapper fees;
@Mock LastlegOrderOperationLogMapper operations;
@Mock InboundPlanCartonMapper cartons;
@Mock FbaWarehouseValidator fbaWarehouses;
@Mock InventoryCommandService inventory;
@Mock InboundOrderDetainedOutboxService detainedOutbox;
LastlegOrderService service;
@BeforeEach
@@ -48,12 +60,16 @@ class LastlegOrderServiceTest {
ReflectionTestUtils.setField(service, "orderMapper", orders);
ReflectionTestUtils.setField(service, "detailMapper", details);
ReflectionTestUtils.setField(service, "historyMapper", histories);
ReflectionTestUtils.setField(service, "feeMapper", fees);
ReflectionTestUtils.setField(service, "operationLogMapper", operations);
ReflectionTestUtils.setField(service, "cartonMapper", cartons);
ReflectionTestUtils.setField(service, "fbaWarehouseValidator", fbaWarehouses);
lenient().when(fbaWarehouses.required(any(), any(), any())).thenAnswer(invocation ->
new FbaWarehouseValidator.VerifiedFbaWarehouse(
Long.parseLong(invocation.getArgument(0)), invocation.getArgument(1), invocation.getArgument(2),
"US", "CA"));
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
}
@Test
@@ -63,6 +79,8 @@ class LastlegOrderServiceTest {
when(orders.selectOne(any())).thenReturn(order);
when(details.selectList(any())).thenReturn(List.of());
when(histories.selectList(any())).thenReturn(List.of());
when(fees.selectList(any())).thenReturn(List.of());
when(operations.selectList(any())).thenReturn(List.of());
var response = service.detail("100", 11L);
@@ -102,6 +120,52 @@ class LastlegOrderServiceTest {
assertEquals("DESTINATION_ASSIGNED", history.getValue().getToStatus());
}
@Test
void createFeePersistsFeeAndOperationForOwnedOrder() {
when(orders.selectOne(any())).thenReturn(order(11L, "100", "PENDING_ASSIGNMENT", null));
var request = new LastlegOrderRequests.CreateFee(
"清关费", "USD", new BigDecimal("420.00"), "PENDING_REVIEW", "报关代理");
var fee = service.createFee("100", 11L, request, "platform_admin");
assertEquals("100", fee.getCustomerId());
assertEquals("清关费", fee.getFeeItem());
assertEquals(new BigDecimal("420.00"), fee.getAmount());
verify(fees).insert(any(com.baiyue.lastleg.entity.LastlegOrderFee.class));
verify(operations).insert(any(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class));
}
@Test
void palletMergeMarkIsPersistedAndAuditedForPdaReminder() {
LastlegOrder order = order(11L, "100", "INBOUND_PLANNED", "IB-1");
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
when(orders.updatePalletMergeFlag(any(), anyLong(), any(), any())).thenReturn(1);
service.setPalletMergeRequired("100", 11L,
new LastlegOrderRequests.SetPalletMergeRequired(true), "platform_admin");
verify(orders).updatePalletMergeFlag(org.mockito.ArgumentMatchers.eq("100"),
org.mockito.ArgumentMatchers.eq(11L), org.mockito.ArgumentMatchers.eq("REQUIRED"), any());
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
verify(operations).insert(operation.capture());
assertEquals("标记合托", operation.getValue().getOperationNode());
assertTrue(operation.getValue().getResult().contains("PDA"));
}
@Test
void completedPhysicalMergeCannotBeUnmarked() {
LastlegOrder order = order(11L, "100", "INBOUND_PLANNED", "IB-1");
order.setPalletMergeFlag("MERGED");
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
assertThrows(ResponseStatusException.class, () -> service.setPalletMergeRequired("100", 11L,
new LastlegOrderRequests.SetPalletMergeRequired(false), "platform_admin"));
verify(orders, never()).updatePalletMergeFlag(any(), anyLong(), any(), any());
}
@Test
void transferRejectsAnOrderAlreadyBoundToInboundPlan() {
LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1");
@@ -145,7 +209,7 @@ class LastlegOrderServiceTest {
@Test
void pageUsesPaginationAndPlatformFiltersWithoutCustomerScope() {
var query = new LastlegOrderRequests.Query(3, 25, "LL-", "FIRST_LEG", "IN-", null,
var query = new LastlegOrderRequests.Query(3L, 25L, "LL-", "FIRST_LEG", "IN-", null,
"", "PENDING_ASSIGNMENT", null, "LAX", null, null, null, null, null);
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(3, 25);
resultPage.setTotal(0);
@@ -222,12 +286,69 @@ class LastlegOrderServiceTest {
15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason);
}
@Test
void pageReturnsPalletMergeRelatedInboundNumbers() {
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IB-1");
order.setPalletMergeFlag("MERGED");
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
resultPage.setRecords(List.of(order));
resultPage.setTotal(1);
when(orders.selectPage(any(), any())).thenReturn(resultPage);
when(orders.findPalletMergeRelations(List.of(11L))).thenReturn(List.of(
new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-2", "PMG-1"),
new LastlegOrderMapper.PalletMergeRelationRow(11L, "INBOUND-3", "PMG-1")));
var query = new LastlegOrderRequests.Query(1L, 10L, null, null, null, null,
null, null, null, null, null, null, null, null, null);
var result = service.page(null, query);
assertEquals("PMG-1", result.records().get(0).palletMergeBatchId());
assertEquals(List.of("INBOUND-2", "INBOUND-3"), result.records().get(0).palletMergeInboundNos());
}
@Test
void holdingBeforeInboundPersistsTheOrderMarkerAndOperationRemark() {
LastlegOrder order = order(11L, "100", "PENDING_INBOUND", null);
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(true, "等待质检"), "operator");
assertTrue(order.getDetained());
verify(orders).updateById(order);
ArgumentCaptor<com.baiyue.lastleg.entity.LastlegOrderOperationLog> operation =
ArgumentCaptor.forClass(com.baiyue.lastleg.entity.LastlegOrderOperationLog.class);
verify(operations).insert(operation.capture());
assertEquals("暂扣", operation.getValue().getOperationNode());
assertEquals("等待质检", operation.getValue().getResult());
verify(detainedOutbox).publish(eq(order), eq(true), eq("等待质检"), isNull(), eq("operator"), any(), anyLong());
verify(inventory, never()).releaseInboundOrderHold(any(), any(), anyLong(), any(), any());
}
@Test
void releasingAfterInboundMovesAllOrderHeldInventoryToAvailable() {
LastlegOrder order = order(11L, "100", "INBOUND_COMPLETED", "IP-1");
order.setOrderNo("LM-1");
order.setDetained(true);
when(orders.selectOne(any())).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
service.setDetained("100", 11L, new LastlegOrderRequests.SetDetained(false, "质检通过"), "operator");
verify(inventory).releaseInboundOrderHold("100", "operator", 11L, "LM-1", "质检通过");
assertEquals(false, order.getDetained());
verify(detainedOutbox).publish(eq(order), eq(false), eq("质检通过"), isNull(), eq("operator"), any(), anyLong());
}
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
LastlegOrder order = new LastlegOrder();
order.setId(id);
order.setCustomerId(customerId);
order.setCurrentStatus(status);
order.setInboundOrderNo(inboundOrderNo);
order.setOrderNo("LM-" + id);
order.setSourceInboundId(String.valueOf(1000 + id));
order.setInboundCode("IN-" + id);
return order;
}
@@ -5,7 +5,7 @@ class LoadingCompletedConsumerTest {
private final Map<String,String> frozenContainerNos=new HashMap<>();
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock LastlegOrderNoDailySequenceMapper sequences; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(3); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("LMO-[0-9]{6}-0001")); }
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
@Test void freezesFormalPlannedWarehouseArrivalFromLoadingEvent(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("estimatedWarehouseArrivalAt","2026-10-03T09:00"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); service.consume(changed); ArgumentCaptor<LastlegOrder> captor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captor.capture()); assertTrue(captor.getAllValues().stream().allMatch(order->java.time.LocalDateTime.of(2026,10,3,9,0).equals(order.getEstimatedWarehouseAt()))); }
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion(){ service.consume(event()); verify(shippingGates).initialize("book-1","100","","MSCU6639870",0L,1L); }
@Test void rejectsUnnormalizedActualContainerNumberInsteadOfUsingCabinetNumber(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("containerNo"," mscu6639870 ").put("cabinetNo","FALLBACK-CABINET"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertThrows(IllegalArgumentException.class,()->service.consume(changed)); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
@@ -22,6 +22,8 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
import org.mockito.Mockito;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -64,6 +66,10 @@ class TaillegRealConsumerAcceptanceTest {
"com.baiyue.lastleg.inventory.command.mapper", "com.baiyue.lastleg.relabel.mapper",
"com.baiyue.lastleg.transfer.mapper", "com.baiyue.lastleg.looseboard.mapper"})
static class AcceptanceApplication {
@Bean ContainerShippingGateMapper containerShippingGateMapper() {
return Mockito.mock(ContainerShippingGateMapper.class);
}
@Bean WarehouseResourceGateway warehouseResourceGateway() {
return new WarehouseResourceGateway() {
public WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId) {
@@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.server.ResponseStatusException;
@@ -46,6 +47,8 @@ import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.lenient;
@@ -70,7 +73,7 @@ class WarehouseTransferServiceTest {
@Mock RelabelApplicationMapper relabelApplications;
@Mock RelabelItemMapper relabelItems;
@Mock RelabelStatusHistoryMapper relabelHistories;
@Mock LastlegOrderNoGenerator orderNoGenerator;
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
@Mock InventoryCommandService inventory;
@Mock InventoryCommandMapper inventoryCommands;
private WarehouseTransferService service;
@@ -94,7 +97,8 @@ class WarehouseTransferServiceTest {
ReflectionTestUtils.setField(service, "inventory", inventory);
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
lenient().when(orderNoGenerator.next()).thenReturn("LMO-TRANSFER-1");
lenient().when(orderNoGenerator.next(anyString(), any(), anyLong()))
.thenReturn("WT-WH-20260901-000001");
lenient().when(inventoryAllocations.selectList(any())).thenReturn(List.of());
lenient().when(transferReceipts.selectList(any())).thenReturn(List.of());
}
@@ -116,7 +120,7 @@ class WarehouseTransferServiceTest {
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("LMO-TRANSFER-1", tailOrder.getValue().getOrderNo());
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));