This commit is contained in:
2026-08-20 18:06:07 +08:00
parent c9a05b108b
commit a38396ce3f
26 changed files with 532 additions and 21 deletions
@@ -23,10 +23,12 @@ class LastlegPlatformPermissionContractTest {
assertPermission(LastlegOrderController.class, "export", "platform:lastleg:order:export");
assertPermission(LastlegOrderController.class, "transferOne", "platform:lastleg:order:transfer");
assertPermission(InboundPlanController.class, "page", "platform:lastleg:inbound-plan:list");
assertPermission(InboundPlanController.class, "detail", "platform:lastleg:inbound-plan:list");
assertPermission(InboundPlanController.class, "fbaAppointmentDetail", "platform:lastleg:inbound-plan:list");
assertPermission(InboundPlanController.class, "convert", "platform:lastleg:inbound-plan:create");
assertPermission(InboundPlanController.class, "batchFbaAppointment", "platform:lastleg:inbound-plan:appointment");
assertPermission(InboundPlanController.class, "updateFbaAppointment", "platform:lastleg:inbound-plan:appointment");
assertPermission(RelabelApplicationController.class, "page", "platform:lastleg:relabel:list");
assertPermission(RelabelApplicationController.class, "detail", "platform:lastleg:relabel:list");
@@ -10,11 +10,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class InboundPlanSchemaContractTest {
@Test
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
void planIsUniqueByTenantAndSourceOrderWhileContainerMayHaveMultiplePlans() 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)"));
assertTrue(schema.contains("default 'pending_assignment'"));
assertTrue(schema.contains("required_package_count"));
assertTrue(schema.contains("planned_package_count"));
@@ -23,6 +24,7 @@ class InboundPlanSchemaContractTest {
assertTrue(schema.contains("create table if not exists ll_inbound_pda_operation"));
assertTrue(schema.contains("create table if not exists ll_inbound_pallet_merge"));
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
assertTrue(schema.contains("expected_loading_at datetime(3) not null comment '预计装车时间'"));
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
assertTrue(schema.contains("fba_warehouse_code"));
@@ -0,0 +1,68 @@
package com.baiyue.lastleg.inboundplan.controller;
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.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
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 org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Set;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InboundPlanControllerTest {
@Mock InboundPlanService service;
@Mock InboundFbaAppointmentService appointmentService;
private InboundPlanController controller;
@BeforeEach
void setUp() {
controller = new InboundPlanController();
ReflectionTestUtils.setField(controller, "service", service);
ReflectionTestUtils.setField(controller, "appointmentService", appointmentService);
}
@Test
void platformPageRequiresAndPassesExplicitCustomerId() {
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null,
null, null, null, null, null, null);
when(service.page("100", query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
controller.page(platform(), "100", query);
verify(service).page("100", query);
assertThatThrownBy(() -> controller.page(platform(), "", query))
.isInstanceOf(ResponseStatusException.class)
.satisfies(error -> org.assertj.core.api.Assertions.assertThat(
((ResponseStatusException) error).getStatusCode().value()).isEqualTo(400));
}
@Test
void platformAppointmentUpdateUsesExplicitCustomerScope() {
UpdateFbaAppointmentRequest request = new UpdateFbaAppointmentRequest("100",
LocalDateTime.of(2026, 8, 20, 10, 30), LocalDateTime.of(2026, 8, 21, 8, 0),
"PENDING", "dock");
controller.updateFbaAppointment(platform(), "APT-1", request);
verify(appointmentService).update("100", "APT-1", request);
}
private static AuthenticatedUser platform() {
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
Set.of("platform:lastleg:inbound-plan:list"));
}
}
@@ -1,12 +1,14 @@
package com.baiyue.lastleg.inboundplan.service;
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointment;
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -35,6 +37,7 @@ class InboundFbaAppointmentServiceTest {
private InboundFbaAppointmentService service;
private final LocalDateTime appointmentAt = LocalDateTime.of(2026, 8, 20, 10, 30);
private final LocalDateTime expectedLoadingAt = LocalDateTime.of(2026, 8, 21, 8, 0);
@BeforeEach
void setUp() {
@@ -57,6 +60,7 @@ class InboundFbaAppointmentServiceTest {
assertEquals("APT-1", result.appointmentNo());
assertEquals("ONT8", result.fbaWarehouseCode());
assertEquals(expectedLoadingAt, result.expectedLoadingAt());
assertEquals(2, result.selectedCount());
verify(appointmentMapper).insert(any(InboundFbaAppointment.class));
verify(itemMapper, org.mockito.Mockito.times(2)).insert(any(InboundFbaAppointmentItem.class));
@@ -114,8 +118,51 @@ class InboundFbaAppointmentServiceTest {
() -> service.batchCreate("100", "7", request(List.of(12L), "APT-1")));
}
@Test
void sameAppointmentNumberWithDifferentExpectedLoadingTimeConflicts() {
when(appointmentMapper.selectOne(any())).thenReturn(appointment(99L, "APT-1", "ONT8"));
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
item.setLineId(11L);
when(itemMapper.selectList(any())).thenReturn(List.of(item));
BatchFbaAppointmentRequest changed = new BatchFbaAppointmentRequest("100", List.of(11L), "APT-1",
appointmentAt, expectedLoadingAt.plusHours(1), "SUCCESS", "dock");
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
() -> service.batchCreate("100", "7", changed));
assertEquals(409, exception.getStatusCode().value());
}
@Test
void stringSnowflakeLineIdsDeserializeWithoutPrecisionLoss() throws Exception {
String json = "{\"customerId\":\"100\",\"lineIds\":[\"9007199254740993\"],"
+ "\"appointmentNo\":\"APT-1\",\"appointmentAt\":\"2026-08-20T10:30:00\","
+ "\"expectedLoadingAt\":\"2026-08-21T08:00:00\",\"appointmentStatus\":\"PENDING\"}";
BatchFbaAppointmentRequest request = new ObjectMapper().findAndRegisterModules()
.readValue(json, BatchFbaAppointmentRequest.class);
assertEquals(List.of(9007199254740993L), request.lineIds());
assertEquals(expectedLoadingAt, request.expectedLoadingAt());
}
@Test
void updatesAppointmentDetailsWithoutReplacingSelectedLines() {
InboundFbaAppointment existing = appointment(99L, "APT-1", "ONT8");
when(appointmentMapper.selectOne(any())).thenReturn(existing);
LocalDateTime loadingAt = expectedLoadingAt.plusHours(2);
var result = service.update("100", "APT-1", new UpdateFbaAppointmentRequest(
"100", appointmentAt.plusDays(1), loadingAt, "PENDING", "updated"));
assertEquals(loadingAt, result.expectedLoadingAt());
assertEquals("PENDING", result.appointmentStatus());
verify(appointmentMapper).updateById(existing);
verify(itemMapper, never()).insert(any(InboundFbaAppointmentItem.class));
}
private BatchFbaAppointmentRequest request(List<Long> lineIds, String number) {
return new BatchFbaAppointmentRequest("100", lineIds, number, appointmentAt, "SUCCESS", "dock");
return new BatchFbaAppointmentRequest("100", lineIds, number, appointmentAt, expectedLoadingAt,
"SUCCESS", "dock");
}
private FbaAppointmentLineSnapshot line(Long id, String inboundCode, String fba) {
@@ -128,6 +175,7 @@ class InboundFbaAppointmentServiceTest {
value.setCustomerId("100");
value.setAppointmentNo(number);
value.setAppointmentAt(appointmentAt);
value.setExpectedLoadingAt(expectedLoadingAt);
value.setAppointmentStatus("SUCCESS");
value.setAppointmentRemark("dock");
value.setFbaWarehouseCode(fba);
@@ -0,0 +1,83 @@
package com.baiyue.lastleg.inboundplan.service;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
import jakarta.validation.Validation;
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.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class InboundPlanPaginationTest {
@Mock InboundPlanQueryMapper queryMapper;
private InboundPlanService service;
@BeforeEach
void setUp() {
service = new InboundPlanService();
ReflectionTestUtils.setField(service, "queryMapper", queryMapper);
}
@Test
void queryDefaultsPageAndSizeAndRejectsOversizedPages() {
InboundPlanRequests.Query defaults = query(0, 0);
assertThat(defaults.page()).isEqualTo(1);
assertThat(defaults.size()).isEqualTo(20);
try (var validatorFactory = Validation.buildDefaultValidatorFactory()) {
var violations = validatorFactory.getValidator().validate(query(1, 201));
assertThat(violations).extracting(value -> value.getPropertyPath().toString()).contains("size");
}
}
@Test
void pagesAtLineGranularityAndPassesTenantAndAllFiltersToDatabase() {
InboundPlanRequests.Query query = new InboundPlanRequests.Query(2, 10, "LIP", "IN-1",
"PENDING_ASSIGNMENT", "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
LocalDateTime.of(2026, 8, 20, 10, 0));
when(queryMapper.count("100", query)).thenReturn(11L);
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
var result = service.page("100", query);
assertThat(result.records()).containsExactly(row);
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
assertThat(result.records().get(0).appointmentStatus()).isNull();
assertThat(result.pages()).isEqualTo(2);
verify(queryMapper).count("100", query);
verify(queryMapper).rows("100", query, 10, 10);
}
@Test
void mapperSqlProjectsLatestAppointmentAndTenantScopedDispatchState() throws Exception {
String sql = String.join("", InboundPlanQueryMapper.class
.getMethod("rows", String.class, InboundPlanRequests.Query.class, long.class, long.class)
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
assertThat(sql).contains("di.customer_id=#{customerId} AND di.order_id=l.order_id");
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.warehouse",
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
assertThat(sql).contains("LIMIT #{limit} OFFSET #{offset}");
}
private static InboundPlanRequests.Query query(long page, long size) {
return new InboundPlanRequests.Query(page, size, null, null, null, null, null,
null, null, null, null);
}
}
@@ -18,7 +18,7 @@ class InventoryQueryControllerTest {
private final InventoryQueryService service = mock(InventoryQueryService.class);
private final InventoryQueryController controller = controller();
private final InventoryQueryRequests.BalanceQuery query = new InventoryQueryRequests.BalanceQuery(
1, 20, null, null, null, null, null, null, null, null);
1, 20, null, null, null, null, null, null, null, null, null, null, null, null);
@Test
void merchantCannotOverrideCustomerScope() {
@@ -6,6 +6,8 @@ import com.baiyue.lastleg.inventory.query.dto.InventoryDashboardResponse;
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
import com.baiyue.lastleg.inventory.query.model.InventoryLocationFact;
import com.baiyue.lastleg.inventory.query.model.InventorySkuFact;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
@@ -58,9 +60,21 @@ class InventoryQueryServiceTest {
balance.setCustomerId("100");
when(mapper.countBalances("100", query)).thenReturn(1L);
when(mapper.balances("100", query, 0, 20)).thenReturn(List.of(balance));
InventorySkuFact sku = new InventorySkuFact();
sku.setBalanceId(1L);
sku.setSkuCode("SKU-1");
sku.setProductName("测试产品");
InventoryLocationFact location = new InventoryLocationFact();
location.setBalanceId(1L);
location.setWarehouseLocationCode(null);
when(mapper.skuFacts("100", List.of(1L))).thenReturn(List.of(sku));
when(mapper.locationFacts("100", List.of(1L))).thenReturn(List.of(location));
service.balances("100", query);
var result = service.balances("100", query);
assertEquals(List.of("SKU-1"), result.records().get(0).skuCodes());
assertEquals(List.of("测试产品"), result.records().get(0).productNames());
assertEquals(List.of(), result.records().get(0).warehouseLocationCodes());
verify(mapper).countBalances("100", query);
verify(mapper).balances("100", query, 0, 20);
}
@@ -106,6 +120,7 @@ class InventoryQueryServiceTest {
void reversedLedgerDateRangeIsRejected() {
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
1, 20, null, null, null, null, null, null, null, null, null,
null, null,
java.time.LocalDateTime.of(2026, 8, 18, 0, 0),
java.time.LocalDateTime.of(2026, 8, 17, 0, 0));
@@ -120,12 +135,12 @@ class InventoryQueryServiceTest {
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
assertTrue(csv.contains("inboundOrderNo,inboundCode,totalPackages"));
assertFalse(csv.contains("skuCode"));
assertTrue(csv.contains("inboundOrderNo,inboundCode,skuCodes"));
assertFalse(csv.contains("Quantity"));
}
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null, status);
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null,
null, null, null, null, status);
}
}