This commit is contained in:
2026-09-01 15:25:15 +08:00
parent 4d7ea28a61
commit d1225845ec
12 changed files with 129 additions and 45 deletions
@@ -100,10 +100,10 @@ class LoadingListControllerContractTest {
}
@Test
void transportCorrectionRequiresReasonAndKeepsNullContainerAsUnchanged() {
void transportCorrectionReasonIsRuntimeConditionalAndNullContainerKeepsCurrentValue() {
assertThat(component(TransportUpdateRequest.class, "operationReason").getAccessor()
.getAnnotation(NotBlank.class))
.isNotNull();
.isNull();
assertThat(component(TransportUpdateRequest.class, "containerNo").getAccessor()
.getAnnotation(NotBlank.class))
.isNull();
@@ -94,7 +94,7 @@ class LoadingListQueryMapperContractTest {
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
.contains("p.plan_status='EDITING'")
.contains("preload_pushed_at IS NOT NULL")
.contains("#{containerNo} IS NOT NULL")
.doesNotContain("#{containerNo} IS NOT NULL")
.contains("fl_loading_pick_task", "fl_loading_pick_assignment")
.doesNotContain("DELETE")
.doesNotContain("fl_loading_plan_item");
@@ -72,4 +72,30 @@ class LoadingListRepositoryTest {
assertThat(confirmed.planId()).isEqualTo(41L);
assertThat(confirmed.containerBookingId()).isEqualTo(61L);
}
@Test
void missingContainerNumberDoesNotBlockPreloadConfirmationOrPdaReadiness() {
AggregateRow row = new AggregateRow();
row.setPlanId(41L);
row.setContainerBookingId(61L);
row.setPlanNo("LP-41");
row.setPlanStatus("EDITING");
row.setVersion(1);
row.setPreloadPushedAt(java.time.LocalDateTime.now());
row.setPreloadLinkCount(2L);
row.setConfirmedPreloadCount(0L);
row.setOceanBookingId(81L);
row.setOceanBookingStatus("RELEASED");
when(mapper.selectItems(41L)).thenReturn(List.of());
when(mapper.selectHistories(41L)).thenReturn(List.of());
var pending = repository.preloadDetail(row).summary().readiness();
assertThat(pending.containerNoReady()).isFalse();
assertThat(pending.canConfirm()).isTrue();
assertThat(pending.blockingReasons()).doesNotContain("CONTAINER_NO_MISSING");
row.setConfirmedPreloadCount(2L);
var confirmed = repository.preloadDetail(row).summary().readiness();
assertThat(confirmed.readyForPda()).isTrue();
}
}
@@ -208,20 +208,43 @@ class LoadingListServiceTest {
}
@Test
void loadingPlanAndMissingInitialOceanBookingNumberBothRejectCorrection() {
void loadingPlanRejectsTransportUpdateButPushedEditingPlanAllowsFirstContainerFill() {
AggregateRow loading = row("LOADING", 3);
loading.setPreloadPushedAt(java.time.LocalDateTime.now());
loading.setContainerNo("MSCU6639870");
AggregateRow missing = row("EDITING", 3);
missing.setPreloadPushedAt(java.time.LocalDateTime.now());
when(repository.aggregate(10L)).thenReturn(loading, missing);
AggregateRow afterFill = row("EDITING", 4);
afterFill.setPreloadPushedAt(missing.getPreloadPushedAt());
afterFill.setContainerNo("CSQU3054383");
when(repository.aggregate(10L)).thenReturn(loading, missing, afterFill);
when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator"))
.thenReturn(1);
when(repository.preloadDetail(afterFill)).thenReturn(
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "loading-key",
new TransportUpdateRequest("CSQU3054383", null, 3, "作业后纠错")))
.isInstanceOf(BizException.class).hasMessageContaining("PDA尚未开始");
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
new TransportUpdateRequest("CSQU3054383", null, 3, "首次补录")))
.isInstanceOf(BizException.class).hasMessageContaining("首次写入");
service.updateTransport(10L, 100L, 9L, "operator", "missing-key",
new TransportUpdateRequest("CSQU3054383", null, 3, null));
verify(repository).updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator");
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
}
@Test
void correctingExistingContainerNumberStillRequiresReason() {
AggregateRow before = row("EDITING", 3);
before.setPreloadPushedAt(java.time.LocalDateTime.now());
before.setContainerNo("MSCU6639870");
when(repository.aggregate(10L)).thenReturn(before);
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "reason-key",
new TransportUpdateRequest("CSQU3054383", null, 3, null)))
.isInstanceOf(BizException.class).hasMessageContaining("纠错原因");
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
}
@Test
@@ -113,7 +113,7 @@ class LoadingPickServiceTest {
}).when(completionTransaction).executeWithoutResult(any());
ContainerBookingEntity booking = new ContainerBookingEntity();
booking.setId(99L);
booking.setContainerNo("CSLU1234567");
booking.setContainerNo("MSCU6639870");
booking.setWarehouseId(101L);
booking.setSourceType("SIMULATION");
booking.setRecordStatus("ACTIVE");
@@ -124,9 +124,9 @@ class LoadingPickServiceTest {
}
@Test
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
void opensTaskAfterReleasedBookingWithoutWaitingForRealContainerNumber() {
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-001");
booking.setId(40L);
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
plan.setId(10L); plan.setContainerBookingId(40L); plan.setPlanStatus("EDITING");
plan.setPreloadPushedAt(java.time.LocalDateTime.now());
@@ -136,7 +136,7 @@ class LoadingPickServiceTest {
when(smartLoadingRepository.planByBooking(40L)).thenReturn(plan);
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
when(loadingModelBridge.byLoadingPlan(10L)).thenReturn(List.of(
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, "MSCU-001", "EDITING", 1, 1, null)));
new Link(1L, 1L, 50L, 2L, "CONFIRMED", 1, 10L, 40L, null, "EDITING", 1, 1, null)));
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
when(repository.taskByPlan(10L)).thenReturn(null);
@@ -191,11 +191,11 @@ class LoadingPickServiceTest {
}
@Test
void planConfirmationGateRequiresRealContainerAndReleasedBooking() {
void planConfirmationGateRequiresReleasedBookingButNotRealContainerNumber() {
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
10L, 40L, "MSCU-001", "EDITING", 3, 2, null);
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L); booking.setContainerNo("MSCU-001");
booking.setId(40L);
var allocation = new OceanBookingContainerAllocationEntity();
allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity();
@@ -209,17 +209,21 @@ class LoadingPickServiceTest {
}
@Test
void planConfirmationGateRejectsMissingRealContainerNumber() {
void planConfirmationGateAllowsMissingRealContainerNumber() {
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
10L, 40L, null, "EDITING", 3, 2, null);
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
booking.setId(40L);
var allocation = new OceanBookingContainerAllocationEntity();
allocation.setOceanBookingId(90L);
var ocean = new OceanBookingEntity();
ocean.setId(90L); ocean.setStatus("RELEASED");
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
when(oceanBookingRepository.allocationByContainer(40L)).thenReturn(allocation);
when(oceanBookingRepository.find(90L)).thenReturn(ocean);
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
.isInstanceOf(BizException.class).hasMessageContaining("真实集装箱号");
verify(oceanBookingRepository, never()).allocationByContainer(anyLong());
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
}
@Test
@@ -775,6 +779,25 @@ class LoadingPickServiceTest {
verify(loadingListService, never()).finishFromPda(anyLong(), anyLong(), anyLong(), anyString());
}
@Test
void completionRejectsMissingRealContainerNumberBeforeCreatingCommand() {
LoadingPickTaskEntity loading = task();
ContainerBookingEntity booking = new ContainerBookingEntity();
booking.setId(99L);
booking.setWarehouseId(101L);
booking.setRecordStatus("ACTIVE");
when(repository.taskForUpdate(20L)).thenReturn(loading);
when(repository.activeBookingByTask(20L)).thenReturn(booking);
assertThatThrownBy(() -> service.complete(20L,
new CompleteLoadingRequest(0, "complete-missing-container", "checked"), 9L, "operator"))
.isInstanceOf(BizException.class).hasMessageContaining("确认装柜完成前");
verify(repository, never()).allTaskItemsResolved(20L);
verify(completionCommands, never()).insert(any());
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
}
@Test
void differentOperationIdIsRejectedImmediatelyWithConflict() {
// different operationId is a canonical A0409 CONFLICT while the durable command is active.