feat: gate overseas receiving and harden phase4 workflows
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
package com.baiyue.lastleg.shipping.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
||||
import com.baiyue.common.event.EventPayloadHash;
|
||||
import com.baiyue.common.event.EventTypes;
|
||||
import com.baiyue.common.event.FirstLegContainerShippingNodeRecordedV1;
|
||||
import com.baiyue.common.event.RegionCode;
|
||||
import com.baiyue.lastleg.entity.LastlegEventInbox;
|
||||
import com.baiyue.lastleg.mapper.LastlegEventInboxMapper;
|
||||
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
|
||||
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ContainerShippingNodeConsumerTest {
|
||||
@Mock LastlegEventInboxMapper inboxes;
|
||||
@Mock ContainerShippingGateMapper gates;
|
||||
private final ObjectMapper json = new ObjectMapper().findAndRegisterModules();
|
||||
private ContainerShippingNodeConsumer consumer;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
consumer = new ContainerShippingNodeConsumer();
|
||||
ReflectionTestUtils.setField(consumer, "inboxes", inboxes);
|
||||
ReflectionTestUtils.setField(consumer, "gates", gates);
|
||||
ReflectionTestUtils.setField(consumer, "objectMapper", json);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pickupReadyMonotonicallyOpensReceivingGate() {
|
||||
ContainerShippingGate gate = gate("AWAITING_PICKUP_READY", 70, 4, Instant.parse("2026-08-20T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
EventEnvelope event = event("123e4567-e89b-12d3-a456-426614174101", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
|
||||
assertThat(consumer.consume(event)).isEqualTo(1);
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getNodeSequence()).isEqualTo(80);
|
||||
assertThat(gate.getAggregateVersion()).isEqualTo(5);
|
||||
verify(gates).updateById(gate);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lowerNodeAndOlderAggregateAreAuditedWithoutRegression() {
|
||||
ContainerShippingGate gate = gate("RECEIVING_AVAILABLE", 90, 8, Instant.parse("2026-08-22T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
|
||||
consumer.consume(event("123e4567-e89b-12d3-a456-426614174102", "PICKUP_READY", 80, 7,
|
||||
Instant.parse("2026-08-23T10:00:00Z")));
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getNodeSequence()).isEqualTo(90);
|
||||
assertThat(gate.getAggregateVersion()).isEqualTo(8);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameNodeOnlyAcceptsLaterActualTimeAndEmptyReturnedNeverClosesGate() {
|
||||
ContainerShippingGate gate = gate("RECEIVING_AVAILABLE", 100, 9, Instant.parse("2026-08-22T10:00:00Z"));
|
||||
when(gates.lock("CB-1")).thenReturn(gate);
|
||||
consumer.consume(event("123e4567-e89b-12d3-a456-426614174103", "EMPTY_RETURNED", 100, 10,
|
||||
Instant.parse("2026-08-22T12:00:00Z")));
|
||||
|
||||
assertThat(gate.getStatus()).isEqualTo("RECEIVING_AVAILABLE");
|
||||
assertThat(gate.getActualAt()).isEqualTo(java.time.LocalDateTime.parse("2026-08-22T12:00:00"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void eventIdIsIdempotentByHashAndRejectsDifferentHash() {
|
||||
EventEnvelope event = event("123e4567-e89b-12d3-a456-426614174104", "PICKUP_READY", 80, 5,
|
||||
Instant.parse("2026-08-21T10:00:00Z"));
|
||||
LastlegEventInbox existing = new LastlegEventInbox(); existing.setPayloadHash(event.payloadHash());
|
||||
when(inboxes.selectById(event.eventId())).thenReturn(existing);
|
||||
assertThat(consumer.consume(event)).isEqualTo(1);
|
||||
verify(gates, never()).updateById(any(ContainerShippingGate.class));
|
||||
|
||||
existing.setPayloadHash("sha256:" + "0".repeat(64));
|
||||
assertThatThrownBy(() -> consumer.consume(event)).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("摘要冲突");
|
||||
}
|
||||
|
||||
private ContainerShippingGate gate(String status, int sequence, long version, Instant actualAt) {
|
||||
ContainerShippingGate gate = new ContainerShippingGate(); gate.setContainerBookingId("CB-1");
|
||||
gate.setCustomerId("100"); gate.setOceanBookingId("OB-1"); gate.setContainerNo("CONT-1");
|
||||
gate.setStatus(status); gate.setNodeSequence(sequence); gate.setAggregateVersion(version);
|
||||
gate.setHandoverVersion(version + 1); gate.setActualAt(java.time.LocalDateTime.ofInstant(actualAt,
|
||||
java.time.ZoneOffset.UTC)); return gate;
|
||||
}
|
||||
|
||||
private EventEnvelope event(String eventId, String node, int sequence, long version, Instant actualAt) {
|
||||
FirstLegContainerShippingNodeRecordedV1 payload = new FirstLegContainerShippingNodeRecordedV1(
|
||||
"Q-1", "100", "OB-1", "CB-1", "CONT-1", node, sequence, actualAt,
|
||||
actualAt, "LAX", "Los Angeles", "CARRIER", "PE-1", node, "REF-1", "a".repeat(64));
|
||||
JsonNode payloadNode = json.valueToTree(payload);
|
||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||
return new EventEnvelope(eventId, EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED, 1,
|
||||
actualAt, RegionCode.CN, RegionCode.OVERSEAS, "100", "CONTAINER_BOOKING", "CB-1",
|
||||
version, version + 1, "123e4567-e89b-12d3-a456-426614174199", "shipping:CB-1", null,
|
||||
payloadNode, hash);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user