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 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 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 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; } }