优化
This commit is contained in:
+4
-2
@@ -10,11 +10,13 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanSchemaContractTest {
|
||||
@Test
|
||||
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
|
||||
void planIsUniqueByCustomerAndSourceOrderWhileContainerCanBeShared() throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||
assertTrue(stream != null, "lastleg schema must be available");
|
||||
String schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_source_order (customer_id, source_order_id)"));
|
||||
assertTrue(schema.contains("key idx_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertFalse(schema.contains("unique key uk_ll_inbound_plan_container"));
|
||||
assertTrue(schema.contains("default 'pending_assignment'"));
|
||||
assertTrue(schema.contains("required_package_count"));
|
||||
assertTrue(schema.contains("planned_package_count"));
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.lastleg.inboundplan.controller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class InboundPlanPermissionContractTest {
|
||||
private static final String LIST = "platform:overseas-warehouse:inbound-order:list";
|
||||
|
||||
@Test
|
||||
void listAndDetailUseOnlyCanonicalInboundOrderListAuthority() {
|
||||
Map<String, Method> methods = Arrays.stream(InboundPlanController.class.getDeclaredMethods())
|
||||
.collect(Collectors.toMap(Method::getName, method -> method));
|
||||
|
||||
for (String name : new String[] {"page", "detail", "fbaAppointmentDetail"}) {
|
||||
Method method = methods.get(name);
|
||||
assertEquals("hasAuthority('" + LIST + "')", method.getAnnotation(PreAuthorize.class).value());
|
||||
assertEquals("Required authority: " + LIST, method.getAnnotation(Operation.class).description());
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-1
@@ -13,6 +13,7 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
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;
|
||||
@@ -42,6 +43,7 @@ import static org.mockito.Mockito.when;
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundStockReceiptServiceTest {
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderDetailMapper orderDetails;
|
||||
@Mock InboundStockReceiptMapper receipts;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock DropshipOrderService dropshipOrders;
|
||||
@@ -52,6 +54,7 @@ class InboundStockReceiptServiceTest {
|
||||
void setUp() {
|
||||
service = new InboundStockReceiptService();
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderDetails", orderDetails);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "dropshipOrders", dropshipOrders);
|
||||
@@ -74,6 +77,7 @@ class InboundStockReceiptServiceTest {
|
||||
assertEquals(5L, request.getValue().packages());
|
||||
assertEquals(101L, request.getValue().inboundOrderId());
|
||||
assertEquals("IN-1", request.getValue().inboundCode());
|
||||
assertEquals("SKU-1", request.getValue().skuCode());
|
||||
ArgumentCaptor<InboundStockReceipt> receipt = ArgumentCaptor.forClass(InboundStockReceipt.class);
|
||||
verify(receipts).insert(receipt.capture());
|
||||
assertEquals(5L, receipt.getValue().getReceivedPackages());
|
||||
@@ -84,10 +88,11 @@ class InboundStockReceiptServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void storesOneInboundOrderAcrossMultiplePalletsWithoutSkuSplitting() {
|
||||
void splitsOneInboundOrderAcrossSkuAndPalletDimensions() {
|
||||
Fixture fixture = fixture(10, 5, 5, 10);
|
||||
fixture.detail.setPackageCount(5);
|
||||
LastlegOrderDetail second = detail(202L, "SKU-2", 5, 7);
|
||||
when(orderDetails.selectList(any())).thenReturn(List.of(fixture.detail, second));
|
||||
InboundPlanPallet secondPallet = pallet(22L, 2002L);
|
||||
InboundPlanPalletAllocation secondAllocation = allocation(32L, 12L, 22L, 5);
|
||||
when(orders.selectBatchIds(any())).thenReturn(List.of(fixture.order));
|
||||
@@ -187,6 +192,7 @@ class InboundStockReceiptServiceTest {
|
||||
order.setCurrentStatus("INBOUND_PLANNED");
|
||||
org.mockito.Mockito.lenient().when(orders.lockById("100", 101L)).thenReturn(order);
|
||||
LastlegOrderDetail detail = detail(201L, "SKU-1", required, quantity);
|
||||
org.mockito.Mockito.lenient().when(orderDetails.selectList(any())).thenReturn(List.of(detail));
|
||||
return new Fixture(plan, task, line, order, detail, allocation(31L, 12L, 21L, picked), pallet(21L, 2001L));
|
||||
}
|
||||
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.OverseasContainerUnloadedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.client.EventBridgeClient;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
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 java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundUnloadCompletedOutboxRelayTest {
|
||||
@Mock private UnloadCompletedOutboxRepository repository;
|
||||
@Mock private EventBridgeClient client;
|
||||
private ObjectMapper objectMapper;
|
||||
private InboundUnloadCompletedOutboxRelay relay;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
|
||||
relay = new InboundUnloadCompletedOutboxRelay();
|
||||
ReflectionTestUtils.setField(relay, "repository", repository);
|
||||
ReflectionTestUtils.setField(relay, "client", client);
|
||||
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
|
||||
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
|
||||
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
|
||||
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void marksOutboxPublishedAfterDomesticBridgeAcknowledgesSameEvent() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
LastlegEventOutbox row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
|
||||
.thenReturn(ApiResponse.success(new EventBridgeClient.Acknowledgement(
|
||||
event.eventId(), "已接收", "2026-08-20T03:01:00")));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).published(eq(9001L), anyString(), any(LocalDateTime.class));
|
||||
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void releasesLeaseForRetryWhenBridgeDeliveryFails() throws Exception {
|
||||
EventEnvelope event = event();
|
||||
LastlegEventOutbox row = row(event);
|
||||
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
|
||||
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
|
||||
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
|
||||
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
|
||||
.thenThrow(new IllegalStateException("bridge unavailable"));
|
||||
|
||||
relay.relay();
|
||||
|
||||
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("bridge unavailable"));
|
||||
verify(repository, never()).published(anyLong(), anyString(), any());
|
||||
}
|
||||
|
||||
private LastlegEventOutbox row(EventEnvelope event) throws Exception {
|
||||
LastlegEventOutbox row = new LastlegEventOutbox();
|
||||
row.setId(9001L);
|
||||
row.setEventKey("OverseasContainerUnloaded:7001");
|
||||
row.setPayloadJson(objectMapper.writeValueAsString(event));
|
||||
return row;
|
||||
}
|
||||
|
||||
private EventEnvelope event() {
|
||||
var payload = objectMapper.valueToTree(new OverseasContainerUnloadedV1("1001", "MSCU1234567",
|
||||
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null));
|
||||
return new EventEnvelope("00000000-0000-4000-8000-000000000009",
|
||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1, Instant.parse("2026-08-20T03:00:01Z"),
|
||||
RegionCode.OVERSEAS, RegionCode.CN, "1", "CONTAINER_BOOKING", "1001", 7L, 1L,
|
||||
"trace-12345678", "correlation-12345678", null, payload,
|
||||
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
|
||||
}
|
||||
}
|
||||
+32
@@ -1,6 +1,8 @@
|
||||
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.InboundPlanSummary;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
@@ -22,6 +24,7 @@ import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -115,6 +118,35 @@ class InboundPlanServiceTest {
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesInboundPlansWithinCustomerScope() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(
|
||||
2, 20, "LIP", "RK", "PENDING_ASSIGNMENT", "WH-1", "ONT8", "ONT8",
|
||||
"NOT_APPOINTED", null, null);
|
||||
InboundPlanSummary row = new InboundPlanSummary(1L, "LIP1", 2L, "100", "Customer A",
|
||||
"FBA", "RK-1", "CONT-1", "WH-1", "LA Warehouse", "ONT8", "ONT8", "ONT8",
|
||||
"ONT8", 10, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", "NOT_APPOINTED", null, null);
|
||||
when(planMapper.countRows("100", query)).thenReturn(21L);
|
||||
when(planMapper.pageRows("100", query, 20L, 20L)).thenReturn(List.of(row));
|
||||
|
||||
var result = service.page("100", query);
|
||||
|
||||
assertEquals(21L, result.total());
|
||||
assertEquals(2L, result.pages());
|
||||
assertEquals("RK-1", result.records().get(0).inboundCode());
|
||||
verify(planMapper).pageRows("100", query, 20L, 20L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReversedInboundPlanDateRange() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null, null,
|
||||
null, null, null, LocalDateTime.of(2026, 8, 20, 0, 0),
|
||||
LocalDateTime.of(2026, 8, 19, 0, 0));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.page("100", query));
|
||||
verify(planMapper, never()).countRows(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "CONT-1", "RK-1",
|
||||
|
||||
Reference in New Issue
Block a user