This commit is contained in:
2026-09-03 20:38:47 +08:00
parent a24d55f526
commit 171f450e59
9 changed files with 173 additions and 11 deletions
@@ -67,7 +67,17 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark, public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark,
@NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion, @NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion,
@NotBlank @Size(max=128) String operationId) {} @NotBlank @Size(max=128) String operationId) {}
public record PlanCoLoadGroup(Long groupId,String groupNo,Long lockedPlanId,Integer memberOrderCount,
Integer inboundFragmentCount,Integer fullPackageCount,List<Long> memberInboundIds) {
public PlanCoLoadGroup { memberInboundIds=memberInboundIds==null?List.of():List.copyOf(memberInboundIds); }
}
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo, public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,
Integer version,Boolean canAssign,Boolean canEdit,String blockedReason,String finalCompletionStatus, Integer version,Boolean canAssign,Boolean canEdit,String blockedReason,String finalCompletionStatus,
Long pdaTaskId,String pdaTaskStatus,Integer pdaTaskVersion,List<LoadingPlanItemEntity> items) {} Long pdaTaskId,String pdaTaskStatus,Integer pdaTaskVersion,List<PlanCoLoadGroup> coLoadGroups,
List<LoadingPlanItemEntity> items) {
public Plan {
coLoadGroups=coLoadGroups==null?List.of():List.copyOf(coLoadGroups);
items=items==null?List.of():List.copyOf(items);
}
}
} }
@@ -12,6 +12,22 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
""") """)
List<ChannelContainerCountRow> channelContainerCounts(@Param("warehouseId") long warehouseId); List<ChannelContainerCountRow> channelContainerCounts(@Param("warehouseId") long warehouseId);
@Select("""
SELECT DISTINCT group_row.id groupId,group_row.group_no groupNo,
group_row.locked_plan_id lockedPlanId,member.order_id orderId,
member.inbound_id inboundId,member.full_package_count fullPackageCount
FROM fl_loading_plan_item plan_item
JOIN fl_coload_group_member member
ON member.tenant_id=plan_item.account_id AND member.inbound_id=plan_item.inbound_id
AND member.active_flag=1
JOIN fl_coload_group group_row ON group_row.id=member.group_id
WHERE plan_item.loading_plan_id=#{planId} AND plan_item.deleted=0
AND group_row.locked_plan_id=#{planId}
AND group_row.group_status IN ('LOCKED','CONSUMED')
ORDER BY group_row.id,member.order_id,member.inbound_id
""")
List<PlanColoadMemberRow> planColoadMembers(@Param("planId") long planId);
@Select(""" @Select("""
SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId, SELECT o.tenant_id accountId,o.id inboundId,i.inbound_detail_id inboundDetailId,
o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode, o.order_id orderId,o.order_no orderNo,o.inbound_code inboundCode,
@@ -121,10 +137,9 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)&gt;0 AND SUM(CASE WHEN LOWER(eligible.inboundCode) LIKE CONCAT('%',LOWER(#{query.inboundCode}),'%') THEN 1 ELSE 0 END)&gt;0
</if> </if>
<if test="query.expectedArrivalStart != null or query.expectedArrivalEnd != null"> <if test="query.expectedArrivalStart != null or query.expectedArrivalEnd != null">
AND SUM(CASE WHEN eligible.expectedArrivalAt IS NOT NULL AND MIN(eligible.expectedArrivalAt) IS NOT NULL
<if test="query.expectedArrivalStart != null">AND DATE(eligible.expectedArrivalAt)&gt;=#{query.expectedArrivalStart}</if> <if test="query.expectedArrivalStart != null">AND DATE(MIN(eligible.expectedArrivalAt))&gt;=#{query.expectedArrivalStart}</if>
<if test="query.expectedArrivalEnd != null">AND DATE(eligible.expectedArrivalAt)&lt;=#{query.expectedArrivalEnd}</if> <if test="query.expectedArrivalEnd != null">AND DATE(MIN(eligible.expectedArrivalAt))&lt;=#{query.expectedArrivalEnd}</if>
THEN 1 ELSE 0 END)&gt;0
</if> </if>
ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId ORDER BY sortArrivalAt IS NULL,sortArrivalAt,sortInboundId
</script> </script>
@@ -286,4 +301,6 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.ba
} }
record CandidateUnit(Long groupId,Long accountId,Long inboundId, record CandidateUnit(Long groupId,Long accountId,Long inboundId,
LocalDateTime sortArrivalAt,Long sortInboundId) {} LocalDateTime sortArrivalAt,Long sortInboundId) {}
record PlanColoadMemberRow(Long groupId,String groupNo,Long lockedPlanId,
Long orderId,Long inboundId,Integer fullPackageCount) {}
} }
@@ -19,6 +19,7 @@ import com.baiyue.firstleg.mapper.LoadingPlanMapper;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.PlanColoadMemberRow;
import com.baiyue.firstleg.mapper.InboundOrderMapper; import com.baiyue.firstleg.mapper.InboundOrderMapper;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -56,6 +57,9 @@ public class SmartLoadingRepository {
public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) { public List<ChannelContainerCountRow> channelContainerCounts(long warehouseId) {
return candidates.channelContainerCounts(warehouseId); return candidates.channelContainerCounts(warehouseId);
} }
public List<PlanColoadMemberRow> planColoadMembers(long planId) {
return candidates.planColoadMembers(planId);
}
public Candidate candidate(long planId, long warehouseId, long inboundId, long detailId) { public Candidate candidate(long planId, long warehouseId, long inboundId, long detailId) {
return candidates.candidate(planId, warehouseId, inboundId, detailId); return candidates.candidate(planId, warehouseId, inboundId, detailId);
} }
@@ -14,6 +14,7 @@ import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.GroupSnapshot;
import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member; import com.baiyue.firstleg.loading.coload.model.ColoadGroupModels.Member;
import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort; import com.baiyue.firstleg.loading.coload.port.ColoadGroupAssignmentPort;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.PlanColoadMemberRow;
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase; import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.split.service.SplitPreloadService; import com.baiyue.firstleg.split.service.SplitPreloadService;
import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.DuplicateKeyException;
@@ -235,12 +236,14 @@ public class SmartLoadingService {
return actual; return actual;
} }
@Transactional(readOnly = true)
public Plan draftByBooking(long bookingId) { public Plan draftByBooking(long bookingId) {
ContainerBookingEntity booking = requireBooking(bookingId); ContainerBookingEntity booking = requireBooking(bookingId);
LoadingPlanEntity plan = repository.planByBooking(bookingId); LoadingPlanEntity plan = repository.planByBooking(bookingId);
return draftView(plan, booking); return draftView(plan, booking);
} }
@Transactional(readOnly = true)
public Plan detail(long planId) { public Plan detail(long planId) {
LoadingPlanEntity plan = repository.plan(planId); LoadingPlanEntity plan = repository.plan(planId);
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在"); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "装柜计划不存在");
@@ -964,7 +967,22 @@ public class SmartLoadingService {
: "COMPLETED".equals(task.taskStatus()) ? "COMPLETED" : "IN_PROGRESS"; : "COMPLETED".equals(task.taskStatus()) ? "COMPLETED" : "IN_PROGRESS";
return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(), return new Plan(p.getId(),p.getPlanNo(),p.getPlanStatus(),p.getContainerBookingId(),
b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus, b.getContainerNo(),b.getVoyageNo(),p.getVersion(),mutable,mutable,blockedReason,finalStatus,
task.taskId(),task.taskStatus(),task.taskVersion(),items); task.taskId(),task.taskStatus(),task.taskVersion(),planCoLoadGroups(p.getId()),items);
}
private List<PlanCoLoadGroup> planCoLoadGroups(long planId) {
Map<Long,List<PlanColoadMemberRow>> grouped=repository.planColoadMembers(planId).stream()
.collect(java.util.stream.Collectors.groupingBy(PlanColoadMemberRow::groupId,
LinkedHashMap::new,java.util.stream.Collectors.toList()));
return grouped.values().stream().map(rows->{
PlanColoadMemberRow first=rows.get(0);
LinkedHashMap<Long,Integer> packagesByInbound=new LinkedHashMap<>();
rows.forEach(row->packagesByInbound.putIfAbsent(row.inboundId(),row.fullPackageCount()));
int orderCount=(int)rows.stream().map(PlanColoadMemberRow::orderId).distinct().count();
int packages=packagesByInbound.values().stream().filter(Objects::nonNull)
.mapToInt(Integer::intValue).sum();
return new PlanCoLoadGroup(first.groupId(),first.groupNo(),first.lockedPlanId(),orderCount,
packagesByInbound.size(),packages,List.copyOf(packagesByInbound.keySet()));
}).toList();
} }
private Plan draftView(LoadingPlanEntity plan, ContainerBookingEntity booking) { private Plan draftView(LoadingPlanEntity plan, ContainerBookingEntity booking) {
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "该货柜尚未创建智能装柜草稿"); if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "该货柜尚未创建智能装柜草稿");
@@ -2035,7 +2035,7 @@ components:
version: {type: integer, minimum: 0, description: 柜次版本} version: {type: integer, minimum: 0, description: 柜次版本}
SmartLoadingPlan: SmartLoadingPlan:
type: object type: object
required: [id, planNo, status, bookingId, version, canAssign, canEdit, finalCompletionStatus, items] required: [id, planNo, status, bookingId, version, canAssign, canEdit, finalCompletionStatus, coLoadGroups, items]
properties: properties:
id: {type: string} id: {type: string}
planNo: {type: string, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 预装与确认装柜清单共用的稳定编号} planNo: {type: string, pattern: '^LP-[0-9]{6}-[0-9]{4}$', example: LP-260818-0001, description: 预装与确认装柜清单共用的稳定编号}
@@ -2051,9 +2051,28 @@ components:
pdaTaskId: {type: string, nullable: true} pdaTaskId: {type: string, nullable: true}
pdaTaskStatus: {type: string, nullable: true, enum: [LOADING, COMPLETED], description: LOADING task 在增量后保持原 taskId;COMPLETED 永久拒绝增量} pdaTaskStatus: {type: string, nullable: true, enum: [LOADING, COMPLETED], description: LOADING task 在增量后保持原 taskId;COMPLETED 永久拒绝增量}
pdaTaskVersion: {type: integer, minimum: 0, nullable: true, description: 服务端同步后最新 taskVersion;Admin 写操作不提交该值} pdaTaskVersion: {type: integer, minimum: 0, nullable: true, description: 服务端同步后最新 taskVersion;Admin 写操作不提交该值}
coLoadGroups:
type: array
description: 当前计划内同柜不可拆组的完整冻结快照;随计划一次返回,调用方不得按 inboundId 扇出重建
items: {$ref: '#/components/schemas/SmartLoadingPlanCoLoadGroup'}
items: items:
type: array type: array
items: {$ref: '#/components/schemas/SmartLoadingPlanItem'} items: {$ref: '#/components/schemas/SmartLoadingPlanItem'}
SmartLoadingPlanCoLoadGroup:
type: object
required: [groupId, groupNo, lockedPlanId, memberOrderCount, inboundFragmentCount, fullPackageCount, memberInboundIds]
properties:
groupId: {type: string, pattern: '^[1-9][0-9]*$'}
groupNo: {type: string, minLength: 1}
lockedPlanId: {type: string, pattern: '^[1-9][0-9]*$'}
memberOrderCount: {type: integer, minimum: 1, description: 组内去重订单数}
inboundFragmentCount: {type: integer, minimum: 1, description: 组内去重当前有效入仓片段数}
fullPackageCount: {type: integer, minimum: 1, description: 全部成员冻结满箱数之和}
memberInboundIds:
type: array
minItems: 1
uniqueItems: true
items: {type: string, pattern: '^[1-9][0-9]*$'}
SmartLoadingPlanItem: SmartLoadingPlanItem:
type: object type: object
required: [id, packageCount, version, pickedPackageCount, minPackageCount, canAdjust, canRemove] required: [id, packageCount, version, pickedPackageCount, minPackageCount, canAdjust, canRemove]
@@ -78,5 +78,13 @@ class PlatformSmartLoadingControllerContractTest {
.contains("回到 IN_PROGRESS") .contains("回到 IN_PROGRESS")
.contains("COMPLETED 永久拒绝增量"); .contains("COMPLETED 永久拒绝增量");
} }
@Test void planContractEmbedsColoadGroupsWithoutPerInboundFanout() throws Exception {
String yaml=java.nio.file.Files.readString(java.nio.file.Path.of("src/main/resources/openapi/firstleg-v1.yaml"));
assertThat(yaml)
.contains("required: [id, planNo, status, bookingId, version, canAssign, canEdit, finalCompletionStatus, coLoadGroups, items]")
.contains("SmartLoadingPlanCoLoadGroup:")
.contains("required: [groupId, groupNo, lockedPlanId, memberOrderCount, inboundFragmentCount, fullPackageCount, memberInboundIds]")
.contains("调用方不得按 inboundId 扇出重建");
}
private void authority(String method,String permission,Class<?>...types)throws Exception{Method m=PlatformSmartLoadingController.class.getDeclaredMethod(method,types);assertThat(m.getAnnotation(PreAuthorize.class).value()).isEqualTo("hasAuthority('"+permission+"')");} private void authority(String method,String permission,Class<?>...types)throws Exception{Method m=PlatformSmartLoadingController.class.getDeclaredMethod(method,types);assertThat(m.getAnnotation(PreAuthorize.class).value()).isEqualTo("hasAuthority('"+permission+"')");}
} }
@@ -14,6 +14,8 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.WorkbenchContainerQuery;
import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery; import com.baiyue.firstleg.dto.SmartLoadingDtos.InboundCandidateQuery;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.time.LocalDate;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -83,6 +85,39 @@ class SmartLoadingCandidateMapperContractTest {
.doesNotContain("fl_container_booking"); .doesNotContain("fl_container_booking");
} }
@Test
void globalCandidateDateFilterUsesTheSameRepresentativeArrivalAsTheReturnedUnit() throws Exception {
Method units = SmartLoadingCandidateMapper.class.getDeclaredMethod("globalCandidateUnits",
Page.class, InboundCandidateQuery.class, Set.class);
String sql = String.join(" ", units.getAnnotation(Select.class).value());
assertThat(sql)
.contains("MIN(eligible.expectedArrivalAt) sortArrivalAt")
.contains("MIN(eligible.expectedArrivalAt) IS NOT NULL")
.contains("DATE(MIN(eligible.expectedArrivalAt))&gt;=#{query.expectedArrivalStart}")
.contains("DATE(MIN(eligible.expectedArrivalAt))&lt;=#{query.expectedArrivalEnd}")
.doesNotContain("SUM(CASE WHEN eligible.expectedArrivalAt IS NOT NULL");
}
@Test
void representativeArrivalControlsWhetherTheWholeGroupMatchesTheClosedDateRange() {
List<LocalDateTime> groupArrivals = List.of(
LocalDateTime.parse("2026-09-01T09:00:00"),
LocalDateTime.parse("2026-09-10T09:00:00"));
LocalDate representative = groupArrivals.stream().min(LocalDateTime::compareTo)
.orElseThrow().toLocalDate();
assertThat(matchesClosedRange(representative,
LocalDate.parse("2026-09-10"), LocalDate.parse("2026-09-10")))
.as("another member matching must not admit a group whose MIN arrival is outside")
.isFalse();
assertThat(matchesClosedRange(representative,
LocalDate.parse("2026-09-01"), LocalDate.parse("2026-09-01")))
.as("a matching MIN arrival admits the whole group")
.isTrue();
}
@Test @Test
void globalCandidateDynamicSqlRendersForStandaloneAndGroupedUnits() throws Exception { void globalCandidateDynamicSqlRendersForStandaloneAndGroupedUnits() throws Exception {
Configuration configuration = new Configuration(); Configuration configuration = new Configuration();
@@ -95,7 +130,10 @@ class SmartLoadingCandidateMapperContractTest {
null, 1, 20); null, 1, 20);
String renderedUnits = driver.createSqlSource(configuration, unitScript, Map.class) String renderedUnits = driver.createSqlSource(configuration, unitScript, Map.class)
.getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql(); .getBoundSql(Map.of("query", query, "actualChannels", Set.of("MATSON_CLX_TRUCK"))).getSql();
assertThat(renderedUnits).contains("o.channel_code IN", "DATE(eligible.expectedArrivalAt)"); assertThat(renderedUnits)
.contains("o.channel_code IN", "MIN(eligible.expectedArrivalAt) IS NOT NULL")
.contains("DATE(MIN(eligible.expectedArrivalAt))>=", "DATE(MIN(eligible.expectedArrivalAt))<=")
.doesNotContain("SUM(CASE WHEN eligible.expectedArrivalAt IS NOT NULL");
Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod( Method details = SmartLoadingCandidateMapper.class.getDeclaredMethod(
"globalCandidatesByUnits", List.class); "globalCandidatesByUnits", List.class);
@@ -172,4 +210,25 @@ class SmartLoadingCandidateMapperContractTest {
.contains("GROUP BY b.channel_code") .contains("GROUP BY b.channel_code")
.doesNotContain("'COMPLETED'"); .doesNotContain("'COMPLETED'");
} }
@Test
void planColoadSnapshotUsesOnePlanScopedBatchQuery() throws Exception {
Method method=SmartLoadingCandidateMapper.class.getDeclaredMethod("planColoadMembers",long.class);
String sql=String.join(" ",method.getAnnotation(Select.class).value());
assertThat(sql)
.contains("plan_item.loading_plan_id=#{planId}")
.contains("plan_item.deleted=0")
.contains("member.tenant_id=plan_item.account_id")
.contains("member.inbound_id=plan_item.inbound_id")
.contains("member.active_flag=1")
.contains("group_row.locked_plan_id=#{planId}")
.contains("group_row.group_status IN ('LOCKED','CONSUMED')")
.contains("SELECT DISTINCT")
.doesNotContain("WHERE member.inbound_id=#{inboundId}");
}
private static boolean matchesClosedRange(LocalDate arrival, LocalDate start, LocalDate end) {
return arrival != null && !arrival.isBefore(start) && !arrival.isAfter(end);
}
} }
@@ -55,7 +55,7 @@ class ContainerServiceTest {
when(smartLoadingService.createForContainer(anyLong(), anyLong(), anyString())).thenAnswer(invocation -> when(smartLoadingService.createForContainer(anyLong(), anyLong(), anyString())).thenAnswer(invocation ->
new com.baiyue.firstleg.dto.SmartLoadingDtos.Plan(501L, "LP-260818-0001", "EDITING", new com.baiyue.firstleg.dto.SmartLoadingDtos.Plan(501L, "LP-260818-0001", "EDITING",
invocation.getArgument(0, Long.class), null, null, 0, true, true, null, invocation.getArgument(0, Long.class), null, null, 0, true, true, null,
"NOT_STARTED", null, null, null, List.of())); "NOT_STARTED", null, null, null, List.of(), List.of()));
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation -> when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓")); activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
when(suppliers.trucking(202L, "拖车公司A")) when(suppliers.trucking(202L, "拖车公司A"))
@@ -31,6 +31,7 @@ import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.C
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase; import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.ChannelContainerCountRow;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit; import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.CandidateUnit;
import com.baiyue.firstleg.mapper.SmartLoadingCandidateMapper.PlanColoadMemberRow;
import com.baiyue.firstleg.split.service.SplitPreloadService; import com.baiyue.firstleg.split.service.SplitPreloadService;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import feign.FeignException; import feign.FeignException;
@@ -116,6 +117,32 @@ class SmartLoadingServiceTest {
assertThat(service.detail(1L).bookingId()).isEqualTo(99L); assertThat(service.detail(1L).bookingId()).isEqualTo(99L);
} }
@Test
void returnsPlanColoadGroupsWithOneBatchLookup() {
LoadingPlanEntity plan = plan("EDITING", 2);
when(repository.plan(1L)).thenReturn(plan);
when(repository.booking(99L)).thenReturn(booking());
when(repository.items(1L)).thenReturn(List.of());
when(repository.planColoadMembers(1L)).thenReturn(List.of(
new PlanColoadMemberRow(700L,"CLG-700",1L,30L,10L,4),
new PlanColoadMemberRow(700L,"CLG-700",1L,30L,11L,6),
new PlanColoadMemberRow(700L,"CLG-700",1L,31L,12L,5)));
Plan result=service.detail(1L);
assertThat(result.coLoadGroups()).singleElement().satisfies(group->{
assertThat(group.groupId()).isEqualTo(700L);
assertThat(group.groupNo()).isEqualTo("CLG-700");
assertThat(group.lockedPlanId()).isEqualTo(1L);
assertThat(group.memberOrderCount()).isEqualTo(2);
assertThat(group.inboundFragmentCount()).isEqualTo(3);
assertThat(group.fullPackageCount()).isEqualTo(15);
assertThat(group.memberInboundIds()).containsExactly(10L,11L,12L);
});
verify(repository).planColoadMembers(1L);
verify(coloadGroups,never()).findByInbound(anyLong(),anyLong());
}
@Test @Test
void restoresLoadingPlanForContinuedAssignment() { void restoresLoadingPlanForContinuedAssignment() {
LoadingPlanEntity plan = plan("LOADING", 3); LoadingPlanEntity plan = plan("LOADING", 3);
@@ -208,8 +235,8 @@ class SmartLoadingServiceTest {
Candidate second = withGroup(withArrival(withChannel(candidate(19L, 11L, 21L, 31L, 6), Candidate second = withGroup(withArrival(withChannel(candidate(19L, 11L, 21L, 31L, 6),
"MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00"), 2, 10); "MATSON_CLX_TRUCK", "美森正班海卡"), "2026-09-10T09:00:00"), 2, 10);
InboundCandidateQuery query = new InboundCandidateQuery( InboundCandidateQuery query = new InboundCandidateQuery(
List.of("MATSON_CLX"), 100L, null, null, LocalDate.parse("2026-09-10"), List.of("MATSON_CLX"), 100L, null, null, LocalDate.parse("2026-09-01"),
LocalDate.parse("2026-09-10"), null, 1, 1); LocalDate.parse("2026-09-01"), null, 1, 1);
CandidateUnit unit = new CandidateUnit(700L, null, null, CandidateUnit unit = new CandidateUnit(700L, null, null,
LocalDateTime.parse("2026-09-01T09:00:00"), 10L); LocalDateTime.parse("2026-09-01T09:00:00"), 10L);
when(repository.globalCandidateUnits(eq(query), anySet())) when(repository.globalCandidateUnits(eq(query), anySet()))