fix(loading): remove ISO container validation across PDA fulfillment
This commit is contained in:
+1
-1
@@ -107,7 +107,7 @@ class ContainerControllerContractTest {
|
||||
.contains("containerNo 可空")
|
||||
.contains("提柜后由拖车链调用柜次服务原子补录真实柜号")
|
||||
.contains("不校验 ISO 6346 长度、结构或校验位,并保证同一订舱内唯一")
|
||||
.contains("确认预装和创建PDA任务前仍校验ISO格式")
|
||||
.contains("运输信息、提柜补录、确认预装、创建PDA任务、装柜完成及跨服务事件统一不校验ISO格式")
|
||||
.doesNotContain("/api/platform/v1/containers/{id}/transport:")
|
||||
.doesNotContain("ContainerOceanBookingUnlinkRequest")
|
||||
.doesNotContain("ContainerTransportUpdateRequest")
|
||||
|
||||
+1
-1
@@ -73,7 +73,7 @@ class LoadingListControllerContractTest {
|
||||
.contains("containerNo")
|
||||
.contains("voyageNo")
|
||||
.contains("首次补录真实集装箱号")
|
||||
.contains("校验ISO 6346结构、设备类别和校验位")
|
||||
.contains("不校验 ISO 6346 长度、结构、设备类别或校验位")
|
||||
.contains("PDA任务尚未领取且无任何物理作业事实")
|
||||
.contains("/loading-plans/{planId}/confirm")
|
||||
.contains("/loading-plans/{planId}/lifecycle")
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class Iso6346ContainerNumberTest {
|
||||
@Test
|
||||
void normalizesAllowedWhitespaceAndLowercase() {
|
||||
assertThat(Iso6346ContainerNumber.require(" ms cu 663987 0 "))
|
||||
.isEqualTo("MSCU6639870");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsWrongEquipmentCategoryAndCheckDigit() {
|
||||
assertThatThrownBy(() -> Iso6346ContainerNumber.require("MSCA6639870"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("设备类别");
|
||||
assertThatThrownBy(() -> Iso6346ContainerNumber.require("MSCU6639871"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("校验位");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class RealContainerNumberTest {
|
||||
/** 录入只去空白转大写,ISO 长度、设备类别与校验位不构成门禁。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCA6639870", "MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void normalizesWithoutIsoValidation(String containerNo) {
|
||||
assertThat(RealContainerNumber.require(" \u00a0" + containerNo.toLowerCase(java.util.Locale.ROOT) + " "))
|
||||
.isEqualTo(containerNo);
|
||||
}
|
||||
|
||||
/** 空柜号不能进入实际作业,包含规范化后为空的 Unicode 空白。 */
|
||||
@ParameterizedTest
|
||||
@NullAndEmptySource
|
||||
@ValueSource(strings = {" ", "\u00a0\u3000"})
|
||||
void rejectsMissingNumber(String containerNo) {
|
||||
assertThatThrownBy(() -> RealContainerNumber.require(containerNo))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不能为空");
|
||||
}
|
||||
}
|
||||
@@ -297,21 +297,23 @@ class ContainerServiceTest {
|
||||
verifyNoInteractions(oceanBookings);
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 提柜补录使用相同的非 ISO 规则,并按版本原子保存。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "LOCAL-2026-001"})
|
||||
@DisplayName("提柜后按柜次版本原子补录真实柜号")
|
||||
void assignsRealContainerNumberAfterPickup() {
|
||||
void assignsRealContainerNumberAfterPickup(String containerNo) {
|
||||
ContainerBookingEntity linked = booking("EDITING", 3);
|
||||
linked.setContainerNo(null); linked.setOceanBookingId(21L); linked.setOceanBookingRequirementId(31L);
|
||||
ContainerBookingEntity assigned = booking("EDITING", 4);
|
||||
assigned.setContainerNo("MSCU6639870"); assigned.setOceanBookingId(21L); assigned.setOceanBookingRequirementId(31L);
|
||||
assigned.setContainerNo(containerNo); assigned.setOceanBookingId(21L); assigned.setOceanBookingRequirementId(31L);
|
||||
when(repository.find(11L)).thenReturn(linked, assigned);
|
||||
when(repository.assignRealContainerNo(11L, "MSCU6639870", 3, 9L, "admin")).thenReturn(1);
|
||||
when(repository.assignRealContainerNo(11L, containerNo, 3, 9L, "admin")).thenReturn(1);
|
||||
|
||||
ContainerBookingEntity result = service.assignRealContainerNo(
|
||||
11L, " ms cu 663987 0 ", 3, 9L, "admin");
|
||||
11L, " " + containerNo.toLowerCase(java.util.Locale.ROOT) + " ", 3, 9L, "admin");
|
||||
|
||||
assertThat(result.getContainerNo()).isEqualTo("MSCU6639870");
|
||||
verify(repository).assignRealContainerNo(11L, "MSCU6639870", 3, 9L, "admin");
|
||||
assertThat(result.getContainerNo()).isEqualTo(containerNo);
|
||||
verify(repository).assignRealContainerNo(11L, containerNo, 3, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -101,6 +101,23 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(first.eventVersion()).isEqualTo(3);
|
||||
}
|
||||
|
||||
/** 非 ISO 柜号能生成当前 V3 事件,并保持冻结柜号原值。 */
|
||||
@org.junit.jupiter.params.ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "LOCAL-2026-001"})
|
||||
void publishesNonIsoContainerNumber(String containerNo) throws Exception {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setContainerNo(containerNo);
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
|
||||
service.publish(90L);
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captured = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(outbox).insert(captured.capture());
|
||||
EventEnvelope event = mapper.readValue(captured.getValue().getPayloadJson(), EventEnvelope.class);
|
||||
assertThat(event.eventVersion()).isEqualTo(3);
|
||||
assertThat(event.payload().path("containerNo").asText()).isEqualTo(containerNo);
|
||||
}
|
||||
|
||||
@Test void requiresFrozenPhysicalContainerNumberBeforePublishing() {
|
||||
Row value = row(11, 100, 101); value.setContainerNo(" ");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
|
||||
@@ -168,17 +168,20 @@ class LoadingListServiceTest {
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
}
|
||||
|
||||
/** 非关联入口新录入或纠正柜号仍使用原有规则,不放宽到所有录入场景。 */
|
||||
@Test
|
||||
void stillRejectsNewNonIsoNumberInTransportCorrection() {
|
||||
/** 运输纠错与关联入口一致,只规范化柜号并保留纠错原因。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void acceptsNonIsoNumberInTransportCorrection(String containerNo) {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setContainerNo("LOCAL-1");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(repository.updateTransport(10L, 3, containerNo, null, 9L, "operator")).thenReturn(1);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "invalid-correction",
|
||||
new TransportUpdateRequest("OTHER-2", null, 3, "纠错")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("ISO 6346");
|
||||
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "non-iso-correction",
|
||||
new TransportUpdateRequest(" " + containerNo.toLowerCase(java.util.Locale.ROOT) + " ", null, 3, "纠错"));
|
||||
|
||||
verify(repository).updateTransport(10L, 3, containerNo, null, 9L, "operator");
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
}
|
||||
|
||||
/** 关联入口接受的非 ISO 柜号,在 PDA 领取前仍可按版本和原因纠正为真实柜号。 */
|
||||
@@ -235,7 +238,7 @@ class LoadingListServiceTest {
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "key-1",
|
||||
new TransportUpdateRequest(" ", "023E", 3, "清空测试")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("ISO 6346");
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不能为空");
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(3),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class), eq(9L),
|
||||
|
||||
@@ -962,10 +962,10 @@ class LoadingPickServiceTest {
|
||||
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
|
||||
}
|
||||
|
||||
/** 关联放行的非 ISO 柜号必须在可纠错阶段明确反馈,纠正后可继续确认。 */
|
||||
/** 关联接受的非 ISO 柜号可直接确认预装,保留订舱放舱门禁。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void rejectsNonIsoNumberBeforeConfirmationAndAllowsCorrection(String containerNo) {
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void acceptsNonIsoNumberBeforeConfirmation(String containerNo) {
|
||||
Link link = new Link(1L, 1L, 50L, 2L, "PENDING", 1,
|
||||
10L, 40L, containerNo, "EDITING", 3, 2, null);
|
||||
var booking = new ContainerBookingEntity();
|
||||
@@ -974,12 +974,6 @@ class LoadingPickServiceTest {
|
||||
when(loadingModelBridge.requireAllocatedLinksForConfirmation(10L)).thenReturn(List.of(link));
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
|
||||
assertThatThrownBy(() -> service.requirePreloadConfirmationReady(10L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("预装运输信息");
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
verify(completionCommands, never()).insert(any());
|
||||
|
||||
booking.setContainerNo("MSCU6639870");
|
||||
var allocation = new OceanBookingContainerAllocationEntity();
|
||||
allocation.setOceanBookingId(90L);
|
||||
var ocean = new OceanBookingEntity();
|
||||
@@ -990,27 +984,58 @@ class LoadingPickServiceTest {
|
||||
assertThat(service.requirePreloadConfirmationReady(10L)).containsExactly(link);
|
||||
}
|
||||
|
||||
/** 任务创建入口同样先校验格式,不能绕过确认门禁进入实物作业。 */
|
||||
/** 非 ISO 柜号可以创建 PDA 任务,任务仍按柜次关联权威柜号。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void rejectsNonIsoNumberBeforeCreatingPdaTask(String containerNo) {
|
||||
assertThatThrownBy(() -> service.begin(10L, containerNo, 9L, "operator", List.of()))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("预装运输信息");
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
verify(repository, never()).insert(any(LoadingPickItemEntity.class));
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void createsPdaTaskWithNonIsoNumber(String containerNo) {
|
||||
var plan = new com.baiyue.firstleg.entity.LoadingPlanEntity();
|
||||
plan.setId(10L); plan.setContainerBookingId(40L);
|
||||
var booking = new ContainerBookingEntity();
|
||||
booking.setId(40L); booking.setContainerNo(containerNo); booking.setCabinetNo("D2092");
|
||||
when(smartLoadingRepository.plan(10L)).thenReturn(plan);
|
||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||
doAnswer(invocation -> {
|
||||
LoadingPickTaskEntity created = invocation.getArgument(0);
|
||||
created.setId(20L);
|
||||
return 1;
|
||||
}).when(repository).insert(any(LoadingPickTaskEntity.class));
|
||||
|
||||
service.begin(10L, containerNo, 9L, "operator", List.of());
|
||||
|
||||
ArgumentCaptor<LoadingPickTaskEntity> captured = ArgumentCaptor.forClass(LoadingPickTaskEntity.class);
|
||||
verify(repository).insert(captured.capture());
|
||||
assertThat(captured.getValue().getContainerBookingId()).isEqualTo(40L);
|
||||
assertThat(captured.getValue().getTaskStatus()).isEqualTo("LOADING");
|
||||
}
|
||||
|
||||
/** 完成前的最终校验仍保留,不能让异常柜号生成无法投递的装柜完成事件。 */
|
||||
@Test
|
||||
void completionStillRequiresIsoNumberForCrossServiceFacts() {
|
||||
repository.activeBookingByTask(20L).setContainerNo("MSCU6639871");
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task());
|
||||
/** 去除 ISO 校验不应放开缺号任务。 */
|
||||
@ParameterizedTest
|
||||
@org.junit.jupiter.params.provider.NullAndEmptySource
|
||||
@ValueSource(strings = {" ", "\u00a0\u3000"})
|
||||
void rejectsMissingNumberBeforeCreatingPdaTask(String containerNo) {
|
||||
assertThatThrownBy(() -> service.begin(10L, containerNo, 9L, "operator", List.of()))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("录入真实集装箱号");
|
||||
verify(repository, never()).insert(any(LoadingPickTaskEntity.class));
|
||||
}
|
||||
|
||||
assertThatThrownBy(() -> service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "invalid-final-container", "checked"), 9L, "operator"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("ISO 6346");
|
||||
verify(completionCommands, never()).insert(any());
|
||||
verify(repository, never()).acceptCompletion(anyLong(), anyInt());
|
||||
/** 非 ISO 柜号不会在完成阶段再次受阻,仍通过持久化命令结算。 */
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"JZXH10231043", "MSCU6639871", "ABC", "ABCU123456789012", "LOCAL-2026-001"})
|
||||
void completionAcceptsNonIsoNumberForCrossServiceFacts(String containerNo) {
|
||||
repository.activeBookingByTask(20L).setContainerNo(containerNo);
|
||||
when(repository.taskForUpdate(20L)).thenReturn(task());
|
||||
LoadingPickTaskEntity completing = task();
|
||||
completing.setTaskStatus("COMPLETING");
|
||||
completing.setVersion(1);
|
||||
when(repository.task(20L)).thenReturn(completing);
|
||||
when(repository.allTaskItemsResolved(20L)).thenReturn(true);
|
||||
|
||||
Task result = service.complete(20L,
|
||||
new CompleteLoadingRequest(0, "complete-non-iso", "checked"), 9L, "operator");
|
||||
|
||||
assertThat(result.taskStatus()).isEqualTo("COMPLETING");
|
||||
verify(completionCommands).insert(any());
|
||||
verify(repository).acceptCompletion(20L, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user