优化
This commit is contained in:
+18
@@ -50,6 +50,24 @@ class LastlegOrderControllerTest {
|
||||
verify(service).page(eq("200"), eq(query));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformOperatorCanQueryCrossCustomerContainerSummaries() {
|
||||
var summaryQuery = new LastlegOrderRequests.PendingContainerQuery(1, 20, "CONT-", null, null);
|
||||
|
||||
controller.containerSummaries(user(0L, "PLATFORM"), summaryQuery);
|
||||
|
||||
verify(service).pendingContainerPage(summaryQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerSummariesRejectMerchantIdentity() {
|
||||
var summaryQuery = new LastlegOrderRequests.PendingContainerQuery(1, 20, null, null, null);
|
||||
|
||||
assertThrows(ResponseStatusException.class,
|
||||
() -> controller.containerSummaries(user(100L, "MERCHANT"), summaryQuery));
|
||||
verify(service, never()).pendingContainerPage(summaryQuery);
|
||||
}
|
||||
|
||||
@Test
|
||||
void iamSecurityEventsAreRejectedBeforeLoadingCompletedConsumer() {
|
||||
EventEnvelope event = mock(EventEnvelope.class);
|
||||
|
||||
+18
@@ -4,6 +4,7 @@ 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.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
@@ -87,6 +88,23 @@ class InboundPlanControllerTest {
|
||||
verify(service).convert("100", 2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformContainerConversionUsesStableBookingIdentity() {
|
||||
controller.convertContainer(platform(), new ConvertContainerInboundPlanRequest(" BOOKING-1 "));
|
||||
|
||||
verify(service).convertContainer(" BOOKING-1 ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantCannotConvertAWholeContainer() {
|
||||
AuthenticatedUser customer = new AuthenticatedUser(8L, 100L, 100L, 9L, "customer", "MERCHANT", "WEB", Set.of());
|
||||
|
||||
assertThatThrownBy(() -> controller.convertContainer(customer,
|
||||
new ConvertContainerInboundPlanRequest("BOOKING-1")))
|
||||
.isInstanceOf(ResponseStatusException.class);
|
||||
verify(service, never()).convertContainer(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
private static AuthenticatedUser platform() {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||
Set.of("platform:overseas-warehouse:inbound-order:list"));
|
||||
|
||||
+87
@@ -191,6 +191,93 @@ class InboundPlanServiceTest {
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void containerConversionPrevalidatesEveryOrderBeforeWritingIndependentPlans() throws Exception {
|
||||
assertNotNull(InboundPlanService.class.getMethod("convertContainer", String.class)
|
||||
.getAnnotation(Transactional.class));
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "DESTINATION_ASSIGNED")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order("100", 1L, "RK-1", "WH-A")));
|
||||
when(orderMapper.lockOrder("200", 2L)).thenReturn(List.of(order("200", 2L, "RK-2", "WH-B")));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
|
||||
var result = service.convertContainer(" BOOKING-1 ");
|
||||
|
||||
assertEquals(2, result.size());
|
||||
assertEquals(1L, result.get(0).sourceOrderId());
|
||||
assertEquals(2L, result.get(1).sourceOrderId());
|
||||
verify(planMapper, times(2)).insert(any(InboundPlan.class));
|
||||
verify(orderMapper).lockContainerOrders("BOOKING-1");
|
||||
verify(orderMapper).lockOrder("100", 1L);
|
||||
verify(orderMapper).lockOrder("200", 2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidLaterOrderCausesNoWritesBecauseWholeContainerIsPrevalidated() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "PENDING_ASSIGNMENT"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "CONT-1", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
verify(orderMapper, never()).markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any());
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void completeContainerReplayReturnsStableExistingPlansWithoutWriting() {
|
||||
var refs = List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("200", 2L, "INBOUND_COMPLETED"));
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(refs);
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
when(planMapper.findBySourceOrder("200", 2L)).thenReturn(existingPlan("200", 2L, "LIP-2"));
|
||||
|
||||
var result = service.convertContainer("BOOKING-1");
|
||||
|
||||
assertEquals(List.of("LIP-1", "LIP-2"), result.stream().map(value -> value.planNo()).toList());
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void partialExistingPlansAreRejectedBeforeAnyWrite() {
|
||||
when(orderMapper.lockContainerOrders("BOOKING-1")).thenReturn(List.of(
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 1L, "INBOUND_PLANNED"),
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT")));
|
||||
when(planMapper.findBySourceOrder("100", 1L)).thenReturn(existingPlan("100", 1L, "LIP-1"));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
verify(orderMapper, never()).lockOrder(any(), any());
|
||||
verify(planMapper, never()).insert(any(InboundPlan.class));
|
||||
}
|
||||
|
||||
private InboundPlan existingPlan(String customerId, long orderId, String planNo) {
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(orderId + 100);
|
||||
plan.setPlanNo(planNo);
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(orderId);
|
||||
plan.setContainerNo("CONT-1");
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(1);
|
||||
plan.setPalletCount(1);
|
||||
plan.setTotalVolumeCbm(BigDecimal.ONE);
|
||||
return plan;
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, customerId, "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "CONT-1", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
@@ -19,6 +20,8 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -133,6 +136,33 @@ class LastlegOrderServiceTest {
|
||||
assertEquals(25, page.getValue().getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
void pendingContainerPageUsesServerSideAggregatePagination() {
|
||||
var query = new LastlegOrderRequests.PendingContainerQuery(2, 10, "CONT-", null, null);
|
||||
var row = new PendingContainerSummary("BOOKING-1", "CONT-1", 15, 15, 3,
|
||||
150, 0, new BigDecimal("12.5000"), true, null);
|
||||
when(orders.countPendingContainers(query)).thenReturn(11L);
|
||||
when(orders.pagePendingContainers(query, 10, 10)).thenReturn(List.of(row));
|
||||
|
||||
var result = service.pendingContainerPage(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 pendingContainerPageRejectsReversedDateRangeBeforeQuerying() {
|
||||
var query = new LastlegOrderRequests.PendingContainerQuery(1, 20, null,
|
||||
LocalDateTime.of(2026, 8, 20, 0, 0), LocalDateTime.of(2026, 8, 19, 0, 0));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.pendingContainerPage(query));
|
||||
|
||||
verify(orders, never()).countPendingContainers(any());
|
||||
}
|
||||
|
||||
private static LastlegOrder order(long id, String customerId, String status, String inboundOrderNo) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(id);
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class PendingContainerSummaryContractTest {
|
||||
@Test
|
||||
void responseFieldsAndServerAggregateSqlStayFrozen() throws Exception {
|
||||
assertEquals(List.of("sourceContainerBookingId", "containerNo", "orderCount", "inboundCodeCount",
|
||||
"customerCount", "totalPackageCount", "totalPalletCount", "totalVolumeCbm", "eligible",
|
||||
"ineligibleReason"),
|
||||
Arrays.stream(PendingContainerSummary.class.getRecordComponents()).map(value -> value.getName()).toList());
|
||||
|
||||
Select rows = LastlegOrderMapper.class.getMethod("pagePendingContainers",
|
||||
com.baiyue.lastleg.dto.LastlegOrderRequests.PendingContainerQuery.class, long.class, long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join("", rows.value()).toLowerCase();
|
||||
assertTrue(sql.contains("group by o.source_container_booking_id"));
|
||||
assertTrue(sql.contains("pending_assignment"));
|
||||
assertTrue(sql.contains("destination_assigned"));
|
||||
assertTrue(sql.contains("limit #{limit} offset #{offset}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void openApiAndSchemaExposeStableContainerIdentity() throws Exception {
|
||||
String openApi = Files.readString(Path.of("src/main/resources/openapi/overseas-business-v1.yaml"),
|
||||
StandardCharsets.UTF_8);
|
||||
String schema = Files.readString(Path.of("src/main/resources/db/lastleg.sql"), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(openApi.contains("/api/platform/v1/last-leg/orders/container-summaries:"));
|
||||
assertTrue(openApi.contains("PendingContainerSummary:"));
|
||||
assertTrue(openApi.contains("sourceContainerBookingId:"));
|
||||
assertTrue(schema.contains("idx_ll_order_pending_container(source_container_booking_id,current_status,created_at)"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user