Merge remote-tracking branch 'origin/test' into dev

# Conflicts:
#	firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java
#	firstleg/src/test/java/com/baiyue/firstleg/service/CustomerDimensionConfigServiceTest.java
#	scripts/admin-test-env-modules/inbound-measurement.mjs
This commit is contained in:
2026-09-04 10:37:12 +08:00
17 changed files with 695 additions and 233 deletions
@@ -21,6 +21,8 @@ class PlatformSmartLoadingControllerContractTest {
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
assertThat(yaml).contains("/api/platform/v1/first-leg/smart-loading/containers:")
.contains("SmartLoadingWorkbenchContainerPageEnvelope:")
.contains("capacityCbm: {type: number, nullable: true, description: 柜型基础容积;装柜上限按正式规则计算}")
.contains("maxWeightKg: {type: number, nullable: true, description: 柜型基础载重;装柜上限按正式规则计算}")
.contains("只返回 WAIT_LOADING、EDITING 或 LOADING");
}
@Test void openApiFreezesGlobalPagedInboundCandidates() throws Exception {
@@ -34,10 +36,15 @@ class PlatformSmartLoadingControllerContractTest {
.contains("name: warehouseId")
.contains("name: createdStart")
.contains("name: createdEnd")
.contains("name: orderStatuses")
.contains("maxItems: 4")
.contains("PENDING_REVIEW, WAIT_MEASURE, WAIT_SPLIT, LOADING")
.contains("SmartLoadingInboundCandidatePageEnvelope")
.doesNotContain("name: orderStatus,")
.doesNotContain("name: expectedArrivalStart")
.doesNotContain("name: expectedArrivalEnd")
.doesNotContain("name: bookingId");
assertThat(yaml).contains("canAssign: {type: boolean, description: 当前是否可加入柜次");
}
@Test void everyMutationDeclaresDedicatedPermission() throws Exception {
authority("assign","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignRequest.class);
@@ -75,6 +82,7 @@ class PlatformSmartLoadingControllerContractTest {
.contains("成功响应中的 version 是递增后的 planVersion")
.contains("items[].version 是递增后的 itemVersion")
.contains("数量调整或动态增量不会覆盖已有 warehouseRemark")
.contains("柜内总容积和总重量分别允许达到柜型额定值的150%")
.contains("canAssign: {type: boolean}")
.contains("canEdit: {type: boolean}")
.contains("pickedPackageCount: {type: integer, minimum: 0}")
@@ -35,7 +35,7 @@ class CustomerDimensionConfigSqlContractTest {
.contains("inner_width_cm DECIMAL(9,3) NOT NULL")
.contains("inner_height_cm DECIMAL(9,3) NOT NULL")
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
.contains("default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ADD'")
.contains("default_calculation_mode VARCHAR(16) NOT NULL DEFAULT 'ACTUAL'")
.contains("initialization_key VARCHAR(128) NOT NULL")
.contains("created_by BIGINT UNSIGNED NOT NULL")
.contains("updated_by BIGINT UNSIGNED NOT NULL")
@@ -86,10 +86,12 @@ class SmartLoadingCandidateMapperContractTest {
assertThat(unitSql)
.contains("#{query.warehouseId} IS NULL OR o.warehouse_id=#{query.warehouseId}")
.contains("o.channel_code IN")
.contains("o.process_status='WAIT_SPLIT'")
.contains("query.orderStatuses")
.contains("status.name() == 'WAIT_MEASURE'")
.contains("status.name() == 'WAIT_SPLIT'")
.contains("status.name() == 'LOADING'")
.contains("current_group.id groupId")
.contains("group_member_count.group_id=current_group.id")
.contains("availablePackageCount=current_member.full_package_count")
.doesNotContain("fl_container_booking")
.doesNotContain("bookingId");
assertThat(detailSql)
@@ -97,6 +99,8 @@ class SmartLoadingCandidateMapperContractTest {
.contains("current_group.id=#{unit.groupId}")
.contains("o.tenant_id=#{unit.accountId}")
.contains("o.id=#{unit.inboundId}")
.contains("'WAIT_SPLIT' orderStatusSnapshot")
.contains("THEN 'LOADING' ELSE 'WAIT_MEASURE' END orderStatusSnapshot")
.doesNotContain("fl_container_booking");
}
@@ -108,8 +112,8 @@ class SmartLoadingCandidateMapperContractTest {
assertThat(sql)
.contains("o.created_at createdAt")
.contains("DATE(MIN(eligible.createdAt))>=#{query.createdStart}")
.contains("DATE(MIN(eligible.createdAt))<=#{query.createdEnd}")
.contains("MIN(eligible.createdAt)>=#{query.createdStart}")
.contains("MIN(eligible.createdAt)<DATE_ADD(#{query.createdEnd},INTERVAL 1 DAY)")
.doesNotContain("query.expectedArrivalStart", "query.expectedArrivalEnd");
}
@@ -141,13 +145,17 @@ class SmartLoadingCandidateMapperContractTest {
String unitScript = String.join(" ", units.getAnnotation(Select.class).value());
var query = new InboundCandidateQuery(List.of("MATSON_CLX"), 100L, 9L, "IN-",
java.time.LocalDate.parse("2026-09-01"), java.time.LocalDate.parse("2026-09-30"),
null, 1, 20);
List.of(com.baiyue.firstleg.dto.SmartLoadingDtos.CandidateOrderStatus.WAIT_MEASURE,
com.baiyue.firstleg.dto.SmartLoadingDtos.CandidateOrderStatus.WAIT_SPLIT), 1, 20);
String renderedUnits = driver.createSqlSource(configuration, unitScript, Map.class)
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
assertThat(renderedUnits)
.contains("o.channel_code IN")
.contains("DATE(MIN(eligible.createdAt))>=", "DATE(MIN(eligible.createdAt))<=")
.doesNotContain("DATE(MIN(eligible.expectedArrivalAt))");
.contains("MIN(eligible.createdAt)>=", "MIN(eligible.createdAt)<DATE_ADD")
.contains("eligible.candidateStatus='WAIT_MEASURE'")
.contains("eligible.candidateStatus='WAIT_SPLIT'")
.contains("MAX(CASE WHEN LOWER(eligible.inboundCode)")
.contains("MAX(CASE WHEN eligible.accountId=");
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
"globalCandidatesByUnits", List.class);
@@ -163,7 +171,7 @@ class SmartLoadingCandidateMapperContractTest {
@Test
void workbenchContainerQueryExcludesCompletedAndMatchesRelatedInboundFacts() throws Exception {
Method list = SmartLoadingCandidateMapper.class.getDeclaredMethod("workbenchContainers",
Page.class, WorkbenchContainerQuery.class, Set.class, String.class);
Page.class, WorkbenchContainerQuery.class, Set.class);
String sql = String.join(" ", list.getAnnotation(Select.class).value());
assertThat(sql)
@@ -173,7 +181,7 @@ class SmartLoadingCandidateMapperContractTest {
.contains("fl_inbound_order filter_inbound")
.contains("filter_item.inbound_code LIKE")
.contains("filter_inbound.created_at")
.contains("filter_inbound.process_status='LOADING'")
.doesNotContain("query.orderStatus", "orderStatuses")
.doesNotContain("query.expectedArrivalStart", "query.expectedArrivalEnd")
.doesNotContain("p.plan_status IN ('EDITING','LOADING','COMPLETED')");
}
@@ -7,6 +7,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.client.OrderCargoClient;
import com.baiyue.firstleg.client.PendingReviewOrderClient;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignInboundRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AssignPendingReviewRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.AdjustRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate;
@@ -14,6 +15,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.CandidateOrderStatus;
import com.baiyue.firstleg.dto.SmartLoadingDtos.DeleteRequest;
import com.baiyue.firstleg.dto.SmartLoadingDtos.GroupMemberCandidate;
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidate;
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainer;
import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
import com.baiyue.firstleg.dto.SmartLoadingDtos.Plan;
@@ -79,6 +81,7 @@ import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.atLeastOnce;
@SpringJUnitConfig(SmartLoadingServiceTest.Config.class)
class SmartLoadingServiceTest {
@@ -192,7 +195,8 @@ class SmartLoadingServiceTest {
Candidate max = withChannel(candidate(19L, 11L, 21L, 31L, 6),
"MATSON_MAX_TRUCK", "美森加班海卡");
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("MATSON_CLX"), null, null, null, null, null, null, 1, 20);
List.of("MATSON_CLX"), null, null, null, null, null,
List.of(CandidateOrderStatus.WAIT_SPLIT), 1, 20);
CandidateUnit unit = new CandidateUnit(null, 9L, 10L, null, 10L);
when(repository.globalCandidateUnits(eq(query), anySet()))
.thenReturn(new PageResult<>(List.of(unit), 1, 20, 1, 1));
@@ -212,19 +216,104 @@ class SmartLoadingServiceTest {
verify(repository, never()).booking(anyLong());
}
@Test
void combinesAllUnfinishedStatusesBeforeReturningTheRequestedPage() {
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("EXX"), null, null, null, null, null, List.of(), 1, 20);
var pending = new PendingReviewOrderClient.Row(31L, "O31", "I31", 9L, "客户",
100L, "EXX_TRUCK", "EXX海卡", 3, LocalDateTime.parse("2026-09-01T09:00:00"),
"PENDING_REVIEW");
when(pendingReviewOrders.page(anyString(), anyList(), isNull(), isNull(), isNull(),
isNull(), isNull(), anyLong(), anyLong())).thenAnswer(invocation -> {
long page = invocation.getArgument(7);
long size = invocation.getArgument(8);
return ApiResponse.success(new PageResult<>(page == 1 ? List.of(pending) : List.of(),
page, size, 1, 1));
});
Candidate measured = withStatus(candidate(9L, 10L, 20L, 30L, 4), "WAIT_SPLIT", false);
CandidateUnit unit = new CandidateUnit(null, 9L, 10L, null, 10L);
when(repository.globalCandidateUnits(any(), anySet())).thenAnswer(invocation -> {
InboundCandidateQuery requested = invocation.getArgument(0);
return new PageResult<>(requested.page() == 1 ? List.of(unit) : List.of(),
requested.page(), requested.size(), 1, 1);
});
when(repository.globalCandidatesByUnits(List.of(unit))).thenReturn(List.of(measured));
var result = service.inboundCandidates(query);
assertThat(result.total()).isEqualTo(2);
assertThat(result.records()).extracting(InboundCandidate::orderStatusSnapshot)
.containsExactly("PENDING_REVIEW", "WAIT_SPLIT");
assertThat(result.records()).extracting(InboundCandidate::canAssign)
.containsExactly(true, true);
ArgumentCaptor<InboundCandidateQuery> firstlegQueries =
ArgumentCaptor.forClass(InboundCandidateQuery.class);
verify(repository, atLeastOnce()).globalCandidateUnits(firstlegQueries.capture(), anySet());
assertThat(firstlegQueries.getAllValues()).allSatisfy(requested ->
assertThat(requested.orderStatuses()).containsExactly(
CandidateOrderStatus.WAIT_MEASURE, CandidateOrderStatus.WAIT_SPLIT,
CandidateOrderStatus.LOADING));
}
@Test
void statusDoesNotBlockAssignmentButAnActiveContainerOccupationDoes() {
InboundCandidateQuery query = new InboundCandidateQuery(List.of("EXX"), null, null, null,
null, null, List.of(CandidateOrderStatus.WAIT_MEASURE, CandidateOrderStatus.LOADING), 1, 20);
Candidate waiting = withStatus(candidate(9L, 10L, 20L, 30L, 4), "WAIT_MEASURE", false);
Candidate loading = withStatus(candidate(19L, 11L, 21L, 31L, 0), "LOADING", true);
List<CandidateUnit> units = List.of(new CandidateUnit(null, 9L, 10L, null, 10L),
new CandidateUnit(null, 19L, 11L, null, 11L));
when(repository.globalCandidateUnits(eq(query), anySet()))
.thenReturn(new PageResult<>(units, 1, 20, 2, 1));
when(repository.globalCandidatesByUnits(units)).thenReturn(List.of(waiting, loading));
var result = service.inboundCandidates(query);
assertThat(result.records()).filteredOn(candidate -> "WAIT_MEASURE".equals(candidate.orderStatusSnapshot()))
.singleElement().satisfies(candidate -> assertThat(candidate.canAssign()).isTrue());
assertThat(result.records()).filteredOn(candidate -> "LOADING".equals(candidate.orderStatusSnapshot()))
.singleElement().satisfies(candidate -> {
assertThat(candidate.canAssign()).isFalse();
assertThat(candidate.blockReason()).isEqualTo("该入仓码已被有效柜次占用");
});
}
@Test
void keepsAnOccupiedLoadingGroupAsOneNonAssignablePagingUnit() {
InboundCandidateQuery query = new InboundCandidateQuery(List.of("EXX"), null, null, null,
null, null, List.of(CandidateOrderStatus.LOADING), 1, 20);
Candidate first = withStatus(withGroup(candidate(9L, 10L, 20L, 30L, 0), 2, 10),
"LOADING", true);
Candidate second = withStatus(withGroup(candidate(19L, 11L, 21L, 31L, 0), 2, 10),
"LOADING", true);
CandidateUnit unit = new CandidateUnit(700L, null, null, null, 10L);
when(repository.globalCandidateUnits(eq(query), anySet()))
.thenReturn(new PageResult<>(List.of(unit), 1, 20, 1, 1));
when(repository.globalCandidatesByUnits(List.of(unit))).thenReturn(List.of(first, second));
var result = service.inboundCandidates(query);
assertThat(result.records()).singleElement().satisfies(candidate -> {
assertThat(candidate.coLoadGroupId()).isEqualTo(700L);
assertThat(candidate.groupMembers()).hasSize(2);
assertThat(candidate.canAssign()).isFalse();
assertThat(candidate.orderStatusSnapshot()).isEqualTo("LOADING");
});
}
@Test
void pagesPendingReviewOrdersFromTheOrderOwnedReadModelAsAssignableRows() {
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("EXX"), 193000000000200002L, null, "EXXHK2609020361",
LocalDate.parse("2026-09-02"), LocalDate.parse("2026-09-02"),
CandidateOrderStatus.PENDING_REVIEW, 1, 20);
List.of(CandidateOrderStatus.PENDING_REVIEW), 1, 20);
var row = new PendingReviewOrderClient.Row(2095052002489335810L, "FL-361",
"EXXHK2609020361", 2092797167699816450L, "客户", 193000000000200002L,
"EXX_TRUCK", "EXX海卡", 100, LocalDateTime.parse("2026-09-02T15:31:36"),
"PENDING_REVIEW");
when(pendingReviewOrders.page(anyString(), anyList(), eq(193000000000200002L),
isNull(), eq("EXXHK2609020361"), eq(LocalDate.parse("2026-09-02")),
eq(LocalDate.parse("2026-09-02")), eq(1L), eq(20L)))
isNull(), eq("EXXHK2609020361"), eq("2026-09-02"),
eq("2026-09-02"), eq(1L), eq(20L)))
.thenReturn(ApiResponse.success(new PageResult<>(List.of(row), 1, 20, 1, 1)));
var result = service.inboundCandidates(query);
@@ -234,6 +323,7 @@ class SmartLoadingServiceTest {
assertThat(candidate.inboundId()).isNull();
assertThat(candidate.inboundCode()).isEqualTo("EXXHK2609020361");
assertThat(candidate.orderStatusSnapshot()).isEqualTo("PENDING_REVIEW");
assertThat(candidate.canAssign()).isTrue();
assertThat(candidate.blockReason()).isNull();
assertThat(candidate.availablePackageCount()).isEqualTo(100);
});
@@ -290,11 +380,55 @@ class SmartLoadingServiceTest {
verify(loadingModelBridge).bindIncrementalAllocation(1L, customerId, 10L, 7L, "u", "pending-1");
}
@Test
void assignsWaitMeasureInboundUsingDeclaredFactsWithoutTreatingStatusAsAGate() {
long customerId = 9L;
long inboundId = 10L;
long orderId = 30L;
InboundOrderEntity projection = new InboundOrderEntity();
projection.setId(inboundId);
projection.setTenantId(customerId);
projection.setOrderId(orderId);
projection.setInboundCode("I10");
projection.setWarehouseId(100L);
projection.setChannelCode("FBA");
projection.setProcessStatus("WAIT_MEASURE");
var snapshot = new com.baiyue.firstleg.dto.OrderApprovedInboundCommand(
customerId, orderId, "O30", "I10", customerId, "客户", 100L, "义乌仓",
null, null, null, "FIRST_LEG", "FBA", "FBA渠道", null, null, null,
null, null, false, "FBA", 200L, "FBA1", "FBA仓", null, null, null, 5);
var label = new OrderCargoClient.CargoLabelBinding(20L, orderId, 1, "货物", 5,
null, null, null);
var cargo = new OrderCargoClient.CargoBinding(20L, orderId, 1, "货物", 5,
new BigDecimal("50"), new BigDecimal("2.5"));
when(repository.inbound(inboundId)).thenReturn(projection);
when(repository.inboundForUpdate(customerId, inboundId)).thenReturn(projection);
when(repository.plan(1L)).thenReturn(plan("EDITING", 0), plan("EDITING", 1));
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of());
when(repository.touch(1L, 0, 7L, "u")).thenReturn(1);
when(cargoClient.inboundSnapshot(anyString(), eq(customerId), eq(orderId)))
.thenReturn(ApiResponse.success(snapshot));
when(cargoClient.listForCartonLabels(anyString(), eq(customerId), eq(orderId)))
.thenReturn(ApiResponse.success(List.of(label)));
when(cargoClient.detail(anyString(), eq(customerId), eq(orderId), eq(20L)))
.thenReturn(ApiResponse.success(cargo));
Plan result = service.assignInbound(1L,
new AssignInboundRequest(inboundId, 5, 0, "wait-measure-1"), 7L, "u");
assertThat(result.version()).isEqualTo(1);
verify(repository).insert(any(LoadingPlanItemEntity.class));
verify(splitPreloadService).ensurePendingForPreAllocation(customerId, inboundId, 7L, "u");
verify(loadingModelBridge).bindIncrementalAllocation(1L, customerId, inboundId,
7L, "u", "wait-measure-1");
}
@Test
void failsClosedWhenPendingReviewOrderSourceIsUnavailable() {
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("EXX"), null, null, null, null, null,
CandidateOrderStatus.PENDING_REVIEW, 1, 20);
List.of(CandidateOrderStatus.PENDING_REVIEW), 1, 20);
when(pendingReviewOrders.page(anyString(), anyList(), isNull(), isNull(), isNull(),
isNull(), isNull(), eq(1L), eq(20L))).thenThrow(new RuntimeException("offline"));
@@ -307,10 +441,11 @@ class SmartLoadingServiceTest {
void pagesWorkbenchContainersWithTheSameGlobalFilters() {
WorkbenchContainer container = new WorkbenchContainer(99L, "CB-1", "CAB-1", null,
100L, "义乌仓", "MATSON_CLX_TRUCK", "美森正班海卡", "40HC",
new BigDecimal("68"), new BigDecimal("28000"),
1L, "LP-1", "EDITING", 2);
WorkbenchContainerQuery query = new WorkbenchContainerQuery(List.of("MATSON_CLX"), 100L,
9L, "IN-10", LocalDate.parse("2026-09-01"), LocalDate.parse("2026-09-30"),
CandidateOrderStatus.WAIT_SPLIT, 1, 20);
1, 20);
when(repository.workbenchContainers(eq(query), any())).thenReturn(
new com.baiyue.common.core.api.PageResult<>(List.of(container), 1, 20, 1, 1));
@@ -332,7 +467,7 @@ class SmartLoadingServiceTest {
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00"), 2, 10);
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("MATSON_CLX"), 100L, null, null, LocalDate.parse("2026-09-01"),
LocalDate.parse("2026-09-01"), null, 1, 1);
LocalDate.parse("2026-09-01"), List.of(CandidateOrderStatus.WAIT_SPLIT), 1, 1);
CandidateUnit unit = new CandidateUnit(700L, null, null,
LocalDateTime.parse("2026-09-01T09:00:00"), 10L);
when(repository.globalCandidateUnits(eq(query), anySet()))
@@ -353,7 +488,8 @@ class SmartLoadingServiceTest {
@Test
void excludesIncompleteColoadGroupInsteadOfPublishingPartialMember() {
InboundCandidateQuery query = new InboundCandidateQuery(
List.of("MATSON_CLX"), 100L, null, null, null, null, null, 1, 20);
List.of("MATSON_CLX"), 100L, null, null, null, null,
List.of(CandidateOrderStatus.WAIT_SPLIT), 1, 20);
when(repository.globalCandidateUnits(eq(query), anySet()))
.thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
@@ -450,6 +586,7 @@ class SmartLoadingServiceTest {
when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get()));
when(repository.planByBooking(99L)).thenAnswer(invocation -> plan("EDITING", version.get()));
when(repository.booking(99L)).thenReturn(booking());
when(repository.inbound(10L)).thenReturn(inbound(9L, 10L));
when(repository.candidates(100L)).thenReturn(List.of(first, second));
when(repository.candidate(1L, 100L, 10L, 20L)).thenReturn(first);
when(repository.candidate(1L, 100L, 11L, 21L)).thenReturn(second);
@@ -493,6 +630,7 @@ class SmartLoadingServiceTest {
org.mockito.ArgumentMatchers.startsWith("LOCK-"), eq(7L), eq("u"));
verify(loadingModelBridge, times(2)).bindIncrementalAllocation(eq(1L), anyLong(), anyLong(),
eq(7L), eq("u"), anyString());
verify(repository, times(1)).candidates(100L);
}
@Test
@@ -504,7 +642,7 @@ class SmartLoadingServiceTest {
new Member(9L, 30L, 10L, "IN-10", 5), new Member(19L, 31L, 11L, "IN-11", 5),
new Member(29L, 32L, 12L, "IN-12", 5));
ContainerBookingEntity small = booking();
small.setCapacityCbm(new BigDecimal("5"));
small.setCapacityCbm(new BigDecimal("4.9"));
LoadingPlanEntity plan = plan("EDITING", 0);
when(repository.plan(1L)).thenReturn(plan);
when(repository.planByBooking(99L)).thenReturn(plan);
@@ -530,7 +668,7 @@ class SmartLoadingServiceTest {
GroupSnapshot group = group("ACTIVE", null,
new Member(9L, 31L, 11L, "IN-11", 4), new Member(9L, 32L, 12L, "IN-12", 4));
ContainerBookingEntity small = booking();
small.setCapacityCbm(new BigDecimal("5"));
small.setCapacityCbm(new BigDecimal("3.9"));
AtomicInteger version = new AtomicInteger();
List<LoadingPlanItemEntity> saved = new ArrayList<>();
when(repository.plan(1L)).thenAnswer(invocation -> plan("EDITING", version.get()));
@@ -582,10 +720,11 @@ class SmartLoadingServiceTest {
GroupSnapshot group = group("ACTIVE", null,
new Member(9L, 31L, 11L, "IN-11", 4), new Member(9L, 32L, 12L, "IN-12", 4));
ContainerBookingEntity small = booking();
small.setCapacityCbm(new BigDecimal("3"));
small.setCapacityCbm(new BigDecimal("2.6"));
when(repository.plan(1L)).thenReturn(plan("EDITING", 0));
when(repository.planByBooking(99L)).thenReturn(plan("EDITING", 0));
when(repository.booking(99L)).thenReturn(small);
when(repository.inbound(11L)).thenReturn(inbound(9L, 11L));
when(repository.items(1L)).thenReturn(List.of());
when(repository.candidates(100L)).thenReturn(List.of(first, second));
when(repository.activeItemsByInbound(anyLong(), anyLong())).thenReturn(List.of());
@@ -597,7 +736,7 @@ class SmartLoadingServiceTest {
11L, 0, "manual-group-too-large"), 7L, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
assertThat(error).hasMessageContaining("110%上限");
assertThat(error).hasMessageContaining("150%上限");
verify(coloadGroups, never()).lockWholeGroupForAssignment(anyLong(), anyLong(), anyLong(),
anyString(), anyLong(), anyString());
}
@@ -800,32 +939,32 @@ class SmartLoadingServiceTest {
}
@Test
void allowsVolumeAndWeightWithinOneHundredTenPercent() {
void allowsVolumeAndWeightWithinOneHundredFiftyPercent() {
stubAssignment();
ContainerBookingEntity tolerant = booking();
tolerant.setCapacityCbm(new BigDecimal("0.91"));
tolerant.setMaxWeightKg(new BigDecimal("3.64"));
tolerant.setCapacityCbm(new BigDecimal("1"));
tolerant.setMaxWeightKg(new BigDecimal("4"));
when(repository.booking(99L)).thenReturn(tolerant);
service.assign(1, new AssignRequest(10L, 20L, 2, 0, "assign-110-ok"), 7, "u");
service.assign(1, new AssignRequest(10L, 20L, 3, 0, "assign-150-ok"), 7, "u");
verify(splitPreloadService).ensurePendingForLoadingAllocation(9L, 10L, 7L, "u");
verify(repository).touch(1, 0, 7, "u");
}
@Test
void rejectsVolumeOrWeightAboveOneHundredTenPercent() {
void rejectsVolumeOrWeightAboveOneHundredFiftyPercent() {
stubAssignment();
ContainerBookingEntity exceeded = booking();
exceeded.setCapacityCbm(new BigDecimal("0.90"));
exceeded.setMaxWeightKg(new BigDecimal("3.63"));
exceeded.setCapacityCbm(new BigDecimal("0.9999"));
exceeded.setMaxWeightKg(new BigDecimal("3.9999"));
when(repository.booking(99L)).thenReturn(exceeded);
BizException error = catchThrowableOfType(() -> service.assign(1,
new AssignRequest(10L, 20L, 2, 0, "assign-110-exceeded"), 7, "u"), BizException.class);
new AssignRequest(10L, 20L, 3, 0, "assign-150-exceeded"), 7, "u"), BizException.class);
assertThat(error.getResultCode()).isEqualTo(ResultCode.VALIDATION_FAILED);
assertThat(error.getMessage()).contains("110%");
assertThat(error.getMessage()).contains("150%");
verify(repository, never()).touch(anyLong(), anyInt(), anyLong(), any());
}
@@ -1322,6 +1461,13 @@ class SmartLoadingServiceTest {
return candidate(9L, 10L, 20L, 30L, boxes);
}
private static InboundOrderEntity inbound(long tenantId, long inboundId) {
InboundOrderEntity value = new InboundOrderEntity();
value.setTenantId(tenantId);
value.setId(inboundId);
return value;
}
private static Candidate candidate(long tenantId, long inboundId, long detailId, long orderId, int boxes) {
return candidate(tenantId, inboundId, detailId, orderId, boxes, "FBA1");
}
@@ -1363,6 +1509,16 @@ class SmartLoadingServiceTest {
false, null, value.orderStatusSnapshot(), value.expectedArrivalAt());
}
private static Candidate withStatus(Candidate value, String status, boolean locked) {
return new Candidate(value.accountId(), value.inboundId(), value.inboundDetailId(), value.orderId(),
value.orderNo(), value.inboundCode(), value.warehouseId(), value.channelCode(), value.channelName(),
value.destinationWarehouseId(), value.destinationWarehouseCode(), value.destinationWarehouseName(),
value.customerName(), value.goodsName(), value.availablePackageCount(), value.unitGrossWeightKg(),
value.unitNetWeightKg(), value.unitVolumeCbm(), value.coLoadGroupId(), value.groupNo(),
value.memberCount(), value.groupTotalBoxes(), locked, value.blockReason(), status,
value.expectedArrivalAt());
}
private static ChannelRow channel(String channelCode, String channelName) {
ChannelRow value = new ChannelRow();
value.setChannelCode(channelCode); value.setChannelName(channelName);