package com.baiyue.lastleg.service; import com.baiyue.lastleg.dto.LastlegOrderRequests; import com.baiyue.lastleg.dto.ContainerPlanProgressDetail; import com.baiyue.lastleg.dto.ContainerPlanProgressSummary; import com.baiyue.lastleg.entity.LastlegOrder; 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.mapper.LastlegOrderDetailMapper; import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; 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.web.server.ResponseStatusException; import org.springframework.test.util.ReflectionTestUtils; import java.util.List; import java.time.LocalDateTime; 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.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) class LastlegOrderServiceTest { // Service tests keep remote master-data validation and persistence independently observable here. @Mock LastlegOrderMapper orders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock FbaWarehouseValidator fbaWarehouses; LastlegOrderService service; @BeforeEach void setUp() { service = new LastlegOrderService(); ReflectionTestUtils.setField(service, "orderMapper", orders); ReflectionTestUtils.setField(service, "detailMapper", details); ReflectionTestUtils.setField(service, "historyMapper", histories); 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")); } @Test void detailDoesNotExposeDropshipCartonsOrInternalEventIds() { LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null); order.setOrderType("ONE_CLICK_DROPSHIP"); when(orders.selectOne(any())).thenReturn(order); when(details.selectList(any())).thenReturn(List.of()); when(histories.selectList(any())).thenReturn(List.of()); var response = service.detail("100", 11L); assertEquals(11L, response.order().id()); assertEquals("100", response.order().customerId()); assertEquals("ONE_CLICK_DROPSHIP", response.order().orderType()); assertEquals("一件代发", response.order().orderTypeName()); assertTrue(java.util.Arrays.stream(response.getClass().getRecordComponents()) .noneMatch(component -> "dropshipPallets".equals(component.getName()))); assertTrue(java.util.Arrays.stream(com.baiyue.lastleg.dto.LastlegOrderDetailResponse.History.class.getRecordComponents()) .noneMatch(component -> "eventId".equals(component.getName()))); } @Test void transferUpdatesOrderAndEveryCargoSnapshot() { LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null); when(orders.selectList(any())).thenReturn(List.of(order)); var request = new LastlegOrderRequests.TransferDestination( List.of(11L), "193000000000000002", "LAX-02", "Los Angeles 02", null); assertEquals(1, service.transferDestination("100", request)); assertEquals("193000000000000002", order.getTransferWarehouseId()); assertEquals("LAX-02", order.getTransferWarehouseCode()); assertEquals("DESTINATION_ASSIGNED", order.getCurrentStatus()); ArgumentCaptor cargo = ArgumentCaptor.forClass(LastlegOrderDetail.class); verify(details).update(cargo.capture(), any()); assertEquals("LAX-02", cargo.getValue().getDestinationWarehouseCode()); ArgumentCaptor carton = ArgumentCaptor.forClass(InboundPlanCarton.class); verify(cartons).update(carton.capture(), any()); assertEquals("193000000000000002", carton.getValue().getDestinationWarehouseId()); assertEquals("LAX-02", carton.getValue().getDestinationWarehouseCode()); ArgumentCaptor history = ArgumentCaptor.forClass(LastlegOrderStatusHistory.class); verify(histories).insert(history.capture()); assertEquals("100", history.getValue().getCustomerId()); assertEquals("PENDING_ASSIGNMENT", history.getValue().getFromStatus()); assertEquals("DESTINATION_ASSIGNED", history.getValue().getToStatus()); } @Test void transferRejectsAnOrderAlreadyBoundToInboundPlan() { LastlegOrder order = order(11L, "100", "DESTINATION_ASSIGNED", "IB-1"); when(orders.selectList(any())).thenReturn(List.of(order)); var request = new LastlegOrderRequests.TransferDestination( List.of(11L), "193000000000000002", "LAX-02", "Los Angeles 02", null); assertThrows(ResponseStatusException.class, () -> service.transferDestination("100", request)); verify(orders, never()).updateById(any(LastlegOrder.class)); verify(details, never()).update(any(), any()); verify(cartons, never()).update(any(), any()); } @Test void batchRejectsMissingOrCrossCustomerIdsBeforeWriting() { when(orders.selectList(any())).thenReturn(List.of(order(11L, "100", "PENDING_ASSIGNMENT", null))); var request = new LastlegOrderRequests.TransferDestination( List.of(11L, 22L), "193000000000000002", "LAX-02", "Los Angeles 02", null); ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.transferDestination("100", request)); assertTrue(error.getStatusCode().is4xxClientError()); verify(orders, never()).updateById(any(LastlegOrder.class)); } @Test void forgedFbaSelectionIsRejectedBeforeLockingOrWritingOrders() { var request = new LastlegOrderRequests.TransferDestination( List.of(11L), "193000000000000002", "FAKE", "伪造仓库", null); when(fbaWarehouses.required("193000000000000002", "FAKE", "伪造仓库")) .thenThrow(new ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST, "所选FBA仓不存在或已停用")); ResponseStatusException error = assertThrows(ResponseStatusException.class, () -> service.transferDestination("100", request)); assertTrue(error.getMessage().contains("所选FBA仓不存在或已停用")); verify(orders, never()).selectList(any()); verify(orders, never()).updateById(any(LastlegOrder.class)); } @Test void pageUsesPaginationAndPlatformFiltersWithoutCustomerScope() { var query = new LastlegOrderRequests.Query(3, 25, "LL-", "FIRST_LEG", "IN-", null, "", "PENDING_ASSIGNMENT", null, "LAX", null, null, null, null, null); var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page(3, 25); resultPage.setTotal(0); when(orders.selectPage(any(), any())).thenReturn(resultPage); service.page(null, query); ArgumentCaptor> page = ArgumentCaptor.forClass(com.baomidou.mybatisplus.extension.plugins.pagination.Page.class); verify(orders).selectPage(page.capture(), any()); assertEquals(3, page.getValue().getCurrent()); assertEquals(25, page.getValue().getSize()); } @Test void containerProgressPageUsesServerSideAggregatePagination() { var query = new LastlegOrderRequests.ContainerProgressQuery(2, 10, "CONT-", null, null); var row = summary(false, 3, "存在3票尚未生成有效入库计划"); when(orders.countContainerProgress(query)).thenReturn(11L); when(orders.pageContainerProgress(query, 10, 10)).thenReturn(List.of(row)); var result = service.containerProgressPage(query); assertEquals(2, result.page()); assertEquals(10, result.size()); assertEquals(11, result.total()); assertEquals(2, result.pages()); assertEquals(List.of(row), result.records()); } @Test void containerProgressPageRejectsReversedDateRangeBeforeQuerying() { var query = new LastlegOrderRequests.ContainerProgressQuery(1, 20, null, LocalDateTime.of(2026, 8, 20, 0, 0), LocalDateTime.of(2026, 8, 19, 0, 0)); assertThrows(ResponseStatusException.class, () -> service.containerProgressPage(query)); verify(orders, never()).countContainerProgress(any()); } @Test void containerProgressDetailReturnsEveryOrderWithoutPagination() { var summary = summary(true, 0, null); var items = List.of( new ContainerPlanProgressDetail.Item(1L, "100", "LMO-1", "RK-1", "INBOUND_PLANNED", 101L, "LIP-1", "PENDING_ASSIGNMENT", true, LocalDateTime.of(2026, 10, 3, 9, 0)), new ContainerPlanProgressDetail.Item(2L, "200", "LMO-1", "RK-2", "INBOUND_PLANNED", 102L, "LIP-2", "PDA_PENDING", true, LocalDateTime.of(2026, 10, 3, 9, 0))); when(orders.findContainerProgress("BOOKING-1")).thenReturn(summary); when(orders.findContainerProgressItems("BOOKING-1")).thenReturn(items); var result = service.containerProgressDetail(" BOOKING-1 "); assertEquals(summary, result.summary()); assertEquals(items, result.orders()); verify(orders).findContainerProgressItems("BOOKING-1"); } private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) { return new ContainerPlanProgressSummary("BOOKING-1", "CONT-1", 15, 15, 0, 15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason); } 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); return order; } }