fix(firstleg): decouple manual container nodes from tracking
This commit is contained in:
-4
@@ -28,8 +28,6 @@ class CarrierScheduleQueryOpenApiContractTest {
|
||||
.contains("Estimated facts are never returned or inferred")
|
||||
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-results:")
|
||||
.contains("/api/platform/v1/carrier-schedule-queries/{queryId}/manual-nodes:")
|
||||
.contains("/api/platform/v1/carrier-schedule-queries/containers/{containerBookingId}/ensure:")
|
||||
.contains("platform-owned consolidated bookings use customerId 0")
|
||||
.contains("x-permission: platform:firstleg:port-schedule-query:manual")
|
||||
.contains("enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE]")
|
||||
.contains("LOADED_ON_VESSEL, DEPARTED_ORIGIN_PORT, IN_TRANSIT, ARRIVED_DESTINATION_PORT")
|
||||
@@ -40,8 +38,6 @@ class CarrierScheduleQueryOpenApiContractTest {
|
||||
.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]")
|
||||
|
||||
+1
-2
@@ -23,7 +23,6 @@ class PlatformCarrierScheduleQueryControllerContractTest {
|
||||
"detail", "platform:firstleg:port-schedule-query:list",
|
||||
"history", "platform:firstleg:port-schedule-query:list",
|
||||
"create", "platform:firstleg:port-schedule-query:create",
|
||||
"ensureForManualEntry", "platform:firstleg:port-schedule-query:manual",
|
||||
"manualResult", "platform:firstleg:port-schedule-query:manual",
|
||||
"manualNode", "platform:firstleg:port-schedule-query:manual");
|
||||
permissions.forEach((name, permission) -> {
|
||||
@@ -31,7 +30,7 @@ class PlatformCarrierScheduleQueryControllerContractTest {
|
||||
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
|
||||
assertThat(method.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('" + permission + "')");
|
||||
if (name.equals("create") || name.equals("ensureForManualEntry") || name.equals("manualResult") || name.equals("manualNode")) {
|
||||
if (name.equals("create") || name.equals("manualResult") || name.equals("manualNode")) {
|
||||
assertThat(method.getAnnotation(BusinessAudit.class)).isNotNull();
|
||||
}
|
||||
});
|
||||
|
||||
+1
-29
@@ -451,34 +451,6 @@ class CarrierScheduleQueryServiceTest {
|
||||
verify(repository).completeOperation(eq(1L), eq(7L), eq("manual-empty-1"), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualActualRejectsSameAndEarlierNodesIncludingSameKeyRetryWithoutDrift() {
|
||||
CarrierScheduleQueryEntity task = task(0, 3);
|
||||
task.setStatus("READY"); task.setLatestStandardNode("PICKED_UP");
|
||||
task.setLatestNodeObservedAt(NOW.minusDays(1));
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
|
||||
ManualNodeRequest sameNode = new ManualNodeRequest(3, "PICKED_UP", NOW, null,
|
||||
NOW.plusDays(1), null, "Los Angeles", "重复确认提柜");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-picked-up-again", sameNode))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
assertThatThrownBy(() -> service.addManualNode(1L, 7L, "admin", "manual-pickup-ready-late",
|
||||
new ManualNodeRequest(3, "PICKUP_READY", NOW, null, NOW.plusDays(2), null,
|
||||
"Los Angeles", "倒序补录可提柜")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("人工实际节点必须严格晚于当前最新实际节点");
|
||||
|
||||
verify(repository, never()).insertIfAbsent(any(CarrierScheduleNodeFactEntity.class));
|
||||
verify(repository, never()).updateState(any(), any(Integer.class), anyLong(), anyString());
|
||||
verify(repository, never()).insert(any(com.baiyue.firstleg.portschedule.entity.CarrierScheduleQueryHistoryEntity.class));
|
||||
verify(repository, never()).completeOperation(anyLong(), anyLong(), anyString(), anyString());
|
||||
verify(nodeOutbox, never()).publish(any(), any(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void manualEstimatedFactAdvancesVersionWithoutAdvancingActualProgress() {
|
||||
CarrierScheduleQueryEntity before = task(0, 3);
|
||||
@@ -549,7 +521,7 @@ class CarrierScheduleQueryServiceTest {
|
||||
existing.setEventKey("ZIM:evt-duplicate"); existing.setRawPayloadHash("a".repeat(64));
|
||||
when(repository.lock(1L)).thenReturn(task);
|
||||
when(repository.insertIfAbsent(any(CarrierScheduleNodeFactEntity.class))).thenReturn(false);
|
||||
when(repository.node(1L, "ZIM:evt-duplicate")).thenReturn(existing);
|
||||
when(repository.nodeByContainer(task.getContainerBookingId(), "ZIM:evt-duplicate")).thenReturn(existing);
|
||||
ProviderObservationRequest replay = new ProviderObservationRequest("ZIM", "evt-duplicate", "GATE_IN",
|
||||
"AT_ORIGIN_PORT", NOW, null, NOW.minusDays(1), null, null, "ZIM:evt", "a".repeat(64));
|
||||
|
||||
|
||||
+16
-1
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.terminaldrayage.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@@ -50,10 +51,24 @@ class TerminalDrayageOpenApiContractTest {
|
||||
"platform:firstleg:terminal-drayage:pickup:confirm",
|
||||
"platform:firstleg:terminal-drayage:empty-return:confirm",
|
||||
"expectedVersion", "Idempotency-Key", "estimatedEmptyReturnAt",
|
||||
"emptyReturnWarning", "actualFacts", "deadlineAt", "overdueHours", "overdueDays");
|
||||
"emptyReturnWarning", "actualFacts", "deadlineAt", "overdueHours", "overdueDays",
|
||||
"船节点跟踪任务尚未建立也返回WAITING_FOR_ARRIVAL空详情",
|
||||
"允许读取空邮件列表,不以船节点跟踪任务是否已经建立作为货柜存在性条件");
|
||||
assertThat(yaml).contains("description: 卸柜完成时间加72小时");
|
||||
}
|
||||
|
||||
@Test
|
||||
void terminalDrayageContextsUseConfirmedOutboundAsTheOverseasBoundary() throws Exception {
|
||||
var mapper = com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class;
|
||||
for (String methodName : java.util.List.of("context", "lockContext", "contexts")) {
|
||||
var method = java.util.Arrays.stream(mapper.getDeclaredMethods())
|
||||
.filter(value -> methodName.equals(value.getName())).findFirst().orElseThrow();
|
||||
String sql = String.join("\n", method.getAnnotation(Select.class).value());
|
||||
assertThat(sql).contains("FROM fl_outbound_dispatch", "d.confirmed_at IS NOT NULL",
|
||||
"d.reversed_at IS NULL", "d.status='WAITING_FOR_LOADING'");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertPermission(String methodName, String permission) {
|
||||
var method = java.util.Arrays.stream(PlatformTerminalDrayageController.class.getDeclaredMethods())
|
||||
.filter(value -> methodName.equals(value.getName())).findFirst().orElseThrow();
|
||||
|
||||
+11
-3
@@ -92,19 +92,27 @@ class ContainerWorkbenchServiceTest {
|
||||
"platform:firstleg:terminal-drayage:document:edit");
|
||||
when(repository.find(11L)).thenReturn(container);
|
||||
when(repository.departed(11L)).thenReturn(true);
|
||||
when(terminalDrayages.views(List.of(11L))).thenReturn(List.of());
|
||||
when(terminalDrayages.authorize(List.of(), permissions)).thenReturn(List.of());
|
||||
var waiting = waitingDrayage();
|
||||
when(terminalDrayages.views(List.of(11L))).thenReturn(List.of(waiting));
|
||||
when(terminalDrayages.authorize(List.of(waiting), permissions)).thenReturn(List.of(waiting));
|
||||
|
||||
var result = service.overseasDetail(11L, permissions);
|
||||
|
||||
assertThat(result.workbenchStage()).isEqualTo("DEPARTED_WAREHOUSE");
|
||||
assertThat(result.actualFacts()).isNull();
|
||||
assertThat(result.terminalDrayage()).isNull();
|
||||
assertThat(result.terminalDrayage()).isEqualTo(waiting);
|
||||
assertThat(result.allowedActions()).containsExactly("MANAGE_DESTINATION_DOCUMENTS");
|
||||
verify(terminalDrayages, never()).detail(11L);
|
||||
verifyNoInteractions(carrierSchedules);
|
||||
}
|
||||
|
||||
private static com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View waitingDrayage() {
|
||||
return new com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.View(
|
||||
11L, "MSCU1234567", 21L, "WAITING_FOR_ARRIVAL", 0, null, null, null,
|
||||
null, null, null, null, new com.baiyue.firstleg.terminaldrayage.dto.TerminalDrayageDtos.Warning(
|
||||
"NOT_APPLICABLE", null, 0, 0), List.of(), null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void realOverseasListAndDetailProjectBothEmptyReturnActions() throws Exception {
|
||||
verifyRealEmptyReturnProjection(true, true);
|
||||
|
||||
+24
-11
@@ -2,7 +2,7 @@ package com.baiyue.firstleg.terminaldrayage.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerManualNodeRequest;
|
||||
import com.baiyue.firstleg.portschedule.dto.CarrierScheduleQueryDtos.ContainerActualFacts;
|
||||
import com.baiyue.firstleg.portschedule.service.CarrierScheduleQueryService;
|
||||
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
|
||||
@@ -125,8 +125,8 @@ class TerminalDrayageServiceTest {
|
||||
|
||||
assertThat(result.status()).isEqualTo("PICKED_UP");
|
||||
assertThat(result.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 2, 10, 0));
|
||||
ArgumentCaptor<ManualNodeRequest> node = ArgumentCaptor.forClass(ManualNodeRequest.class);
|
||||
verify(carrierSchedules).addManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
ArgumentCaptor<ContainerManualNodeRequest> node = ArgumentCaptor.forClass(ContainerManualNodeRequest.class);
|
||||
verify(carrierSchedules).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
assertThat(node.getValue().standardNode()).isEqualTo("PICKED_UP");
|
||||
verify(mapper, times(1)).updateAggregate(any(), anyInt(), anyLong(), anyString());
|
||||
}
|
||||
@@ -144,7 +144,7 @@ class TerminalDrayageServiceTest {
|
||||
new ActualTimeRequest(3, LocalDateTime.of(2026, 9, 4, 10, 0), null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessage("海外卸柜收货完成后才能确认还柜");
|
||||
verify(carrierSchedules, never()).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, never()).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -212,7 +212,7 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.allowedActions()).doesNotContain("RECORD_EMPTY_RETURN");
|
||||
assertThat(service.recordEmptyReturn(11L, 7L, "admin", "return-new", request)).isEqualTo(result);
|
||||
verify(mapper, times(1)).insert(any(TerminalDrayageEntity.class));
|
||||
verify(carrierSchedules, times(1)).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, times(1)).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -222,7 +222,7 @@ class TerminalDrayageServiceTest {
|
||||
new ActualTimeRequest(0, LocalDateTime.of(2026, 9, 2, 23, 0), null)))
|
||||
.isInstanceOf(BizException.class).hasMessage("实际还柜时间不能早于提柜或卸柜完成时间");
|
||||
verify(mapper, never()).insert(any(TerminalDrayageEntity.class));
|
||||
verify(carrierSchedules, never()).addManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
verify(carrierSchedules, never()).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,13 +243,26 @@ class TerminalDrayageServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void bulkViewOmitsContainerWhenActualFactPortHasNoTrackingRecord() {
|
||||
void readsDepartedContainerBeforeCarrierTrackingRecordExists() {
|
||||
when(mapper.contexts(List.of(11L))).thenReturn(List.of(
|
||||
new Context(11L, "MSCU1234567", 21L, "ACTIVE", null)));
|
||||
when(mapper.findAll(List.of(11L))).thenReturn(List.of());
|
||||
when(mapper.context(11L)).thenReturn(
|
||||
new Context(11L, "MSCU1234567", 21L, "ACTIVE", null));
|
||||
when(mapper.find(11L)).thenReturn(null);
|
||||
|
||||
assertThat(service.views(List.of(11L))).isEmpty();
|
||||
verify(carrierSchedules).containerActualFacts(List.of(11L));
|
||||
var bulk = service.views(List.of(11L));
|
||||
var detail = service.detail(11L);
|
||||
|
||||
assertThat(bulk).singleElement().satisfies(view -> {
|
||||
assertThat(view.status()).isEqualTo("WAITING_FOR_ARRIVAL");
|
||||
assertThat(view.actualFacts()).isNull();
|
||||
assertThat(view.allowedActions()).isEmpty();
|
||||
});
|
||||
assertThat(detail.status()).isEqualTo("WAITING_FOR_ARRIVAL");
|
||||
assertThat(detail.actualFacts()).isNull();
|
||||
assertThat(detail.allowedActions()).isEmpty();
|
||||
verify(carrierSchedules, times(2)).containerActualFacts(List.of(11L));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -338,8 +351,8 @@ class TerminalDrayageServiceTest {
|
||||
assertThat(result.status()).isEqualTo("EMPTY_RETURNED");
|
||||
assertThat(result.estimatedEmptyReturnAt()).isEqualTo(unloadedAt.plusHours(72));
|
||||
assertThat(result.emptyReturnWarning().status()).isEqualTo("RESOLVED");
|
||||
ArgumentCaptor<ManualNodeRequest> node = ArgumentCaptor.forClass(ManualNodeRequest.class);
|
||||
verify(carrierSchedules).addManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
ArgumentCaptor<ContainerManualNodeRequest> node = ArgumentCaptor.forClass(ContainerManualNodeRequest.class);
|
||||
verify(carrierSchedules).addContainerManualNode(anyLong(), anyLong(), anyString(), anyString(), node.capture());
|
||||
assertThat(node.getValue().standardNode()).isEqualTo("EMPTY_RETURNED");
|
||||
assertThat(node.getValue().reason()).isEqualTo("已还柜");
|
||||
assertThat(node.getValue().actualAt()).isEqualTo(LocalDateTime.of(2026, 9, 4, 9, 0));
|
||||
|
||||
Reference in New Issue
Block a user