This commit is contained in:
2026-08-20 18:02:38 +08:00
parent b55b049e77
commit 1cd8b6a473
210 changed files with 6974 additions and 825 deletions
@@ -0,0 +1,21 @@
package com.baiyue.firstleg.controller;
import com.baiyue.firstleg.dto.ContainerOverviewDtos.ContainerBookingEntityView;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class ContainerOverviewTransportContractTest {
@Test
void overviewPublishesMaritimeAndEmptyReportFacts() {
assertThat(Arrays.stream(ContainerBookingEntityView.class.getRecordComponents()).map(value -> value.getName()))
.contains("billOfLadingNo", "shippingCompanyId", "shippingCompanyName", "departurePortCode",
"departurePortName", "destinationPortCode", "destinationPortName", "estimatedSailingAt",
"actualSailingAt", "estimatedPortArrivalAt", "actualPortArrivalAt",
"estimatedWarehouseArrivalAt", "actualWarehouseArrivalAt", "unloadingCompletedAt",
"unloadingPhotoUploadedAt", "estimatedEmptyReportedAt", "actualEmptyReportedAt",
"emptyReportSourceVersion");
}
}
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.outbound.dispatch.gate.OutboundDispatchGateContributo
import com.baiyue.firstleg.outbound.dispatch.mapper.OutboundDispatchReadMapper;
import com.baiyue.firstleg.outbound.dispatch.repository.OutboundDispatchRepository;
import com.baiyue.firstleg.outbound.dispatch.service.OutboundDispatchService;
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
import org.junit.jupiter.api.Test;
import java.util.List;
@@ -67,6 +68,26 @@ class OutboundDispatchServiceTest {
verify(repository).completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-1");
}
@Test
void confirmCreatesCanonicalCarrierScheduleTaskInTheSameTransaction() {
CarrierScheduleQueryService carrierSchedules = mock(CarrierScheduleQueryService.class);
OutboundDispatchService service = new OutboundDispatchService(repository,
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
stubReadyChain();
OutboundDispatchEntity state = new OutboundDispatchEntity();
state.setId(33L); state.setTenantId(7L); state.setInboundId(11L);
state.setContainerBookingId(22L); state.setStatus("READY_FOR_DISPATCH"); state.setVersion(0);
when(repository.dispatch(7L, 11L, 22L)).thenReturn(state);
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("confirm-carrier"), anyString())).thenReturn(true);
when(repository.confirm(33L, 0, 9L, "admin", null)).thenReturn(1);
when(repository.completeOperation(33L, 9L, "CONFIRM_DISPATCH", "confirm-carrier")).thenReturn(1);
when(repository.timeline(any(), any(), any())).thenReturn(List.of());
service.confirm(9L, "admin", "confirm-carrier", new ActionRequest(11L, 22L, 0, null));
verify(carrierSchedules).ensureFromOutboundConfirm(7L, 66L, 22L, 9L, "admin");
}
@Test
void confirmNeverWritesStateWhenAnyRealGateFails() {
OutboundDispatchService service = new OutboundDispatchService(repository,
@@ -9,19 +9,17 @@ import static org.assertj.core.api.Assertions.assertThat;
class CarrierScheduleQueryOpenApiContractTest {
@Test
void publishesFrequencyFreshnessAndManualFallbackContract() throws Exception {
void publishesBusinessNodesWithoutSchedulerOrAttemptEndpoints() throws Exception {
String yaml = new ClassPathResource("openapi/carrier-schedule-query-v1.yaml")
.getContentAsString(StandardCharsets.UTF_8);
assertThat(yaml)
.contains("/api/platform/v1/carrier-schedule-queries:")
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/attempts:")
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-results:")
.contains("enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]")
.contains("DEPARTED_WAREHOUSE, AT_ORIGIN_PORT, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED, PICKUP_READY")
.contains("MANUAL_REQUIRED")
.contains("coverageGap")
.contains("lastAttemptAt")
.contains("lastSuccessAt")
.contains("sourceUpdatedAt")
.contains("nextAllowedAt");
.contains("scheduler leases and raw attempts are never exposed")
.doesNotContain("/{queryId}/attempts:")
.doesNotContain("port-schedule-query:attempt");
}
}
@@ -22,16 +22,17 @@ class PlatformCarrierScheduleQueryControllerContractTest {
"detail", "platform:firstleg:port-schedule-query:list",
"history", "platform:firstleg:port-schedule-query:list",
"create", "platform:firstleg:port-schedule-query:create",
"attempt", "platform:firstleg:port-schedule-query:attempt",
"manualResult", "platform:firstleg:port-schedule-query:manual");
permissions.forEach((name, permission) -> {
Method method = Arrays.stream(PlatformCarrierScheduleQueryController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + permission + "')");
if (name.equals("create") || name.equals("attempt") || name.equals("manualResult")) {
if (name.equals("create") || name.equals("manualResult")) {
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
}
});
assertThat(Arrays.stream(PlatformCarrierScheduleQueryController.class.getDeclaredMethods())
.map(Method::getName)).doesNotContain("attempt");
}
}
@@ -17,11 +17,16 @@ class CarrierScheduleQuerySqlContractTest {
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_attempt")
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_history")
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_query_operation")
.contains("CREATE TABLE IF NOT EXISTS fl_carrier_schedule_node_fact")
.contains("attempt_count BETWEEN 0 AND 3")
.contains("window_attempt_no BETWEEN 1 AND 3")
.contains("UNIQUE KEY uk_carrier_schedule_operation (operator_id,idempotency_key)")
.contains("船司查询尝试不可修改")
.contains("船司查询历史不可删除");
.contains("船司查询历史不可删除")
.contains("标准船节点事实不可修改")
.contains("ARRIVED','PICKUP_READY','STOPPED")
.contains("lease_owner")
.contains("consecutive_failures");
}
@Test
@@ -29,9 +34,9 @@ class CarrierScheduleQuerySqlContractTest {
assertThat(moduleSql())
.contains("booking.id=NEW.ocean_booking_id AND booking.customer_id=NEW.customer_id")
.contains("allocation.container_booking_id=NEW.container_booking_id")
.contains("5102184 AND 5102188")
.contains("platform:firstleg:port-schedule-query:attempt")
.contains("id IN (5102184,5102186,5102188)")
.contains("platform:firstleg:port-schedule-query:manual")
.contains("'platform:firstleg:port-schedule-query:attempt'")
.contains("role.role_code IN ('PLATFORM_ADMIN','ORDER_OPERATOR')")
.contains("船司查询任务角色授权未完整创建");
}
@@ -43,7 +48,7 @@ class CarrierScheduleQuerySqlContractTest {
assertThat(unified)
.contains("fl_carrier_schedule_query")
.contains("uk_carrier_schedule_query_scope")
.contains("5102184 AND 5102188")
.contains("id IN (5102184,5102186,5102188)")
.contains("assert_carrier_schedule_query_schema")
.contains("船司查询任务角色授权未完整创建");
}
@@ -0,0 +1,41 @@
package com.baiyue.firstleg.portschedule.service;
import com.baiyue.firstleg.client.CarrierScheduleIntegrationClient;
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class CarrierScheduleQuerySchedulerTest {
@Test
void leaseWinnerPersistsStandardNodesAndCompletesAttempt() {
CarrierScheduleQueryRepository repository = mock(CarrierScheduleQueryRepository.class);
CarrierScheduleQueryService service = mock(CarrierScheduleQueryService.class);
CarrierScheduleIntegrationClient integration = mock(CarrierScheduleIntegrationClient.class);
CarrierScheduleQueryScheduler scheduler = new CarrierScheduleQueryScheduler(repository, service, integration);
CarrierScheduleQueryEntity task = new CarrierScheduleQueryEntity(); task.setId(1L);
task.setCarrierCode("COSCO"); task.setTrackingType("CONTAINER_NO"); task.setTrackingNo("CSNU1234567");
LocalDateTime now = LocalDateTime.of(2026, 8, 20, 12, 0);
when(repository.claimLease(anyLong(), anyString(), any(), any())).thenReturn(true);
var observation = new CarrierScheduleIntegrationClient.Observation("evt-1", "DISCHARGED", "ARRIVED",
now, null, now, "USLAX", "Los Angeles", "provider:evt-1",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
when(integration.query(any(), any())).thenReturn(new CarrierScheduleIntegrationClient.Envelope<>("200", null,
new CarrierScheduleIntegrationClient.QueryResult("TRACK_API", "SUCCESS", List.of(observation),
null, null, now)));
scheduler.execute(task, now);
verify(service).applyProviderObservation(anyLong(), any());
verify(service).completeScheduledAttempt(1L, "SUCCESS", null, null);
}
}
@@ -3,10 +3,9 @@ package com.baiyue.firstleg.portschedule.service;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.AttemptRequest;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.CreateRequest;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualResultRequest;
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryAttemptEntity;
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ProviderObservationRequest;
import com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryEntity;
import com.baiyue.firstleg.portschedule.entity.PortScheduleEntity;
import com.baiyue.firstleg.portschedule.repository.CarrierScheduleQueryRepository;
@@ -14,7 +13,6 @@ import com.baiyue.firstleg.portschedule.repository.PortScheduleRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -48,7 +46,7 @@ class CarrierScheduleQueryServiceTest {
set("clock", Clock.fixed(Instant.parse("2026-08-10T04:00:00Z"), ZoneOffset.UTC));
when(repository.claimOperation(any(), anyLong(), anyString(), anyString(), anyString())).thenReturn(true);
when(repository.completeOperation(anyLong(), anyLong(), anyString(), anyString())).thenReturn(1);
when(repository.attempts(anyLong())).thenReturn(List.of());
when(repository.nodes(anyLong())).thenReturn(List.of());
when(repository.histories(anyLong())).thenReturn(List.of());
}
@@ -62,42 +60,48 @@ class CarrierScheduleQueryServiceTest {
var result = service.create(7L, "admin", "carrier-create",
new CreateRequest(0, 10001L, 10L, 20L, "ZIM", "BILL_OF_LADING", "GAOU6898053", null));
assertThat(result.query().query().getCarrierCode()).isEqualTo("ZIM");
assertThat(result.query().query().getStatus()).isEqualTo("READY");
assertThat(result.query().query().carrierCode()).isEqualTo("ZIM");
assertThat(result.query().query().status()).isEqualTo("READY");
assertThat(result.query().expired()).isTrue();
verify(repository).insert(any(CarrierScheduleQueryEntity.class));
}
@Test
void recordsManualRequiredAttemptWithoutBypassingCaptcha() {
CarrierScheduleQueryEntity task = task(0, 0);
when(repository.lock(1L)).thenReturn(task);
when(repository.updateState(any(), any(Integer.class), anyLong(), anyString())).thenReturn(1);
when(repository.find(1L)).thenAnswer(invocation -> task);
void outboundConfirmationCreatesTaskWithContainerTrackingAndDepartureNode() {
OceanBookingEntity booking = new OceanBookingEntity(); booking.setCustomerId(10001L);
booking.setShippingCompanyName("中远海运"); booking.setBookingNo("OB-260810-0001");
var container = new com.baiyue.firstleg.entity.ContainerBookingEntity();
container.setContainerNo("CSNU1234567");
when(portSchedules.oceanBooking(10L)).thenReturn(booking);
when(portSchedules.containerBooking(20L)).thenReturn(container);
when(portSchedules.allocation(10L, 20L)).thenReturn(new OceanBookingContainerAllocationEntity());
var result = service.attempt(1L, 7L, "admin", "carrier-attempt",
new AttemptRequest(0, "官网查询"));
CarrierScheduleQueryEntity result = service.ensureFromOutboundConfirm(10001L, 10L, 20L, 7L, "admin");
assertThat(result.query().query().getStatus()).isEqualTo("MANUAL_REQUIRED");
assertThat(result.query().query().getAttemptCount()).isEqualTo(1);
assertThat(result.query().query().getManualReason()).contains("hCaptcha");
ArgumentCaptor<CarrierScheduleQueryAttemptEntity> attempt =
ArgumentCaptor.forClass(CarrierScheduleQueryAttemptEntity.class);
verify(repository).insert(attempt.capture());
assertThat(attempt.getValue().getCoverageGap()).isTrue();
assertThat(attempt.getValue().getCoverageGapCode()).isEqualTo("HCAPTCHA_REQUIRED");
assertThat(result.getTriggerSource()).isEqualTo("OUTBOUND_CONFIRM");
assertThat(result.getCarrierCode()).isEqualTo("COSCO");
assertThat(result.getTrackingType()).isEqualTo("CONTAINER_NO");
assertThat(result.getLatestStandardNode()).isEqualTo("DEPARTED_WAREHOUSE");
verify(repository).insertIfAbsent(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity.class));
}
@Test
void blocksFourthAttemptInsideSameThreeDayWindow() {
CarrierScheduleQueryEntity task = task(3, 4);
void providerObservationAdvancesToArrivedWithoutExposingAttempts() {
CarrierScheduleQueryEntity task = task(0, 0);
task.setLatestStandardNode("IN_TRANSIT");
when(repository.lock(1L)).thenReturn(task);
when(repository.find(1L)).thenReturn(task);
when(repository.insertIfAbsent(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleNodeFactEntity.class)))
.thenReturn(true);
when(repository.updateState(any(), any(Integer.class), anyLong(), anyString())).thenReturn(1);
assertThatThrownBy(() -> service.attempt(1L, 7L, "admin", "carrier-fourth",
new AttemptRequest(4, null)))
.isInstanceOf(BizException.class).hasMessageContaining("最多查询3次");
var result = service.applyProviderObservation(1L, new ProviderObservationRequest("TRACK_API", "evt-1",
"DISCHARGED", "ARRIVED", NOW, null, NOW, "USLAX", "Los Angeles", "provider:evt-1",
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());
assertThat(result.query().query().status()).isEqualTo("ARRIVED");
assertThat(result.query().query().latestStandardNode()).isEqualTo("ARRIVED");
assertThat(result.nodes()).isEmpty();
}
@Test
@@ -149,7 +149,7 @@ class Phase3FinalUnifiedSqlContractTest {
.contains("存在缺失中文注释的业务表")
.contains("存在缺失中文注释的业务字段")
.doesNotContain("CREATE PROCEDURE upgrade_column_comment");
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(150);
assertThat(count(sql, "CALL queue_table_comment(")).isEqualTo(151);
assertThat(count(sql, "CALL queue_column_comment(")).isGreaterThanOrEqualTo(2459);
assertThat(sql)
.contains("CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_fee_detail', 'supplier_id'")
@@ -0,0 +1,96 @@
package com.baiyue.firstleg.service;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.FirstLegLoadingPlanCompletedV1;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.RegionCode;
import com.baiyue.firstleg.client.EventBridgeClient;
import com.baiyue.firstleg.entity.InboundOutboxEntity;
import com.baiyue.firstleg.repository.LoadingCompletedOutboxRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.List;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class LoadingCompletedOutboxRelayTest {
@Mock private LoadingCompletedOutboxRepository repository;
@Mock private EventBridgeClient client;
private ObjectMapper objectMapper;
private LoadingCompletedOutboxRelay relay;
@BeforeEach
void setUp() {
objectMapper = new ObjectMapper().registerModule(new JavaTimeModule());
relay = new LoadingCompletedOutboxRelay();
ReflectionTestUtils.setField(relay, "repository", repository);
ReflectionTestUtils.setField(relay, "client", client);
ReflectionTestUtils.setField(relay, "objectMapper", objectMapper);
ReflectionTestUtils.setField(relay, "internalToken", "internal-token");
ReflectionTestUtils.setField(relay, "leaseSeconds", 60L);
ReflectionTestUtils.setField(relay, "retrySeconds", 30L);
}
@Test
void claimsExpiredLeasesAndPublishesAfterOverseasBridgeAcknowledgement() throws Exception {
EventEnvelope event = event();
InboundOutboxEntity row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class))).thenReturn(ApiResponse.success(
new EventBridgeClient.Acknowledgement(event.eventId(), "已接收", "2026-08-20T03:01:00")));
relay.relay();
verify(repository).published(eq(9001L), anyString(), any(LocalDateTime.class));
verify(repository, never()).retry(anyLong(), anyString(), any(), anyString());
}
@Test
void redactsInternalTokenBeforePersistingRetryError() throws Exception {
EventEnvelope event = event();
InboundOutboxEntity row = row(event);
when(repository.pending(eq(50), any(), any())).thenReturn(List.of(row));
when(repository.claim(eq(9001L), anyString(), any(), any())).thenReturn(true);
when(repository.claimed(eq(9001L), anyString())).thenReturn(row);
when(client.receive(eq("internal-token"), any(EventEnvelope.class)))
.thenThrow(new IllegalStateException("failed internal-token\nremote"));
relay.relay();
verify(repository).retry(eq(9001L), anyString(), any(LocalDateTime.class), eq("failed *** remote"));
}
private InboundOutboxEntity row(EventEnvelope event) throws Exception {
InboundOutboxEntity row = new InboundOutboxEntity();
row.setId(9001L);
row.setEventKey(event.eventId());
row.setPayloadJson(objectMapper.writeValueAsString(event));
return row;
}
private EventEnvelope event() {
var payload = objectMapper.valueToTree(new FirstLegLoadingPlanCompletedV1(
"LOAD-1", "PLAN-1", "BOOK-1", "MSCU1234567", null,
"WH-1", "Warehouse", List.of()));
return new EventEnvelope("00000000-0000-4000-8000-000000000019", EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED, 1,
Instant.parse("2026-08-20T03:00:01Z"), RegionCode.CN, RegionCode.OVERSEAS, "1",
"LOADING_PLAN", "LOAD-1", 7L, 1L, "trace-12345678", "correlation-12345678", null,
payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload)));
}
}
@@ -43,12 +43,12 @@ class OverseasContainerEventConsumerTest {
@Test
void derivesEstimatedEmptyReportedAtFromPhotoUploadAndPersistsInbox() {
ContainerBookingEntity container = container();
when(containers.selectActiveByContainerNo("MSCU1234567")).thenReturn(container);
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container);
Instant completedAt = Instant.parse("2026-08-20T03:00:00Z");
Instant uploadedAt = Instant.parse("2026-08-20T02:30:00Z");
consumer.consume(event("00000000-0000-4000-8000-000000000001", 7,
new OverseasContainerUnloadedV1("MSCU1234567", completedAt, uploadedAt, null)));
new OverseasContainerUnloadedV1("1001", "MSCU1234567", 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,13 +62,13 @@ class OverseasContainerEventConsumerTest {
@Test
void repeatedEventWithSameHashDoesNotApplyAgain() {
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("MSCU1234567",
OverseasContainerUnloadedV1 payload = new OverseasContainerUnloadedV1("1001", "MSCU1234567",
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();
existing.setPayloadHash(event.payloadHash());
when(inbox.selectById(event.eventId())).thenReturn(existing);
when(containers.selectActiveByContainerNo("MSCU1234567")).thenReturn(container());
when(containers.selectActiveBookingForUpdate(1001L)).thenReturn(container());
consumer.consume(event);
@@ -79,7 +79,7 @@ class OverseasContainerEventConsumerTest {
@Test
void rejectsTamperedPayloadHash() {
EventEnvelope valid = event("00000000-0000-4000-8000-000000000003", 7,
new OverseasContainerUnloadedV1("MSCU1234567", Instant.parse("2026-08-20T03:00:00Z"),
new OverseasContainerUnloadedV1("1001", "MSCU1234567", 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(),
@@ -96,7 +96,7 @@ class OverseasContainerEventConsumerTest {
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node));
return new EventEnvelope(eventId, EventTypes.OVERSEAS_CONTAINER_UNLOADED, 1,
Instant.parse("2026-08-20T02:31:00Z"), RegionCode.OVERSEAS, RegionCode.CN, "1",
"CONTAINER_BOOKING", payload.containerNo(), aggregateVersion, null,
"CONTAINER_BOOKING", payload.sourceContainerBookingId(), aggregateVersion, null,
"trace-12345678", "correlation-12345678", null, node, hash);
}