This commit is contained in:
2026-08-20 18:22:20 +08:00
23 changed files with 401 additions and 48 deletions
@@ -26,6 +26,7 @@ class InboundPlanSchemaContractTest {
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("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
assertTrue(schema.contains("expected_loading_at"));
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
assertTrue(schema.contains("fba_warehouse_code"));
assertFalse(schema.contains("cabinet_no"));
@@ -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, 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:overseas-warehouse:inbound-order: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,84 @@
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", null, null, "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.overseasWarehouseId",
"q.destinationWarehouseCode", "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, null, null);
}
}
@@ -2,7 +2,6 @@ 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;
@@ -118,33 +117,13 @@ 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),
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
@@ -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, null, null);
1, 20, null, null, null, null, null, null, null, null, null, null, null, null);
@Test
void merchantCannotOverrideCustomerScope() {
@@ -105,7 +105,7 @@ class InventoryQueryServiceTest {
@Test
void reversedLedgerDateRangeIsRejected() {
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
1, 20, null, null, null, null, null, null, null, null, null,
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));
@@ -125,6 +125,6 @@ class InventoryQueryServiceTest {
}
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, 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);
}
}