fix(events): allow manual nodes without tracking query

This commit is contained in:
2026-09-10 13:56:07 +08:00
parent c051be3a67
commit a3890e7a55
4 changed files with 35 additions and 4 deletions
@@ -28,7 +28,10 @@ public record FirstLegContainerShippingNodeRecordedV1(
"EMPTY_RETURNED", 100); "EMPTY_RETURNED", 100);
public FirstLegContainerShippingNodeRecordedV1 { public FirstLegContainerShippingNodeRecordedV1 {
if (queryId == null || queryId.isBlank() || oceanBookingId == null || oceanBookingId.isBlank() if (queryId != null && queryId.isBlank()) {
throw new IllegalArgumentException("船节点跟踪任务标识不能为空字符串");
}
if (oceanBookingId == null || oceanBookingId.isBlank()
|| containerBookingId == null || containerBookingId.isBlank() || containerBookingId == null || containerBookingId.isBlank()
|| containerNo == null || containerNo.isBlank()) { || containerNo == null || containerNo.isBlank()) {
throw new IllegalArgumentException("船节点交接业务标识不能为空"); throw new IllegalArgumentException("船节点交接业务标识不能为空");
@@ -42,10 +42,9 @@ components:
destinationType: {type: string, enum: [FBA, OVERSEAS]} destinationType: {type: string, enum: [FBA, OVERSEAS]}
FirstLegContainerShippingNodeRecordedV1: FirstLegContainerShippingNodeRecordedV1:
type: object type: object
required: [queryId, customerId, oceanBookingId, sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash] required: [oceanBookingId, sourceContainerBookingId, containerNo, standardNode, nodeSequence, observedAt, actualAt, providerCode, providerEventId, providerNodeCode, rawPayloadHash]
properties: properties:
queryId: {type: string} queryId: {type: [string, 'null'], description: Automatic tracking task ID; null for a direct platform manual node.}
customerId: {type: string}
oceanBookingId: {type: string} oceanBookingId: {type: string}
sourceContainerBookingId: {type: string, minLength: 1} sourceContainerBookingId: {type: string, minLength: 1}
containerNo: {type: string, minLength: 1, description: Uppercase normalized physical container number; never cabinetNo or a technical identity} containerNo: {type: string, minLength: 1, description: Uppercase normalized physical container number; never cabinetNo or a technical identity}
@@ -35,6 +35,18 @@ class FirstLegContainerShippingNodeRecordedV1Test {
.hasMessageContaining("normalized"); .hasMessageContaining("normalized");
} }
@Test
void acceptsDirectManualNodeWithoutTrackingQuery() {
var payload = payload();
var manual = new FirstLegContainerShippingNodeRecordedV1(
null, payload.oceanBookingId(), payload.containerBookingId(), payload.containerNo(),
payload.standardNode(), payload.nodeSequence(), payload.observedAt(), payload.actualAt(),
payload.locationCode(), payload.locationName(), "MANUAL", "manual-event", payload.providerNodeCode(),
"MANUAL:7", payload.rawPayloadHash());
assertThat(manual.queryId()).isNull();
}
@Test @Test
void rejectsEstimatedOnlyOrIncompleteProviderFacts() { void rejectsEstimatedOnlyOrIncompleteProviderFacts() {
assertThatThrownBy(() -> new FirstLegContainerShippingNodeRecordedV1( assertThatThrownBy(() -> new FirstLegContainerShippingNodeRecordedV1(
@@ -93,6 +93,23 @@ class CarrierScheduleNodeOutboxServiceTest {
verify(mapper, never()).insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class)); verify(mapper, never()).insert(org.mockito.ArgumentMatchers.any(InboundOutboxEntity.class));
} }
@Test
void directManualActualFactDoesNotRequireTrackingQuery() throws Exception {
CarrierScheduleNodeFactEntity fact = fact();
fact.setProviderCode("MANUAL"); fact.setProviderEventId("manual-event");
fact.setSourceReference("MANUAL:7");
service.publishContainerNode(2088000000000000004L, 2088000000000000003L,
fact, "ZCSU7881970");
ArgumentCaptor<InboundOutboxEntity> captor = ArgumentCaptor.forClass(InboundOutboxEntity.class);
verify(mapper).insert(captor.capture());
EventEnvelope envelope = objectMapper.readValue(captor.getValue().getPayloadJson(), EventEnvelope.class);
assertThat(envelope.payload().path("queryId").isNull()).isTrue();
assertThat(envelope.payload().path("sourceContainerBookingId").asText())
.isEqualTo("2088000000000000003");
}
@Test @Test
void initialOutboundDepartureUsesVersionZeroWithoutEnvelopeFailure() throws Exception { void initialOutboundDepartureUsesVersionZeroWithoutEnvelopeFailure() throws Exception {
CarrierScheduleQueryEntity query = query(); CarrierScheduleQueryEntity query = query();