577 lines
30 KiB
Java
577 lines
30 KiB
Java
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.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.financefee.entity.OverseasFeeSource;
|
|
import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
|
|
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 com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
|
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
|
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 java.time.LocalDate;
|
|
import java.math.BigDecimal;
|
|
|
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
|
import static org.junit.jupiter.api.Assertions.assertSame;
|
|
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.verifyNoInteractions;
|
|
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 OverseasFeeSourceService feeSources;
|
|
@Mock LastlegOrderOperationLogMapper operations;
|
|
@Mock LastlegOrderManualNodeMapper manualNodes;
|
|
@Mock InboundPlanCartonMapper cartons;
|
|
@Mock FbaWarehouseValidator fbaWarehouses;
|
|
@Mock InventoryCommandService inventory;
|
|
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
|
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
|
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
|
@Mock ContainerShippingGateMapper containerShippingGates;
|
|
LastlegOrderService service;
|
|
|
|
@Test
|
|
void exposesWarehouseTransferNumberAsTailOrderNumber() {
|
|
LastlegOrder order = new LastlegOrder();
|
|
order.setOrderNo("WT-WH-20260911-020226");
|
|
order.setOrderType("WAREHOUSE_TRANSFER");
|
|
order.setSourceOrderNo("WT-WH-20260911-020225");
|
|
|
|
assertEquals("WT-WH-20260911-020225", LastlegOrderService.displayOrderNo(order));
|
|
}
|
|
|
|
@BeforeEach
|
|
void setUp() {
|
|
service = new LastlegOrderService();
|
|
ReflectionTestUtils.setField(service, "orderMapper", orders);
|
|
ReflectionTestUtils.setField(service, "detailMapper", details);
|
|
ReflectionTestUtils.setField(service, "historyMapper", histories);
|
|
ReflectionTestUtils.setField(service, "feeSources", feeSources);
|
|
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 ->
|
|
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);
|
|
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
|
|
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
|
|
ReflectionTestUtils.setField(service, "containerShippingGates", containerShippingGates);
|
|
}
|
|
|
|
@Test
|
|
void changesReceivingWarehouseForAllPlansReadyContainerBeforePdaSession() {
|
|
LastlegOrder first = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
|
LastlegOrder second = order(2L, "200", "PENDING_ASSIGNMENT", null);
|
|
for (LastlegOrder value : List.of(first, second)) {
|
|
value.setReceivingWarehouseVersion(0); value.setOverseasWarehouseId("193000000000200003");
|
|
value.setOverseasWarehouseCode("US-LAX-01"); value.setOverseasWarehouseName("洛杉矶仓");
|
|
}
|
|
var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900",
|
|
"US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 2, 2, 0,
|
|
0, null, true, null);
|
|
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second));
|
|
when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn(
|
|
new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE"));
|
|
when(orders.updateContainerReceivingWarehouse("BOOKING-1", "900", "US-NYC-01", "纽约仓", 0))
|
|
.thenReturn(2);
|
|
when(orders.findContainerProgress("BOOKING-1")).thenReturn(updated);
|
|
|
|
var result = service.changeReceivingWarehouse("BOOKING-1", "change-1",
|
|
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, "改派纽约"), "admin");
|
|
|
|
assertEquals("US-NYC-01", result.overseasWarehouseCode());
|
|
assertTrue(result.allPlansReady());
|
|
assertTrue(result.canChangeReceivingWarehouse());
|
|
verify(inventory).rebindReceivingWarehouse("100", 1L, 900L,
|
|
"US-NYC-01", "纽约仓", "admin", "change-1");
|
|
verify(inventory).rebindReceivingWarehouse("200", 2L, 900L,
|
|
"US-NYC-01", "纽约仓", "admin", "change-1");
|
|
verify(orders).updateContainerPlanReceivingWarehouse("BOOKING-1", 900L);
|
|
verify(receivingWarehouseOperations).insert(any(com.baiyue.lastleg.entity.ReceivingWarehouseOperation.class));
|
|
}
|
|
|
|
@Test
|
|
void permanentlyLocksReceivingWarehouseAfterPdaSessionExists() {
|
|
LastlegOrder order = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
|
order.setReceivingWarehouseVersion(0); order.setOverseasWarehouseId("193000000000200003");
|
|
order.setOverseasWarehouseCode("US-LAX-01"); order.setOverseasWarehouseName("洛杉矶仓");
|
|
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(order));
|
|
when(orders.countReceivingSessions("BOOKING-1")).thenReturn(1L);
|
|
|
|
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
|
|
service.changeReceivingWarehouse("BOOKING-1", "change-2",
|
|
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, null), "admin"));
|
|
|
|
assertEquals(409, error.getStatusCode().value());
|
|
verifyNoInteractions(warehouseResourceGateway);
|
|
}
|
|
|
|
@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());
|
|
when(feeSources.listForBusiness("100", "LASTLEG_ORDER", 11L)).thenReturn(List.of());
|
|
when(operations.selectList(any())).thenReturn(List.of());
|
|
when(manualNodes.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 detailReadsContainerNodeFromTheContainerShippingGate() {
|
|
LastlegOrder order = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
|
order.setSourceContainerBookingId("BOOKING-1");
|
|
ContainerShippingGate gate = new ContainerShippingGate();
|
|
gate.setContainerBookingId("BOOKING-1");
|
|
gate.setStandardNode("PICKUP_READY");
|
|
when(orders.selectOne(any())).thenReturn(order);
|
|
when(details.selectList(any())).thenReturn(List.of());
|
|
when(histories.selectList(any())).thenReturn(List.of());
|
|
when(feeSources.listForBusiness("100", "LASTLEG_ORDER", 11L)).thenReturn(List.of());
|
|
when(operations.selectList(any())).thenReturn(List.of());
|
|
when(manualNodes.selectList(any())).thenReturn(List.of());
|
|
when(containerShippingGates.selectById("BOOKING-1")).thenReturn(gate);
|
|
|
|
var response = service.detail("100", 11L);
|
|
|
|
assertEquals("PICKUP_READY", response.order().containerNode());
|
|
}
|
|
|
|
@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<LastlegOrderDetail> cargo = ArgumentCaptor.forClass(LastlegOrderDetail.class);
|
|
verify(details).update(cargo.capture(), any());
|
|
assertEquals("LAX-02", cargo.getValue().getDestinationWarehouseCode());
|
|
ArgumentCaptor<InboundPlanCarton> carton = ArgumentCaptor.forClass(InboundPlanCarton.class);
|
|
verify(cartons).update(carton.capture(), any());
|
|
assertEquals("193000000000000002", carton.getValue().getDestinationWarehouseId());
|
|
assertEquals("LAX-02", carton.getValue().getDestinationWarehouseCode());
|
|
ArgumentCaptor<LastlegOrderStatusHistory> 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 createFeePersistsFeeAndOperationForOwnedOrder() {
|
|
when(orders.selectOne(any())).thenReturn(order(11L, "100", "PENDING_ASSIGNMENT", null));
|
|
var request = new LastlegOrderRequests.CreateFee(
|
|
"CUSTOMS_CLEARANCE", "清关费", "RECEIVABLE", "USD", new BigDecimal("420.00"),
|
|
LocalDate.of(2026, 9, 9), null, null, null, null, null, null, null, List.of(),
|
|
null, null, null, null, null);
|
|
OverseasFeeSource draft = new OverseasFeeSource();
|
|
draft.setSourceFeeId(701L); draft.setCustomerId("100"); draft.setFeeItemName("清关费");
|
|
draft.setOriginalCurrency("USD"); draft.setOriginalAmount(new BigDecimal("420.00"));
|
|
when(feeSources.create(eq("platform_admin"), eq("fee-key"), any())).thenReturn(draft);
|
|
when(feeSources.submit(701L, "platform_admin", "fee-key:submit")).thenReturn(draft);
|
|
|
|
var fee = service.createFee("100", 11L, request, "platform_admin", "fee-key");
|
|
|
|
assertEquals("100", fee.getCustomerId());
|
|
assertEquals("清关费", fee.getFeeItemName());
|
|
verify(feeSources).create(eq("platform_admin"), eq("fee-key"), any());
|
|
verify(feeSources).submit(701L, "platform_admin", "fee-key:submit");
|
|
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");
|
|
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(3L, 25L, "LL-", "FIRST_LEG", "IN-", null,
|
|
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);
|
|
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
|
|
|
service.page(null, query);
|
|
|
|
ArgumentCaptor<com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>> 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 pageWarehouseFilterMatchesFrozenWarehouseName() {
|
|
assertWarehouseFilter("洛杉矶仓", "洛杉矶仓", true);
|
|
}
|
|
|
|
@Test
|
|
void pageWarehouseFilterMatchesFrozenWarehouseCodeIgnoringCaseAndSpaces() {
|
|
assertWarehouseFilter(" us-lax-01 ", "us-lax-01", true);
|
|
}
|
|
|
|
@Test
|
|
void pageWarehouseFilterMatchesPartialFrozenWarehouseCode() {
|
|
assertWarehouseFilter("LAX-0", "LAX-0", true);
|
|
}
|
|
|
|
@Test
|
|
void pageWarehouseFilterReturnsEmptyPageWhenNothingMatches() {
|
|
assertWarehouseFilter("WAREHOUSE-NOT-FOUND", "WAREHOUSE-NOT-FOUND", false);
|
|
}
|
|
|
|
@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");
|
|
}
|
|
|
|
@Test
|
|
void containerProgressDetailKeepsInvalidHistoricalContainerReadableAndBlocked() {
|
|
var drifted = new ContainerPlanProgressSummary("BOOKING-1", null, 2, 2, 0,
|
|
2, 2, 0, LocalDateTime.of(2026, 10, 3, 9, 0), false, "柜号缺失或不一致");
|
|
when(orders.findContainerProgress("BOOKING-1")).thenReturn(drifted);
|
|
when(orders.findContainerProgressItems("BOOKING-1")).thenReturn(List.of());
|
|
|
|
var result = service.containerProgressDetail("BOOKING-1");
|
|
|
|
assertFalse(result.summary().allPlansReady());
|
|
assertFalse(result.summary().canChangeReceivingWarehouse());
|
|
assertEquals("来源柜真实集装箱号缺失、格式无效或存在漂移",
|
|
result.summary().blockingReason());
|
|
assertEquals(result.summary().blockingReason(), result.summary().receivingWarehouseLockReason());
|
|
verify(orders).findContainerProgressItems("BOOKING-1");
|
|
}
|
|
|
|
@Test
|
|
void containerProgressPageKeepsMissingWarehouseSnapshotFromBreakingHealthyContainers() {
|
|
var query = new LastlegOrderRequests.ContainerProgressQuery(1, 10, null, null, null);
|
|
var healthy = summary(false, 3, "存在3票尚未生成有效入库计划");
|
|
var missingWarehouse = new ContainerPlanProgressSummary("BOOKING-2", "MSCU6639870",
|
|
null, null, null, 0, true, null, 2, 2, 0, 0, 0, 2,
|
|
0, LocalDateTime.of(2026, 10, 4, 9, 0), false, "存在2票尚未生成有效入库计划");
|
|
when(orders.countContainerProgress(query)).thenReturn(2L);
|
|
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(healthy, missingWarehouse));
|
|
|
|
var result = service.containerProgressPage(query);
|
|
|
|
assertEquals(2, result.records().size());
|
|
assertSame(healthy, result.records().get(0));
|
|
assertEquals("柜内接柜仓业务快照不一致", result.records().get(1).blockingReason());
|
|
assertFalse(result.records().get(1).allPlansReady());
|
|
assertTrue(result.records().get(1).canChangeReceivingWarehouse());
|
|
}
|
|
|
|
@Test
|
|
void containerProgressPageKeepsReadyAndPdaLockedContainersVisible() {
|
|
var query = new LastlegOrderRequests.ContainerProgressQuery(1, 10, null, null, null);
|
|
var ready = summary(true, 0, null);
|
|
var locked = new ContainerPlanProgressSummary("BOOKING-2", "TSTU0000061",
|
|
"193000000000200003", "US-LAX-01", "洛杉矶仓", 4, false,
|
|
"海外PDA已建立接柜会话,接柜仓已永久锁定", 1, 1, 0, 1, 1, 0,
|
|
0, LocalDateTime.of(2026, 10, 4, 9, 0), true, null);
|
|
when(orders.countContainerProgress(query)).thenReturn(2L);
|
|
when(orders.pageContainerProgress(query, 0, 10)).thenReturn(List.of(ready, locked));
|
|
|
|
var result = service.containerProgressPage(query);
|
|
|
|
assertEquals(List.of(ready, locked), result.records());
|
|
assertTrue(result.records().get(0).allPlansReady());
|
|
assertTrue(result.records().get(0).canChangeReceivingWarehouse());
|
|
assertFalse(result.records().get(1).canChangeReceivingWarehouse());
|
|
assertEquals("海外PDA已建立接柜会话,接柜仓已永久锁定",
|
|
result.records().get(1).receivingWarehouseLockReason());
|
|
}
|
|
|
|
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {
|
|
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003",
|
|
"US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0,
|
|
15 - pending, 15 - pending, pending, 0, 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, 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 pageDoesNotExposeBusinessOrderNumbersAsInboundCodes() {
|
|
LastlegOrder relabel = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
|
relabel.setOrderType("RELABEL_APPLICATION");
|
|
relabel.setInboundCode("RL-WH-20260906-701570");
|
|
LastlegOrder looseboard = order(12L, "100", "PENDING_ASSIGNMENT", null);
|
|
looseboard.setOrderType("LOCAL_LOOSEBOARD");
|
|
looseboard.setInboundCode("LB-WH-20260906-187457");
|
|
LastlegOrder firstleg = order(13L, "100", "PENDING_ASSIGNMENT", null);
|
|
firstleg.setOrderType("FIRST_LEG");
|
|
firstleg.setInboundCode("NYEXX2609060063");
|
|
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
|
resultPage.setRecords(List.of(relabel, looseboard, firstleg));
|
|
resultPage.setTotal(3);
|
|
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
|
var query = new LastlegOrderRequests.Query(1L, 10L, null, null, null, null,
|
|
null, null, null, null, null, null, null, null, null, null);
|
|
|
|
var result = service.page(null, query);
|
|
|
|
assertNull(result.records().get(0).inboundCode());
|
|
assertNull(result.records().get(1).inboundCode());
|
|
assertEquals("NYEXX2609060063", result.records().get(2).inboundCode());
|
|
}
|
|
|
|
@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;
|
|
}
|
|
|
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
|
private void assertWarehouseFilter(String input, String normalized, boolean matched) {
|
|
var resultPage = new com.baomidou.mybatisplus.extension.plugins.pagination.Page<LastlegOrder>(1, 10);
|
|
LastlegOrder row = order(11L, "100", "PENDING_ASSIGNMENT", null);
|
|
row.setOverseasWarehouseCode("US-LAX-01");
|
|
row.setOverseasWarehouseName("洛杉矶仓");
|
|
resultPage.setRecords(matched ? List.of(row) : List.of());
|
|
resultPage.setTotal(matched ? 1 : 0);
|
|
when(orders.selectPage(any(), any())).thenReturn(resultPage);
|
|
|
|
var result = service.page(null, new LastlegOrderRequests.Query(1L, 10L, null, null, null, null,
|
|
null, null, null, null, input, null, null, null, null, null));
|
|
|
|
ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<LastlegOrder>> filter =
|
|
ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
|
verify(orders).selectPage(any(), filter.capture());
|
|
var wrapper = (com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<LastlegOrder>) filter.getValue();
|
|
String warehouseSql = wrapper.getExpression().getNormal().getSqlSegment();
|
|
assertTrue(warehouseSql.contains("LOWER(TRIM(overseas_warehouse_code))"));
|
|
assertTrue(warehouseSql.contains("LOWER(TRIM(overseas_warehouse_name))"));
|
|
assertTrue(wrapper.getParamNameValuePairs().containsValue(normalized),
|
|
wrapper.getParamNameValuePairs().toString());
|
|
assertEquals(matched ? 1 : 0, result.total());
|
|
assertEquals(matched ? 1 : 0, result.records().size());
|
|
}
|
|
|
|
}
|