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:
+8
@@ -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}")
|
||||
|
||||
+1
-1
@@ -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")
|
||||
|
||||
+17
-9
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user