feat: complete tail order controls and terminal drayage

This commit is contained in:
2026-09-07 23:58:18 +08:00
parent f018ca85a9
commit e32fe65fa8
53 changed files with 2566 additions and 87 deletions
@@ -9,6 +9,8 @@ import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.LastlegOrderSummary;
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
import com.baiyue.lastleg.service.LastlegOrderService;
import com.baiyue.lastleg.service.LastlegOrderCommandService;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
import com.baiyue.lastleg.service.LoadingCompletedConsumer;
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer;
import org.junit.jupiter.api.Test;
@@ -32,6 +34,7 @@ import static org.mockito.Mockito.when;
class LastlegOrderControllerTest {
// Controller contract tests intentionally instantiate the controller without a Spring context here.
private final LastlegOrderService service = mock(LastlegOrderService.class);
private final LastlegOrderCommandService commandService = mock(LastlegOrderCommandService.class);
private final LastlegOrderController controller = controller();
private final LastlegOrderRequests.Query query = new LastlegOrderRequests.Query(
1L, 20L, null, null, null, null, null, null, null, null, null, null, null, null, null, null);
@@ -59,6 +62,23 @@ class LastlegOrderControllerTest {
verify(service).page(eq("200"), eq(query));
}
@Test
void holdCommandPassesVersionedIdempotentRequestToSingleOrderService() {
var request = new LastlegOrderCommandDtos.HoldRequest(true, true, "海关查验", 2);
controller.hold(user(0L, "PLATFORM"), "100", 11L, "hold-11", request);
verify(commandService).hold("100", 11L, "hold-11", request, "1", "tester");
}
@Test
void manualNodeQueryUsesOrderScopeRatherThanContainerScope() {
controller.manualNodes(user(0L, "PLATFORM"), "100", 11L);
verify(commandService).manualNodes("100", 11L);
verify(service, never()).containerProgressDetail(org.mockito.ArgumentMatchers.anyString());
}
@Test
void platformOperatorCanQueryCrossCustomerContainerSummaries() {
var summaryQuery = new LastlegOrderRequests.ContainerProgressQuery(1, 20, "CONT-", null, null);
@@ -130,6 +150,7 @@ class LastlegOrderControllerTest {
ReflectionTestUtils.setField(controller, "consumer", mock(LoadingCompletedConsumer.class));
ReflectionTestUtils.setField(controller, "shippingNodeConsumer", mock(ContainerShippingNodeConsumer.class));
ReflectionTestUtils.setField(controller, "orderService", service);
ReflectionTestUtils.setField(controller, "commandService", commandService);
ReflectionTestUtils.setField(controller, "tokenGuard", mock(InternalServiceTokenGuard.class));
return controller;
}
@@ -18,6 +18,8 @@ import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask;
import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper;
import com.baiyue.lastleg.entity.LastlegOrder;
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;
@@ -52,6 +54,7 @@ class DispatchServiceTest {
@Mock DispatchDraftMapper drafts;
@Mock DispatchDraftContainerMapper draftContainers;
@Mock DispatchPdaTaskMapper pdaTasks;
@Mock LastlegOrderMapper tailOrders;
private DispatchService service;
@BeforeEach
@@ -64,8 +67,26 @@ class DispatchServiceTest {
ReflectionTestUtils.setField(service, "drafts", drafts);
ReflectionTestUtils.setField(service, "draftContainers", draftContainers);
ReflectionTestUtils.setField(service, "pdaTasks", pdaTasks);
ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of());
lenient().when(tailOrders.selectList(any())).thenReturn(List.of());
}
@Test
void preArrangeRejectsAHeldTailOrderBeforeCreatingDispatchDocuments() {
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
LastlegOrder held = new LastlegOrder();
held.setId(2L); held.setCustomerId("100"); held.setOrderNo("LMO-HOLD-1"); held.setDetained(true);
when(tailOrders.selectList(any())).thenReturn(List.of(held));
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
null, null, null)));
assertEquals(409, exception.getStatusCode().value());
assertEquals("暂扣订单不可创建排车或出库计划:LMO-HOLD-1", exception.getReason());
verify(orders, never()).insert(any(DispatchOrder.class));
}
@Test
@@ -27,6 +27,13 @@ class InboundPlanSchemaContractTest {
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
assertTrue(schema.contains("expected_loading_at"));
assertTrue(schema.contains("planned_arrival_date"));
assertTrue(schema.contains("creation_mode"));
assertTrue(schema.contains("pallet_type"));
assertTrue(schema.contains("create table if not exists ll_order_command_record"));
assertTrue(schema.contains("create table if not exists ll_order_hold_history"));
assertTrue(schema.contains("create table if not exists ll_order_manual_node"));
assertTrue(schema.contains("create table if not exists ll_order_manual_node_history"));
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
assertTrue(schema.contains("fba_warehouse_code"));
assertFalse(schema.contains("cabinet_no"));
@@ -0,0 +1,27 @@
package com.baiyue.lastleg.inboundplan;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TailPlanCommandOpenApiContractTest {
@Test
void exposesOnlyLatestManualPlanHoldAndEightRowNodeContracts() throws Exception {
try (var stream = getClass().getClassLoader()
.getResourceAsStream("openapi/overseas-business-v1.yaml")) {
assertTrue(stream != null);
String api = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(api.contains("/api/platform/v1/last-leg/inbound-plans/manual:"));
assertFalse(api.contains("/api/platform/v1/last-leg/inbound-plans/convert:"));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/hold:"));
assertFalse(api.contains("/api/platform/v1/last-leg/orders/{id}/detained:"));
assertTrue(api.contains("/api/platform/v1/last-leg/orders/{id}/manual-nodes:"));
assertTrue(api.contains("minItems: 8"));
assertTrue(api.contains("maxItems: 8"));
assertTrue(api.contains("ACTUAL_ARRIVAL_WAITING_UNLOAD"));
}
}
}
@@ -3,8 +3,8 @@ package com.baiyue.lastleg.inboundplan.controller;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.ConvertContainerInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.ManualInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
@@ -19,6 +19,7 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Set;
import java.time.LocalDateTime;
import java.time.LocalDate;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
@@ -61,31 +62,19 @@ class InboundPlanControllerTest {
}
@Test
void platformConversionRequiresAnExplicitCustomer() {
assertThatThrownBy(() -> controller.convert(platform(), new ConvertInboundPlanRequest(2L, null)))
.isInstanceOf(ResponseStatusException.class)
.hasMessageContaining("平台生成入库计划必须指定客户");
verify(service, never()).convert(org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
void platformManualPlanPassesVersionedIdempotentCommand() {
ManualInboundPlanRequest request = manualRequest();
controller.manual(platform(), "manual-key", request);
verify(service).convertManual(request, "manual-key", "7");
}
@Test
void platformConversionPassesTheProvidedLongCustomerId() {
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(9223372036854775807L,
"193000000000200003");
controller.convert(platform(), request);
verify(service).convert("193000000000200003", 9223372036854775807L);
}
@Test
void customerConversionIgnoresRequestedCustomerAndUsesAuthenticatedScope() {
void merchantCannotCreateManualPlan() {
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
ConvertInboundPlanRequest request = new ConvertInboundPlanRequest(2L, "200");
controller.convert(customer, request);
verify(service).convert("100", 2L);
assertThatThrownBy(() -> controller.manual(customer, "manual-key", manualRequest()))
.isInstanceOf(ResponseStatusException.class);
verify(service, never()).convertManual(org.mockito.ArgumentMatchers.any(),
org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
}
@Test
@@ -109,4 +98,10 @@ class InboundPlanControllerTest {
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
Set.of("platform:overseas-warehouse:inbound-order:list"));
}
private static ManualInboundPlanRequest manualRequest() {
return new ManualInboundPlanRequest("193000000000200003", 9223372036854775807L, 0,
193000000000200003L, "US-LAX-01", "洛杉矶仓", LocalDate.of(2026, 9, 8),
"FBA-001", 2, "STANDARD", false, true, true, "加急入仓");
}
}
@@ -33,7 +33,7 @@ class InboundPlanPermissionContractTest {
@Test
void conversionUsesCanonicalInboundOrderCreateAuthority() {
Method method = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals("convert"))
.filter(candidate -> candidate.getName().equals("manual"))
.findFirst()
.orElseThrow();
@@ -3,6 +3,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.dto.ManualInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
@@ -15,6 +16,9 @@ import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.dropship.model.DropshipPalletSnapshot;
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -28,11 +32,13 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.math.BigDecimal;
import java.util.List;
import java.time.LocalDateTime;
import java.time.LocalDate;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -47,6 +53,8 @@ class InboundPlanServiceTest {
@Mock InboundPlanOrderMapper orderMapper;
@Mock InventoryBalanceMapper inventoryBalanceMapper;
@Mock DropshipOrderService dropshipOrders;
@Mock WarehouseResourceGateway warehouseGateway;
@Mock LastlegOrderCommandRecordMapper commandMapper;
private InboundPlanService service;
@@ -60,6 +68,9 @@ class InboundPlanServiceTest {
ReflectionTestUtils.setField(service, "orderMapper", orderMapper);
ReflectionTestUtils.setField(service, "inventoryBalanceMapper", inventoryBalanceMapper);
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
ReflectionTestUtils.setField(service, "warehouseGateway", warehouseGateway);
ReflectionTestUtils.setField(service, "commandMapper", commandMapper);
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
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());
@@ -112,6 +123,52 @@ class InboundPlanServiceTest {
assertEquals("洛杉矶仓", result.overseasWarehouseName());
}
@Test
void manuallyCreatesOneIndependentPlanWithFrozenMetadata() {
SourceOrderSnapshot source = order(2L, "RK-2", "1.0000", "WH-A");
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(source));
when(warehouseGateway.requireActiveSharedWarehouse(193000000000200003L)).thenReturn(
new WarehouseResourceGateway.WarehouseReference(
193000000000200003L, "US-LAX-01", "洛杉矶仓", "ACTIVE"));
when(orderMapper.markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any()))
.thenReturn(1);
ManualInboundPlanRequest request = new ManualInboundPlanRequest("100", 2L, 0,
193000000000200003L, "US-LAX-01", "洛杉矶仓", LocalDate.of(2026, 9, 15),
"RK-2", 2, "STANDARD", true, true, true, "优先收货");
var result = service.convertManual(request, "manual-2", "900");
ArgumentCaptor<InboundPlan> plan = ArgumentCaptor.forClass(InboundPlan.class);
verify(planMapper).insert(plan.capture());
assertEquals(2L, plan.getValue().getSourceOrderId());
assertEquals(LocalDate.of(2026, 9, 15), plan.getValue().getPlannedArrivalDate());
assertEquals("STANDARD", plan.getValue().getPalletType());
assertEquals("MANUAL", plan.getValue().getCreationMode());
assertEquals(2, result.palletCount());
verify(palletMapper, times(2)).insert(any(InboundPlanPallet.class));
verify(orderMapper).markPlannedManual(eq("100"), eq(2L), any(), eq(2), eq(true), eq(0), any());
verify(commandMapper).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
verify(orderMapper, never()).lockContainerOrders(any());
}
@Test
void refusesManualPlanForHeldOrderBeforeWriting() {
SourceOrderSnapshot held = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
"PENDING_ASSIGNMENT", null, new BigDecimal("1.0000"), 10, 0,
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
null, "WH-A", "仓A", 1, 0, true, 5);
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(held));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.convertManual(
new ManualInboundPlanRequest("100", 2L, 5, 193000000000200003L, "US-LAX-01", "洛杉矶仓",
LocalDate.of(2026, 9, 15), "RK-2", 1, "STANDARD", false, false, false, null),
"manual-held", "900"));
assertEquals(409, error.getStatusCode().value());
verify(planMapper, never()).insert(any(InboundPlan.class));
verify(commandMapper, never()).insert(any(com.baiyue.lastleg.entity.LastlegOrderCommandRecord.class));
}
@Test
void convertsAConsistentDestinationAssignedOrder() {
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
@@ -0,0 +1,161 @@
package com.baiyue.lastleg.service;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderCommandRecord;
import com.baiyue.lastleg.entity.LastlegOrderHoldHistory;
import com.baiyue.lastleg.entity.LastlegOrderManualNodeHistory;
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
import com.baiyue.lastleg.mapper.LastlegOrderCommandRecordMapper;
import com.baiyue.lastleg.mapper.LastlegOrderHoldHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeHistoryMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
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 org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.List;
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.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LastlegOrderCommandServiceTest {
@Mock LastlegOrderMapper orders;
@Mock LastlegOrderCommandRecordMapper commands;
@Mock LastlegOrderHoldHistoryMapper holdHistory;
@Mock LastlegOrderManualNodeMapper manualNodes;
@Mock LastlegOrderManualNodeHistoryMapper manualNodeHistory;
@Mock LastlegOrderOperationLogMapper operationLogs;
@Mock InboundOrderDetainedOutboxService detainedOutbox;
LastlegOrderCommandService service;
@BeforeEach
void setUp() {
service = new LastlegOrderCommandService();
ReflectionTestUtils.setField(service, "orders", orders);
ReflectionTestUtils.setField(service, "commands", commands);
ReflectionTestUtils.setField(service, "holdHistory", holdHistory);
ReflectionTestUtils.setField(service, "manualNodes", manualNodes);
ReflectionTestUtils.setField(service, "manualNodeHistory", manualNodeHistory);
ReflectionTestUtils.setField(service, "operationLogs", operationLogs);
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
}
@Test
void holdsOnlySelectedOrderWithVersionAndImmutableHistory() {
LastlegOrder order = order(11L, 3);
when(orders.selectById(11L)).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
when(orders.updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3),
any(LocalDateTime.class))).thenReturn(1);
var response = service.hold("100", 11L, "hold-11",
new LastlegOrderCommandDtos.HoldRequest(true, true, "海关查验", 3), "900", "管理员");
assertTrue(response.held());
assertTrue(response.chargeable());
assertEquals(4, response.version());
verify(orders).updateHold(eq("100"), eq(11L), eq(true), eq(true), eq("海关查验"), eq(3), any());
verify(holdHistory).insert(any(LastlegOrderHoldHistory.class));
verify(commands).insert(any(LastlegOrderCommandRecord.class));
verify(orders, never()).lockContainerOrders(any());
}
@Test
void rejectsStaleHoldWithoutAnyWrite() {
LastlegOrder order = order(11L, 4);
when(orders.selectById(11L)).thenReturn(order);
when(orders.lockById("100", 11L)).thenReturn(order);
ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.hold("100", 11L,
"hold-stale", new LastlegOrderCommandDtos.HoldRequest(true, false, "待核对", 3), "900", "管理员"));
assertEquals(409, error.getStatusCode().value());
verify(orders, never()).updateHold(anyString(), anyLong(), anyBoolean(), anyBoolean(),
nullable(String.class), anyInt(), any(LocalDateTime.class));
verify(commands, never()).insert(any(LastlegOrderCommandRecord.class));
}
@Test
void savesExactEightRowManualSnapshotInFrozenDisplayOrder() {
LastlegOrder order = order(12L, 0);
when(orders.selectById(12L)).thenReturn(order);
when(orders.lockById("100", 12L)).thenReturn(order);
when(orders.updateManualNodes(eq("100"), eq(12L), any(), any(), any(), eq(0), any())).thenReturn(1);
LocalDateTime filled = LocalDateTime.of(2026, 9, 8, 10, 0);
List<LastlegOrderCommandDtos.ManualNode> nodes = List.of(
node("UNLOADED", null), node("PICKUP_APPOINTED", null),
node("ACTUAL_ARRIVAL_WAITING_UNLOAD", null), node("OVERSEAS_CUSTOMS_COMPLETED", null),
node("ESTIMATED_ARRIVAL", filled), node("ACTUAL_DEPARTURE", null),
node("ESTIMATED_DEPARTURE", null), node("DOMESTIC_CUSTOMS_COMPLETED", null));
var response = service.saveManualNodes("100", 12L, "nodes-12",
new LastlegOrderCommandDtos.ManualNodesRequest(0, nodes), "900", "管理员");
assertTrue(response.manualOverrideEnabled());
assertEquals(8, response.nodes().size());
assertEquals("DOMESTIC_CUSTOMS_COMPLETED", response.nodes().get(0).nodeCode());
assertEquals("UNLOADED", response.nodes().get(7).nodeCode());
ArgumentCaptor<LastlegOrderManualNode> rows = ArgumentCaptor.forClass(LastlegOrderManualNode.class);
verify(manualNodes, org.mockito.Mockito.times(8)).insert(rows.capture());
assertEquals(8, rows.getAllValues().size());
verify(manualNodeHistory).insert(any(LastlegOrderManualNodeHistory.class));
}
@Test
void rejectsIncompleteOrFullyEmptyManualSnapshot() {
LastlegOrder order = order(12L, 0);
when(orders.selectById(12L)).thenReturn(order);
when(orders.lockById("100", 12L)).thenReturn(order);
List<LastlegOrderCommandDtos.ManualNode> nodes = List.of(
node("DOMESTIC_CUSTOMS_COMPLETED", null), node("ESTIMATED_DEPARTURE", null),
node("ACTUAL_DEPARTURE", null), node("ESTIMATED_ARRIVAL", null),
node("OVERSEAS_CUSTOMS_COMPLETED", null), node("ACTUAL_ARRIVAL_WAITING_UNLOAD", null),
node("PICKUP_APPOINTED", null), node("UNLOADED", null));
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
service.saveManualNodes("100", 12L, "nodes-empty",
new LastlegOrderCommandDtos.ManualNodesRequest(0, nodes), "900", "管理员"));
assertEquals(400, error.getStatusCode().value());
verify(orders, never()).updateManualNodes(anyString(), anyLong(), nullable(LocalDateTime.class),
nullable(LocalDateTime.class), nullable(LocalDateTime.class), anyInt(), any(LocalDateTime.class));
}
private LastlegOrderCommandDtos.ManualNode node(String code, LocalDateTime time) {
return new LastlegOrderCommandDtos.ManualNode(code, time);
}
private LastlegOrder order(long id, int version) {
LastlegOrder order = new LastlegOrder();
order.setId(id);
order.setCustomerId("100");
order.setOrderNo("LL-" + id);
order.setCurrentStatus("PENDING_ASSIGNMENT");
order.setDetained(false);
order.setVersion(version);
return order;
}
}
@@ -14,6 +14,7 @@ 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 com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import org.junit.jupiter.api.BeforeEach;
@@ -54,6 +55,7 @@ class LastlegOrderServiceTest {
@Mock LastlegOrderStatusHistoryMapper histories;
@Mock LastlegOrderFeeMapper fees;
@Mock LastlegOrderOperationLogMapper operations;
@Mock LastlegOrderManualNodeMapper manualNodes;
@Mock InboundPlanCartonMapper cartons;
@Mock FbaWarehouseValidator fbaWarehouses;
@Mock InventoryCommandService inventory;
@@ -70,6 +72,7 @@ class LastlegOrderServiceTest {
ReflectionTestUtils.setField(service, "historyMapper", histories);
ReflectionTestUtils.setField(service, "feeMapper", fees);
ReflectionTestUtils.setField(service, "operationLogMapper", operations);
ReflectionTestUtils.setField(service, "manualNodeMapper", manualNodes);
ReflectionTestUtils.setField(service, "cartonMapper", cartons);
ReflectionTestUtils.setField(service, "fbaWarehouseValidator", fbaWarehouses);
lenient().when(fbaWarehouses.required(any(), any(), any())).thenAnswer(invocation ->
@@ -133,6 +136,7 @@ class LastlegOrderServiceTest {
when(histories.selectList(any())).thenReturn(List.of());
when(fees.selectList(any())).thenReturn(List.of());
when(operations.selectList(any())).thenReturn(List.of());
when(manualNodes.selectList(any())).thenReturn(List.of());
var response = service.detail("100", 11L);