feat: enforce canonical container identity across regions
This commit is contained in:
+4
-5
@@ -22,9 +22,7 @@ class ContainerControllerContractTest {
|
||||
assertPermission("detail", "platform:firstleg:container:list");
|
||||
assertPermission("create", "platform:firstleg:container:create");
|
||||
assertPermission("linkOceanBooking", "platform:firstleg:container:edit");
|
||||
assertPermission("unlinkOceanBooking", "platform:firstleg:container:edit");
|
||||
assertPermission("updateDraft", "platform:firstleg:container:edit");
|
||||
assertPermission("updateTransport", "platform:firstleg:container:edit");
|
||||
assertPermission("uploadClearance", "platform:firstleg:customs:manage");
|
||||
assertPermission("overview", "platform:firstleg:container:list");
|
||||
assertPermission("downloadPdaPhoto", "platform:firstleg:container:list");
|
||||
@@ -60,14 +58,15 @@ class ContainerControllerContractTest {
|
||||
openapi.indexOf(" /openapi/v1/inbound-measurements:"));
|
||||
|
||||
assertThat(section)
|
||||
.contains("/api/platform/v1/containers/{id}/transport:")
|
||||
.contains("/api/platform/v1/containers/{id}/ocean-booking-link:")
|
||||
.contains("首次关联允许 WAIT_LOADING 或已有预排货的 EDITING 模拟柜")
|
||||
.contains("RELEASED oceanBookingId")
|
||||
.contains("未满配 requirementId")
|
||||
.contains("唯一 containerNo")
|
||||
.contains("解绑或换绑仅允许完全空的 WAIT_LOADING 模拟柜")
|
||||
.contains("任何排货、PDA、有效装柜证据或业务单证事实均拒绝")
|
||||
.contains("校验ISO 6346结构、设备类别和校验位")
|
||||
.doesNotContain("/api/platform/v1/containers/{id}/transport:")
|
||||
.doesNotContain("ContainerOceanBookingUnlinkRequest")
|
||||
.doesNotContain("ContainerTransportUpdateRequest")
|
||||
.doesNotContain("realContainerBookingId")
|
||||
.contains("/api/platform/v1/containers/{id}/clearance/document/upload:")
|
||||
.contains("multipart/form-data")
|
||||
|
||||
+14
-2
@@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -67,8 +68,9 @@ class LoadingListControllerContractTest {
|
||||
.contains("beforeSnapshot")
|
||||
.contains("containerNo")
|
||||
.contains("voyageNo")
|
||||
.contains("已推送且仍为 EDITING")
|
||||
.contains("不能清空已创建 PDA 任务的柜号")
|
||||
.contains("唯一纠错入口")
|
||||
.contains("校验ISO 6346结构、设备类别和校验位")
|
||||
.contains("PDA任务尚未领取且无任何物理作业事实")
|
||||
.contains("/loading-plans/{planId}/confirm")
|
||||
.contains("confirmationStatus")
|
||||
.contains("canConfirm")
|
||||
@@ -94,6 +96,16 @@ class LoadingListControllerContractTest {
|
||||
assertThat(component(ConfirmedQuery.class, "warehouseId").getAnnotation(NotNull.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void transportCorrectionRequiresReasonAndKeepsNullContainerAsUnchanged() {
|
||||
assertThat(component(TransportUpdateRequest.class, "operationReason").getAccessor()
|
||||
.getAnnotation(NotBlank.class))
|
||||
.isNotNull();
|
||||
assertThat(component(TransportUpdateRequest.class, "containerNo").getAccessor()
|
||||
.getAnnotation(NotBlank.class))
|
||||
.isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlParses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
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("校验位");
|
||||
}
|
||||
}
|
||||
+3
@@ -22,12 +22,15 @@ class CarrierScheduleQueryOpenApiContractTest {
|
||||
.contains("DISCHARGED_FROM_VESSEL, PICKUP_READY, PICKED_UP, EMPTY_RETURNED")
|
||||
.contains("CUSTOMS_HOLD, TERMINAL_HOLD, DOCUMENT_HOLD, UNKNOWN_HOLD")
|
||||
.contains("enum: [READY, MANUAL_REQUIRED, STOPPED]")
|
||||
.contains("enum: [CONTAINER_NO]")
|
||||
.contains("cabinetNo, orderNo, bookingNo and bill of lading are never fallbacks")
|
||||
.contains("latestActualNode:")
|
||||
.contains("latestActualAt:")
|
||||
.contains("manual actual node is equal to or earlier than latestActualNode")
|
||||
.contains("strictly greater sequence than latestActualNode")
|
||||
.contains("internal execution fields are intentionally absent")
|
||||
.doesNotContain("enum: [READY, MANUAL_REQUIRED, ARRIVED")
|
||||
.doesNotContain("enum: [BOOKING_NO, BILL_OF_LADING, CONTAINER_NO]")
|
||||
.doesNotContain("/{queryId}/attempts:")
|
||||
.doesNotContain("port-schedule-query:attempt");
|
||||
}
|
||||
|
||||
+32
-1
@@ -19,6 +19,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
|
||||
class CarrierScheduleNodeOutboxServiceTest {
|
||||
private final InboundOutboxMapper mapper = mock(InboundOutboxMapper.class);
|
||||
@@ -51,9 +54,35 @@ class CarrierScheduleNodeOutboxServiceTest {
|
||||
assertThat(envelope.aggregateVersion()).isEqualTo(7);
|
||||
assertThat(envelope.handoverVersion()).isEqualTo(8);
|
||||
assertThat(envelope.payload().path("standardNode").asText()).isEqualTo("PICKUP_READY");
|
||||
assertThat(envelope.payload().path("sourceContainerBookingId").asText())
|
||||
.isEqualTo("2088000000000000003");
|
||||
assertThat(envelope.payload().has("containerBookingId")).isFalse();
|
||||
assertThat(envelope.payload().path("actualAt").asText()).isEqualTo("2026-08-25T01:00:00Z");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCanonicalOrDriftingTrackingIdentity() {
|
||||
CarrierScheduleQueryEntity query = query();
|
||||
query.setTrackingType("CONTAINER_NO"); query.setTrackingNo("zcsu7881970");
|
||||
assertThatThrownBy(() -> service.publish(query, fact(), "ZCSU7881970"))
|
||||
.hasMessageContaining("不一致");
|
||||
|
||||
query.setTrackingNo("MATU4646247");
|
||||
assertThatThrownBy(() -> service.publish(query, fact(), "ZCSU7881970"))
|
||||
.hasMessageContaining("不一致");
|
||||
}
|
||||
|
||||
@Test
|
||||
void duplicateEventKeyWithDifferentPayloadFailsClosed() {
|
||||
InboundOutboxEntity existing = new InboundOutboxEntity(); existing.setPayloadHash("different");
|
||||
when(mapper.insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)))
|
||||
.thenThrow(new DuplicateKeyException("duplicate"));
|
||||
when(mapper.selectByEventKey(org.mockito.ArgumentMatchers.anyString())).thenReturn(existing);
|
||||
|
||||
assertThatThrownBy(() -> service.publish(query(), fact(), "ZCSU7881970"))
|
||||
.hasMessageContaining("eventKey").hasMessageContaining("漂移");
|
||||
}
|
||||
|
||||
@Test
|
||||
void estimatedOnlyFactNeverEntersCrossRegionOutbox() {
|
||||
CarrierScheduleNodeFactEntity fact = fact(); fact.setActualAt(null);
|
||||
@@ -70,7 +99,8 @@ class CarrierScheduleNodeOutboxServiceTest {
|
||||
fact.setProviderCode("FIRSTLEG"); fact.setProviderEventId("OUTBOUND_CONFIRM");
|
||||
fact.setProviderNodeCode("OUTBOUND_CONFIRM");
|
||||
|
||||
service.publish(query, fact, "CSNU1234567");
|
||||
query.setTrackingNo("MATU4646247");
|
||||
service.publish(query, fact, "MATU4646247");
|
||||
|
||||
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
|
||||
verify(mapper).insert(captor.capture());
|
||||
@@ -84,6 +114,7 @@ class CarrierScheduleNodeOutboxServiceTest {
|
||||
CarrierScheduleQueryEntity value = new CarrierScheduleQueryEntity();
|
||||
value.setId(2088000000000000001L); value.setCustomerId(2088000000000000002L);
|
||||
value.setContainerBookingId(2088000000000000003L); value.setOceanBookingId(2088000000000000004L);
|
||||
value.setTrackingType("CONTAINER_NO"); value.setTrackingNo("ZCSU7881970");
|
||||
value.setVersion(7); return value;
|
||||
}
|
||||
|
||||
|
||||
+58
-9
@@ -76,13 +76,17 @@ class CarrierScheduleQueryServiceTest {
|
||||
void createsTaskOnlyForBookingCustomerAndAllocatedContainer() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(new com.baiyue.firstleg.entity.ContainerBookingEntity());
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
var result = service.create(7L, "admin", "carrier-create",
|
||||
new CreateRequest(0, 10001L, 10L, 20L, "ZIM", "BILL_OF_LADING", "GAOU6898053", null));
|
||||
new CreateRequest(0, 10001L, 10L, 20L, "ZIM", "CONTAINER_NO", "ZCSU7881970", null));
|
||||
|
||||
assertThat(result.query().query().carrierCode()).isEqualTo("ZIM");
|
||||
assertThat(result.query().query().trackingType()).isEqualTo("CONTAINER_NO");
|
||||
assertThat(result.query().query().trackingNo()).isEqualTo("ZCSU7881970");
|
||||
assertThat(result.query().query().status()).isEqualTo("READY");
|
||||
assertThat(result.query().expired()).isTrue();
|
||||
ArgumentCaptor<CarrierScheduleQueryEntity> created = ArgumentCaptor.forClass(CarrierScheduleQueryEntity.class);
|
||||
@@ -90,12 +94,57 @@ class CarrierScheduleQueryServiceTest {
|
||||
assertThat(created.getValue().getNextAllowedAt()).isEqualTo(LocalDateTime.of(2026, 8, 10, 12, 30));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundRequiresPhysicalContainerNumberAndNeverFallsBackToBookingOrBillOfLading() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setCarrierCode("ZIM"); booking.setBookingNo("OB-1"); booking.setBillOfLadingNo("BL-1");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(new com.baiyue.firstleg.entity.ContainerBookingEntity());
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.ensureFromOutboundConfirm(10001L, 10L, 20L, 0L, "SYSTEM"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("规范化真实集装箱号");
|
||||
verify(repository, never()).insert(any(CarrierScheduleQueryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualCreateAlsoRejectsBookingOrBillOfLadingTrackingKeys() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.create(7L, "admin", "carrier-create-bol",
|
||||
new CreateRequest(0, 10001L, 10L, 20L, "ZIM", "BILL_OF_LADING", "BL-1", null)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("仅支持CONTAINER_NO");
|
||||
verify(repository, never()).insert(any(CarrierScheduleQueryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundReplayFailsClosedWhenFrozenTrackingNumberDrifts() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
CarrierScheduleQueryEntity existing = new CarrierScheduleQueryEntity();
|
||||
existing.setTrackingType("CONTAINER_NO"); existing.setTrackingNo("ZCSU0000000");
|
||||
when(repository.findByBusinessScope(10001L, 10L, 20L)).thenReturn(existing);
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.ensureFromOutboundConfirm(10001L, 10L, 20L, 0L, "SYSTEM"))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("柜号").hasMessageContaining("漂移");
|
||||
}
|
||||
|
||||
@Test
|
||||
void outboundConfirmationCreatesTaskWithContainerTrackingAndDepartureNode() {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("中远海运"); booking.setCarrierCode("COSCO"); booking.setBookingNo("OB-260810-0001");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
@@ -121,7 +170,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
&& "OUTBOUND_CONFIRM".equals(fact.getProviderEventId())
|
||||
&& "OUTBOUND_CONFIRM".equals(fact.getProviderNodeCode())
|
||||
&& "DEPARTED_WAREHOUSE".equals(fact.getStandardNode())
|
||||
&& fact.getActualAt() != null), eq("CSNU1234567"));
|
||||
&& fact.getActualAt() != null), eq("ZCSU7881970"));
|
||||
verify(repository).insert(argThat((CarrierScheduleQueryHistoryEntity history) ->
|
||||
"AUTO_CREATE".equals(history.getAction())
|
||||
&& history.getFromVersion() == null
|
||||
@@ -136,7 +185,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyId(9988L); booking.setShippingCompanyName("示例船公司"); booking.setCarrierCode("OTHER");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
@@ -176,7 +225,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("示例船公司"); booking.setCarrierCode("OTHER");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
@@ -190,7 +239,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
verify(repository, times(1)).insert(any(CarrierScheduleQueryEntity.class));
|
||||
verify(repository, times(1)).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(nodeOutbox, times(1)).publish(any(CarrierScheduleQueryEntity.class),
|
||||
any(CarrierScheduleNodeFactEntity.class), eq("CSNU1234567"));
|
||||
any(CarrierScheduleNodeFactEntity.class), eq("ZCSU7881970"));
|
||||
verify(repository, times(1)).insert(any(CarrierScheduleQueryHistoryEntity.class));
|
||||
}
|
||||
|
||||
@@ -199,7 +248,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("示例船公司"); booking.setCarrierCode("OTHER");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
@@ -234,7 +283,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
|
||||
booking.setShippingCompanyName("示例船公司"); booking.setCarrierCode("OTHER");
|
||||
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||
container.setContainerNo("CSNU1234567");
|
||||
container.setContainerNo("ZCSU7881970");
|
||||
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
|
||||
when(portSchedules.containerBooking(20L)).thenReturn(container);
|
||||
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
|
||||
|
||||
+31
-11
@@ -10,6 +10,8 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -73,21 +75,39 @@ class ContainerBookingMapperContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("草稿完整更新和预装运输白名单都带版本与状态门禁")
|
||||
void guardsUpdatesInSql() {
|
||||
String allSql = Arrays.stream(ContainerBookingMapper.class.getDeclaredMethods())
|
||||
.map(method -> method.getAnnotation(Update.class))
|
||||
.filter(annotation -> annotation != null)
|
||||
.flatMap(annotation -> Arrays.stream(annotation.value()))
|
||||
.reduce("", (left, right) -> left + "\n" + right);
|
||||
assertThat(allSql)
|
||||
@DisplayName("货柜草稿更新不得形成真实柜号或内部柜次号写入口")
|
||||
void onlyOceanBookingLinkWritesInitialContainerNumber() throws Exception {
|
||||
String draftSql = String.join("\n", ContainerBookingMapper.class
|
||||
.getDeclaredMethod("updateDraft", ContainerBookingEntity.class, int.class,
|
||||
long.class, String.class).getAnnotation(Update.class).value());
|
||||
String linkSql = String.join("\n", Arrays.stream(ContainerBookingMapper.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals("linkReleasedBooking"))
|
||||
.findFirst().orElseThrow().getAnnotation(Update.class).value());
|
||||
assertThat(draftSql)
|
||||
.contains("capacity_cbm")
|
||||
.contains("p.plan_status!='EDITING'")
|
||||
.contains("p.plan_status='COMPLETED'")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task")
|
||||
.contains("version=#{expectedVersion}")
|
||||
.doesNotContain("container_no=", "cabinet_no=")
|
||||
.doesNotContain("<")
|
||||
.doesNotContain(">");
|
||||
assertThat(linkSql).contains("container_no=#{containerNo}", "source_type='SIMULATION'",
|
||||
"ocean_booking_id IS NULL");
|
||||
assertThat(Arrays.stream(ContainerBookingMapper.class.getDeclaredMethods()).map(java.lang.reflect.Method::getName))
|
||||
.doesNotContain("updateTransport", "unlinkReleasedBooking");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("初始化与统一增量锁定真实柜号权威、唯一和ISO结构约束")
|
||||
void databaseContractsFreezeAuthoritativeContainerNumber() throws Exception {
|
||||
for (String source : new String[] {
|
||||
Files.readString(Path.of("src/main/resources/db/phase3-smart-loading-schema.sql")),
|
||||
Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))
|
||||
}) {
|
||||
assertThat(source)
|
||||
.contains("国内唯一权威真实集装箱号")
|
||||
.contains("UNIQUE KEY uk_container_booking_container_no (container_no)")
|
||||
.contains("CONSTRAINT chk_container_booking_iso6346")
|
||||
.contains("container_no REGEXP '^[A-Z]{3}[UJZ][0-9]{7}$'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
@@ -38,6 +38,15 @@ class ContainerOverviewSqlContractTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void transportTimelineReadsTheSingleLoadingPlanCorrectionAudit() throws Exception {
|
||||
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("transport", long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertThat(sql)
|
||||
.contains("fl_loading_plan_history", "action_type='UPDATE_PRELOAD_TRANSPORT'")
|
||||
.contains("before_snapshot", "after_snapshot", "operation_reason")
|
||||
.doesNotContain("fl_container_transport_history");
|
||||
}
|
||||
|
||||
@Test void developmentAndUnifiedSchemasContainImmutableIdempotentExceptionHistory() throws Exception {
|
||||
String development = new ClassPathResource("db/phase3-smart-loading-schema.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
+4
-3
@@ -76,16 +76,17 @@ class LoadingListQueryMapperContractTest {
|
||||
String advance = sql("advance", Update.class);
|
||||
|
||||
assertThat(touch)
|
||||
.contains("plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("plan_status = 'EDITING'")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("fl_loading_pick_assignment", "item.item_status<>'PENDING'")
|
||||
.contains("version = #{expectedVersion}")
|
||||
.contains("version = version + 1");
|
||||
assertThat(transport)
|
||||
.contains("SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo}")
|
||||
.contains("plan_status IN ('EDITING', 'LOADING')")
|
||||
.contains("p.plan_status='EDITING'")
|
||||
.contains("preload_pushed_at IS NOT NULL")
|
||||
.contains("#{containerNo} IS NOT NULL")
|
||||
.contains("fl_loading_pick_task")
|
||||
.contains("fl_loading_pick_task", "fl_loading_pick_assignment")
|
||||
.doesNotContain("DELETE")
|
||||
.doesNotContain("fl_loading_plan_item");
|
||||
assertThat(advance)
|
||||
|
||||
+1
-1
@@ -177,7 +177,7 @@ class SmartLoadingSqlContractTest {
|
||||
.contains("三期入库装柜角色授权未完整写入")
|
||||
.contains("permission.permission_code IN (")
|
||||
.contains("role.role_code IN ('PLATFORM_ADMIN', 'ORDER_OPERATOR')")
|
||||
.contains("permission.permission_code IN (\n 'firstleg:inbound:split'")
|
||||
.contains("'firstleg:inbound:split', 'firstleg:inbound:preload'")
|
||||
.contains(")) <> 18 THEN");
|
||||
}
|
||||
|
||||
|
||||
@@ -8,16 +8,8 @@ import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingUnlinkRequest;
|
||||
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPlanEntity;
|
||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||
import com.baiyue.firstleg.mapper.ContainerTransportHistoryMapper;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||
import com.baiyue.firstleg.pricebookquery.application.model.PriceBookQueryRows.ChannelRow;
|
||||
import com.baiyue.firstleg.pricebookquery.application.port.in.PriceBookQueryUseCase;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
@@ -49,14 +41,10 @@ import static org.mockito.Mockito.when;
|
||||
class ContainerServiceTest {
|
||||
@MockBean private ContainerRepository repository;
|
||||
@MockBean private WarehouseMasterDataClient warehouses;
|
||||
@MockBean private SmartLoadingRepository smartLoadingRepository;
|
||||
@MockBean private LoadingPickRepository loadingPickRepository;
|
||||
@MockBean private LoadingPickService loadingPickService;
|
||||
@MockBean private PriceBookQueryUseCase priceBooks;
|
||||
@MockBean private OceanBookingRepository oceanBookings;
|
||||
@MockBean private WarehouseSupplierSnapshotService suppliers;
|
||||
@MockBean private FirstlegDocumentNumberGenerator documentNumbers;
|
||||
@MockBean private ContainerTransportHistoryMapper transportHistories;
|
||||
@Resource private ContainerService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -140,152 +128,12 @@ class ContainerServiceTest {
|
||||
assertThatThrownBy(() -> service.updateDraft(
|
||||
11L, 9L, "admin", new FullUpdateRequest(2, request())))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("只能修改集装箱号和航次");
|
||||
.hasMessageContaining("基础资料不可完整编辑");
|
||||
|
||||
verify(repository, never()).updateDraft(any(), any(Integer.class),
|
||||
any(Long.class), any(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("预装柜中和预装柜完成只更新集装箱号与航次")
|
||||
void updatesTransportWhitelistAfterPush() {
|
||||
for (String status : new String[] {"LOADING"}) {
|
||||
ContainerBookingEntity current = booking(status, 2);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
|
||||
service.updateTransport(11L, 9L, "admin", "transport-1",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2));
|
||||
}
|
||||
|
||||
verify(repository, org.mockito.Mockito.times(1))
|
||||
.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin");
|
||||
ArgumentCaptor<ContainerTransportHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(ContainerTransportHistoryEntity.class);
|
||||
verify(transportHistories).insert(history.capture());
|
||||
assertThat(history.getValue().getBeforeContainerNo()).isEqualTo("MSCU1234567");
|
||||
assertThat(history.getValue().getAfterVoyageNo()).isEqualTo("V002");
|
||||
assertThat(history.getValue().getOperationId()).hasSize(64);
|
||||
assertThat(history.getValue().getRequestHash()).hasSize(64);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运输字段没有有效变化时不递增版本且不写历史")
|
||||
void ignoresTransportNoOp() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
current.setVoyageNo("V002");
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
|
||||
ContainerBookingEntity result = service.updateTransport(11L, 9L, "admin", "transport-noop",
|
||||
new TransportUpdateRequest(" MSCU1234567 ", " V002 ", 2));
|
||||
|
||||
assertThat(result).isSameAs(current);
|
||||
verify(repository, never()).updateTransport(anyLong(), any(), any(), anyInt(), anyLong(), any());
|
||||
verify(transportHistories, never()).insert(any(ContainerTransportHistoryEntity.class));
|
||||
verifyNoInteractions(loadingPickService);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("运输字段精确幂等重放零新增")
|
||||
void replaysTransportWithoutSecondMutationOrHistory() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
ContainerBookingEntity updated = booking("LOADING", 3);
|
||||
updated.setVoyageNo("V002");
|
||||
when(repository.find(11L)).thenReturn(current, updated, updated);
|
||||
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
ArgumentCaptor<ContainerTransportHistoryEntity> history =
|
||||
ArgumentCaptor.forClass(ContainerTransportHistoryEntity.class);
|
||||
|
||||
TransportUpdateRequest request = new TransportUpdateRequest("MSCU1234567", "V002", 2);
|
||||
service.updateTransport(11L, 9L, "admin", "transport-replay", request);
|
||||
verify(transportHistories).insert(history.capture());
|
||||
when(transportHistories.selectOperation(eq(11L), eq(9L), anyString()))
|
||||
.thenReturn(history.getValue());
|
||||
|
||||
ContainerBookingEntity replay = service.updateTransport(
|
||||
11L, 9L, "admin", "transport-replay", request);
|
||||
|
||||
assertThat(replay.getVersion()).isEqualTo(3);
|
||||
verify(repository, org.mockito.Mockito.times(1))
|
||||
.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin");
|
||||
verify(transportHistories, org.mockito.Mockito.times(1))
|
||||
.insert(any(ContainerTransportHistoryEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("预装清单推送后再绑定集装箱号会创建PDA拣货任务")
|
||||
void startsPdaTaskWhenContainerIsBoundAfterPreloadPush() {
|
||||
ContainerBookingEntity current = booking("EDITING", 2);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(31L);
|
||||
plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0));
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateTransport(11L, "MSCU1234567", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
|
||||
service.updateTransport(11L, 9L, "admin", "transport-2",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2));
|
||||
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PDA任务创建后替换真实柜号不重同步任务明细")
|
||||
void doesNotResynchronizeExistingPdaTaskWhenContainerNumberChanges() {
|
||||
ContainerBookingEntity current = booking("EDITING", 2);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(31L);
|
||||
plan.setPlanStatus("EDITING");
|
||||
plan.setPreloadPushedAt(LocalDateTime.of(2026, 8, 11, 20, 0));
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(repository.updateTransport(11L, "MSCU7654321", "V002", 2, 9L, "admin"))
|
||||
.thenReturn(1);
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
service.updateTransport(11L, 9L, "admin", "transport-3",
|
||||
new TransportUpdateRequest("MSCU7654321", "V002", 2));
|
||||
|
||||
verify(loadingPickService).beginIfReady(11L, 9L, "admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PDA任务创建后禁止清空真实集装箱号")
|
||||
void rejectsClearingContainerNumberAfterPdaTaskStarts() {
|
||||
ContainerBookingEntity current = booking("LOADING", 2);
|
||||
LoadingPlanEntity plan = new LoadingPlanEntity();
|
||||
plan.setId(31L);
|
||||
when(repository.find(11L)).thenReturn(current);
|
||||
when(smartLoadingRepository.planByBooking(11L)).thenReturn(plan);
|
||||
when(loadingPickRepository.taskByPlan(31L)).thenReturn(new LoadingPickTaskEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin", "transport-4",
|
||||
new TransportUpdateRequest(null, "V002", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不能清空真实集装箱号");
|
||||
|
||||
verify(repository, never()).updateTransport(any(Long.class), any(), any(),
|
||||
any(Integer.class), any(Long.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("已确认货柜全部只读")
|
||||
void rejectsConfirmedUpdate() {
|
||||
when(repository.find(11L)).thenReturn(booking("COMPLETED", 2));
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(11L, 9L, "admin", "transport-5",
|
||||
new TransportUpdateRequest("MSCU1234567", "V002", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("全部只读");
|
||||
|
||||
verify(repository, never()).updateTransport(any(Long.class), any(), any(),
|
||||
any(Integer.class), any(Long.class), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("旧版本编辑请求触发乐观锁冲突")
|
||||
void rejectsStaleVersion() {
|
||||
@@ -339,6 +187,7 @@ class ContainerServiceTest {
|
||||
@DisplayName("首次关联允许已有预排货的编辑计划且保持内部柜和计划身份")
|
||||
void linksReleasedBookingAfterPreloading() {
|
||||
ContainerBookingEntity container = booking("EDITING", 2);
|
||||
container.setContainerNo(null);
|
||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US"); container.setContainerType("40HC");
|
||||
container.setCapacityCbm(new BigDecimal("85.5000")); container.setMaxWeightKg(new BigDecimal("27500.000"));
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
@@ -349,20 +198,44 @@ class ContainerServiceTest {
|
||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
||||
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU7654321"),
|
||||
when(repository.linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
||||
same(booking), eq(2), eq(9L), eq("admin"))).thenReturn(1);
|
||||
|
||||
ContainerBookingEntity result = service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2));
|
||||
new OceanBookingLinkRequest(21L, 31L, " ms cu 663987 0 ", 2));
|
||||
|
||||
assertThat(result).isSameAs(container);
|
||||
assertThat(container.getCabinetNo()).isEqualTo("柜-001");
|
||||
verify(repository, never()).hasActiveLoadingItems(11L);
|
||||
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU7654321"),
|
||||
verify(repository).linkReleasedBooking(eq(11L), eq(21L), eq(31L), eq("MSCU6639870"),
|
||||
same(booking), eq(2), eq(9L), eq("admin"));
|
||||
verify(oceanBookings).insert(any(OceanBookingContainerAllocationEntity.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝错误ISO 6346校验位")
|
||||
void rejectsInvalidContainerNumberCheckDigit() {
|
||||
stubLinkContext();
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639871", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("校验位");
|
||||
|
||||
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
||||
any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联规范化后按全局唯一真实柜号拒绝冲突")
|
||||
void rejectsNormalizedContainerNumberConflict() {
|
||||
stubLinkContext();
|
||||
when(repository.containerNoExists("MSCU6639870", 11L)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "ms cu6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联拒绝装柜仓库不一致")
|
||||
void rejectsLinkWhenWarehouseDiffers() {
|
||||
@@ -385,13 +258,14 @@ class ContainerServiceTest {
|
||||
@DisplayName("未发布订舱不可关联")
|
||||
void rejectsDraftBooking() {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setContainerNo(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(21L); booking.setStatus("DRAFT");
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(oceanBookings.find(21L)).thenReturn(booking);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已发布");
|
||||
}
|
||||
|
||||
@@ -399,6 +273,7 @@ class ContainerServiceTest {
|
||||
@DisplayName("订舱箱型需求达到数量上限时拒绝关联")
|
||||
void rejectsRequirementOverAllocation() {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setContainerNo(null);
|
||||
OceanBookingEntity booking = new OceanBookingEntity();
|
||||
booking.setId(21L); booking.setStatus("RELEASED");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
@@ -409,7 +284,7 @@ class ContainerServiceTest {
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(1L);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("数量上限");
|
||||
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
||||
any(), anyInt(), anyLong(), anyString());
|
||||
@@ -418,6 +293,7 @@ class ContainerServiceTest {
|
||||
private void assertThreeFieldMismatch(long bookingWarehouseId, String bookingChannel,
|
||||
String requirementType) {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setContainerNo(null);
|
||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US");
|
||||
container.setContainerType("40HC");
|
||||
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
||||
@@ -431,38 +307,38 @@ class ContainerServiceTest {
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("仓库、渠道或柜型不一致");
|
||||
verify(repository, never()).linkReleasedBooking(anyLong(), anyLong(), anyLong(), anyString(),
|
||||
any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("完全空的模拟柜直接解绑并删除当前allocation,不依赖MERGED来源柜")
|
||||
void unlinksEmptySimulationDirectly() {
|
||||
ContainerBookingEntity linked = booking("WAIT_LOADING", 2);
|
||||
linked.setOceanBookingId(21L);
|
||||
ContainerBookingEntity detached = booking("WAIT_LOADING", 3);
|
||||
when(repository.find(11L)).thenReturn(linked, detached);
|
||||
when(repository.unlinkReleasedBooking(11L, 2, 9L, "admin")).thenReturn(1);
|
||||
|
||||
ContainerBookingEntity result = service.unlinkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingUnlinkRequest(2, "换绑"));
|
||||
|
||||
assertThat(result).isSameAs(detached);
|
||||
verify(repository).unlinkReleasedBooking(11L, 2, 9L, "admin");
|
||||
verify(oceanBookings).deleteAllocation(11L);
|
||||
private void stubLinkContext() {
|
||||
ContainerBookingEntity container = booking("WAIT_LOADING", 2);
|
||||
container.setContainerNo(null);
|
||||
container.setWarehouseId(101L); container.setChannelCode("SEA-US");
|
||||
container.setContainerType("40HC");
|
||||
OceanBookingEntity oceanBooking = new OceanBookingEntity();
|
||||
oceanBooking.setId(21L); oceanBooking.setStatus("RELEASED");
|
||||
oceanBooking.setWarehouseId(101L); oceanBooking.setChannelCode("SEA-US");
|
||||
OceanBookingContainerRequirementEntity requirement = new OceanBookingContainerRequirementEntity();
|
||||
requirement.setId(31L); requirement.setQuantity(2); requirement.setContainerType("40HC");
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(oceanBookings.find(21L)).thenReturn(oceanBooking);
|
||||
when(oceanBookings.requirement(21L, 31L)).thenReturn(requirement);
|
||||
when(oceanBookings.activeLinkedCount(21L, 31L)).thenReturn(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("首次关联遇到有效装柜证据时拒绝")
|
||||
void rejectsLinkWhenEvidenceExists() {
|
||||
ContainerBookingEntity container = booking("EDITING", 2);
|
||||
container.setContainerNo(null);
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(repository.hasEvidence(11L)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.linkOceanBooking(11L, 9L, "admin",
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU7654321", 2)))
|
||||
new OceanBookingLinkRequest(21L, 31L, "MSCU6639870", 2)))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("证据");
|
||||
verifyNoInteractions(oceanBookings);
|
||||
}
|
||||
@@ -485,7 +361,7 @@ class ContainerServiceTest {
|
||||
}
|
||||
|
||||
private static SaveRequest request() {
|
||||
return new SaveRequest("MSCU1234567", 101L, "上海仓", "BOL-001", "柏悦轮",
|
||||
return new SaveRequest(101L, "上海仓", "BOL-001", "柏悦轮",
|
||||
"V001", "40HC", 201L, "船司A", "CNSHA", "上海港", "USLAX", "洛杉矶港",
|
||||
"ONT8", "ONT8", new BigDecimal("76.0000"), new BigDecimal("28000.000"), 202L, "拖车公司A",
|
||||
LocalDateTime.of(2026, 8, 10, 8, 0), LocalDateTime.of(2026, 8, 12, 8, 0), null,
|
||||
|
||||
+27
-1
@@ -72,10 +72,36 @@ class LoadingCompletedOutboxServiceTest {
|
||||
assertThat(first.payload().at("/estimatedPortArrivalAt").asText()).isEqualTo("2026-09-30T12:00");
|
||||
assertThat(first.payload().at("/actualPortArrivalAt").asText()).isEqualTo("2026-10-01T08:30");
|
||||
assertThat(first.payload().at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
|
||||
assertThat(first.payload().at("/sourceContainerBookingId").asText()).isEqualTo("91");
|
||||
assertThat(first.payload().has("containerBookingId")).isFalse();
|
||||
assertThat(rows.get(0).getEventKey()).isEqualTo("FIRSTLEG_LOADING_COMPLETED:90:11:2");
|
||||
assertThat(first.eventVersion()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test void requiresFrozenPhysicalContainerNumberBeforePublishing() {
|
||||
Row value = row(11, 100, 101); value.setContainerNo(" ");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
assertThatThrownBy(() -> service.publish(90L))
|
||||
.hasMessageContaining("真实集装箱号");
|
||||
verify(outbox, org.mockito.Mockito.never()).insert(
|
||||
org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
|
||||
}
|
||||
|
||||
@Test void rejectsNonCanonicalContainerNumberAndDuplicateEventKeyPayloadDrift() {
|
||||
Row value = row(11, 100, 101); value.setContainerNo(" zcsu7881970 ");
|
||||
when(snapshots.rows(90L)).thenReturn(List.of(value));
|
||||
assertThatThrownBy(() -> service.publish(90L)).hasMessageContaining("真实集装箱号");
|
||||
|
||||
value.setContainerNo("ZCSU7881970");
|
||||
InboundOutboxEntity existing = new InboundOutboxEntity(); existing.setPayloadHash("different");
|
||||
org.mockito.Mockito.when(outbox.insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)))
|
||||
.thenThrow(new org.springframework.dao.DuplicateKeyException("duplicate"));
|
||||
org.mockito.Mockito.when(outbox.selectByEventKey(org.mockito.ArgumentMatchers.anyString()))
|
||||
.thenReturn(existing);
|
||||
assertThatThrownBy(() -> service.publish(90L))
|
||||
.hasMessageContaining("eventKey").hasMessageContaining("漂移");
|
||||
}
|
||||
|
||||
@Test void rejectsOrdinaryItemWhenSkuToCartonMappingCannotBeBuilt() {
|
||||
Row value = row(11, 100, 101);
|
||||
value.setSkuCode(null);
|
||||
@@ -295,7 +321,7 @@ class LoadingCompletedOutboxServiceTest {
|
||||
|
||||
private Row row(long account,long inbound,long detail) {
|
||||
Row r=new Row(); r.setLoadingPlanId(90L); r.setPlanNo("P90"); r.setContainerBookingId(91L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("CONT"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
r.setAggregateVersion(8); r.setContainerNo("ZCSU7881970"); r.setCabinetNo("CAB"); r.setWarehouseId(92L);
|
||||
r.setWarehouseName("WH"); r.setCustomerName("客户-" + account);
|
||||
r.setEstimatedPortArrivalAt(java.time.LocalDateTime.of(2026, 9, 30, 12, 0));
|
||||
r.setActualPortArrivalAt(java.time.LocalDateTime.of(2026, 10, 1, 8, 30));
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.baiyue.firstleg.repository.ContainerRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -36,6 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
@@ -53,6 +55,8 @@ class LoadingListServiceTest {
|
||||
@MockBean
|
||||
private LoadingPickRepository pickRepository;
|
||||
@MockBean
|
||||
private ContainerRepository containerRepository;
|
||||
@MockBean
|
||||
private LoadingPickService loadingPickService;
|
||||
@MockBean
|
||||
private LoadingDocumentFileClient documentFiles;
|
||||
@@ -80,40 +84,43 @@ class LoadingListServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldOnlyUpdateTransportFieldsWithOptimisticLockAndHistory() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
before.setContainerNo("OLD");
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
before.setVoyageNo("001E");
|
||||
AggregateRow after = row("LOADING", 4);
|
||||
after.setContainerNo("CSLU1234567");
|
||||
AggregateRow after = row("EDITING", 4);
|
||||
after.setPreloadPushedAt(before.getPreloadPushedAt());
|
||||
after.setContainerNo("CSQU3054383");
|
||||
after.setVoyageNo("023E");
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before, after);
|
||||
when(repository.updateTransport(10L, 3, "CSLU1234567", "023E", 9L, "operator"))
|
||||
when(repository.updateTransport(10L, 3, "CSQU3054383", "023E", 9L, "operator"))
|
||||
.thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "key-1",
|
||||
new TransportUpdateRequest(" CSLU1234567 ", "023E", 3, "补录箱号"));
|
||||
new TransportUpdateRequest(" cs qu 305438 3 ", "023E", 3, "纠错箱号"));
|
||||
|
||||
verify(repository).updateTransport(10L, 3, "CSLU1234567", "023E", 9L, "operator");
|
||||
verify(repository).updateTransport(10L, 3, "CSQU3054383", "023E", 9L, "operator");
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("UPDATE_PRELOAD_TRANSPORT"),
|
||||
eq("LOADING"), eq("LOADING"), anyString(), anyString(), eq(9L),
|
||||
eq("operator"), eq("补录箱号"));
|
||||
eq("EDITING"), eq("EDITING"), anyString(), anyString(), eq(9L),
|
||||
eq("operator"), eq("纠错箱号"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotWritePlanOrHistoryWhenTransportFieldsAreUnchanged() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
before.setContainerNo("CSLU1234567");
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
before.setVoyageNo("023E");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(repository.preloadDetail(before)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "noop-key",
|
||||
new TransportUpdateRequest(" CSLU1234567 ", " 023E ", 3, "重复保存"));
|
||||
new TransportUpdateRequest(" ms cu 663987 0 ", " 023E ", 3, "重复保存"));
|
||||
|
||||
verify(repository, never()).updateTransport(anyLong(), org.mockito.ArgumentMatchers.anyInt(),
|
||||
any(), any(), anyLong(), any());
|
||||
@@ -123,29 +130,32 @@ class LoadingListServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushedEditingPreloadCanReceiveContainerNumber() {
|
||||
void pushedEditingPreloadCanCorrectExistingContainerNumberBeforePdaClaim() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
AggregateRow after = row("EDITING", 4);
|
||||
after.setPreloadPushedAt(before.getPreloadPushedAt());
|
||||
after.setContainerNo("CSLU1234567");
|
||||
after.setContainerNo("CSQU3054383");
|
||||
when(repository.aggregate(10L)).thenReturn(before, after);
|
||||
when(repository.updateTransport(10L, 3, "CSLU1234567", null, 9L, "operator")).thenReturn(1);
|
||||
when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator")).thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "editing-key",
|
||||
new TransportUpdateRequest("CSLU1234567", null, 3, "补录柜号"));
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "纠错柜号"));
|
||||
|
||||
verify(loadingPickService).beginIfReady(30L, 9L, "operator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void editingPlanCannotReceiveTransportBeforePush() {
|
||||
when(repository.aggregate(10L)).thenReturn(row("EDITING", 3));
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setContainerNo("MSCU6639870");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "editing-key",
|
||||
new TransportUpdateRequest("CSLU1234567", null, 3, "补录柜号")))
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "纠错柜号")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已推送");
|
||||
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(3), anyString(),
|
||||
@@ -159,7 +169,7 @@ class LoadingListServiceTest {
|
||||
when(repository.aggregate(10L)).thenReturn(row("COMPLETED", 5));
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "key-1",
|
||||
new TransportUpdateRequest("CSLU1234567", "023E", 5, null)))
|
||||
new TransportUpdateRequest("CSQU3054383", "023E", 5, "完成后纠错")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode())
|
||||
.isEqualTo(ResultCode.CONFLICT);
|
||||
@@ -167,22 +177,73 @@ class LoadingListServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRejectClearingContainerNumberAfterPdaTaskStarts() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
void shouldRejectBlankContainerNumberInsteadOfClearingAuthorityField() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(pickRepository.taskByPlan(10L)).thenReturn(new com.baiyue.firstleg.entity.LoadingPickTaskEntity());
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "key-1",
|
||||
new TransportUpdateRequest(null, "023E", 3, "清空测试")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("不能清空真实集装箱号");
|
||||
new TransportUpdateRequest(" ", "023E", 3, "清空测试")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("ISO 6346");
|
||||
verify(repository, never()).updateTransport(eq(10L), eq(3),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class), eq(9L),
|
||||
org.mockito.ArgumentMatchers.nullable(String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void unclaimedPdaTaskStillAllowsCorrectionButClaimOrPhysicalWorkRejectsIt() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(repository.hasPdaWorkStarted(10L)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "claimed-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "领取后纠错")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已领取");
|
||||
|
||||
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadingPlanAndMissingInitialOceanBookingNumberBothRejectCorrection() {
|
||||
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);
|
||||
|
||||
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("首次写入");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsGlobalUniqueConflictIncludingConcurrentConstraintRace() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(containerRepository.containerNoExists("CSQU3054383", 30L)).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "unique-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "唯一冲突")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
||||
|
||||
when(containerRepository.containerNoExists("CSQU3054383", 30L)).thenReturn(false);
|
||||
when(repository.updateTransport(10L, 3, "CSQU3054383", null, 9L, "operator"))
|
||||
.thenThrow(new org.springframework.dao.DuplicateKeyException("uk_container_booking_container_no"));
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "race-key",
|
||||
new TransportUpdateRequest("CSQU3054383", null, 3, "并发唯一冲突")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("已存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompleteLoadingAfterAllPdaItemsPicked() {
|
||||
AggregateRow before = row("LOADING", 2);
|
||||
@@ -220,23 +281,26 @@ class LoadingListServiceTest {
|
||||
|
||||
@Test
|
||||
void shouldReplaySameTransportRequestWithoutSecondMutation() {
|
||||
AggregateRow before = row("LOADING", 3);
|
||||
AggregateRow after = row("LOADING", 4);
|
||||
after.setContainerNo("CSLU1234567");
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
AggregateRow after = row("EDITING", 4);
|
||||
after.setPreloadPushedAt(before.getPreloadPushedAt());
|
||||
after.setContainerNo("CSQU3054383");
|
||||
after.setVoyageNo("023E");
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.aggregate(10L)).thenReturn(before, after);
|
||||
when(repository.updateTransport(10L, 3, "CSLU1234567", "023E", 9L, "operator"))
|
||||
when(repository.updateTransport(10L, 3, "CSQU3054383", "023E", 9L, "operator"))
|
||||
.thenReturn(1);
|
||||
when(repository.preloadDetail(after)).thenReturn(
|
||||
new com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail(null, List.of(), List.of()));
|
||||
ArgumentCaptor<String> snapshot = ArgumentCaptor.forClass(String.class);
|
||||
|
||||
TransportUpdateRequest request = new TransportUpdateRequest("CSLU1234567", "023E", 3, null);
|
||||
TransportUpdateRequest request = new TransportUpdateRequest("CSQU3054383", "023E", 3, "纠错");
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "key-1", request);
|
||||
verify(repository).appendHistory(eq(10L), anyString(), eq("UPDATE_PRELOAD_TRANSPORT"),
|
||||
eq("LOADING"), eq("LOADING"), anyString(), snapshot.capture(), eq(9L),
|
||||
eq("operator"), eq(null));
|
||||
eq("EDITING"), eq("EDITING"), anyString(), snapshot.capture(), eq(9L),
|
||||
eq("operator"), eq("纠错"));
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot(snapshot.getValue());
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
@@ -244,7 +308,24 @@ class LoadingListServiceTest {
|
||||
|
||||
service.updateTransport(10L, 100L, 9L, "operator", "key-1", request);
|
||||
|
||||
verify(repository, times(1)).updateTransport(10L, 3, "CSLU1234567", "023E", 9L, "operator");
|
||||
verify(repository, times(1)).updateTransport(10L, 3, "CSQU3054383", "023E", 9L, "operator");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reusedTransportKeyWithDifferentPayloadFailsBeforeMutation() {
|
||||
AggregateRow before = row("EDITING", 3);
|
||||
before.setPreloadPushedAt(java.time.LocalDateTime.now());
|
||||
before.setContainerNo("MSCU6639870");
|
||||
HistoryRow replay = new HistoryRow();
|
||||
replay.setAfterSnapshot("{\"requestHash\":\"different\"}");
|
||||
when(repository.aggregate(10L)).thenReturn(before);
|
||||
when(repository.history(anyString())).thenReturn(replay);
|
||||
|
||||
assertThatThrownBy(() -> service.updateTransport(10L, 100L, 9L, "operator", "reused-key",
|
||||
new TransportUpdateRequest("CSQU3054383", "023E", 3, "纠错")))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不同请求");
|
||||
|
||||
verify(repository, never()).updateTransport(anyLong(), anyInt(), any(), any(), anyLong(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-4
@@ -48,7 +48,7 @@ class OverseasContainerEventConsumerTest {
|
||||
Instant uploadedAt = Instant.parse("2026-08-20T02:30:00Z");
|
||||
|
||||
consumer.consume(event("00000000-0000-4000-8000-000000000001", 7,
|
||||
new OverseasContainerUnloadedV1("1001", "MSCU1234567", completedAt, uploadedAt, null)));
|
||||
new OverseasContainerUnloadedV1("1001", "ZCSU7881970", completedAt, uploadedAt, null)));
|
||||
|
||||
verify(containers).applyEmptyReport(eq(1001L), eq(LocalDateTime.parse("2026-08-20T03:00:00")),
|
||||
eq(LocalDateTime.parse("2026-08-20T02:30:00")),
|
||||
@@ -62,7 +62,7 @@ class OverseasContainerEventConsumerTest {
|
||||
|
||||
@Test
|
||||
void repeatedEventWithSameHashDoesNotApplyAgain() {
|
||||
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("1001", "MSCU1234567",
|
||||
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("1001", "ZCSU7881970",
|
||||
Instant.parse("2026-08-20T03:00:00Z"), Instant.parse("2026-08-20T02:30:00Z"), null);
|
||||
EventEnvelope event = event("00000000-0000-4000-8000-000000000002", 7, payload);
|
||||
ContainerEventInboxEntity existing = new ContainerEventInboxEntity();
|
||||
@@ -79,7 +79,7 @@ class OverseasContainerEventConsumerTest {
|
||||
@Test
|
||||
void rejectsTamperedPayloadHash() {
|
||||
EventEnvelope valid = event("00000000-0000-4000-8000-000000000003", 7,
|
||||
new OverseasContainerUnloadedV1("1001", "MSCU1234567", Instant.parse("2026-08-20T03:00:00Z"),
|
||||
new OverseasContainerUnloadedV1("1001", "ZCSU7881970", Instant.parse("2026-08-20T03:00:00Z"),
|
||||
Instant.parse("2026-08-20T02:30:00Z"), null));
|
||||
EventEnvelope tampered = new EventEnvelope(valid.eventId(), valid.eventType(), valid.eventVersion(),
|
||||
valid.occurredAt(), valid.sourceRegion(), valid.targetRegion(), valid.tenantId(), valid.aggregateType(),
|
||||
@@ -102,7 +102,7 @@ class OverseasContainerEventConsumerTest {
|
||||
|
||||
private ContainerBookingEntity container() {
|
||||
ContainerBookingEntity value = new ContainerBookingEntity();
|
||||
value.setId(1001L); value.setContainerNo("MSCU1234567");
|
||||
value.setId(1001L); value.setContainerNo("ZCSU7881970");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user