This commit is contained in:
2026-09-15 14:18:18 +08:00
parent 6a2075fe4b
commit a474ca4d93
35 changed files with 751 additions and 228 deletions
@@ -33,7 +33,6 @@ class TerminalDrayageOpenApiContractTest {
assertPermission("retryEmail", "platform:firstleg:terminal-drayage:fleet:edit");
assertPermission("emails", "platform:firstleg:terminal-drayage:list");
assertPermission("schedulePickup", "platform:firstleg:terminal-drayage:pickup-appointment:edit");
assertPermission("recordPickup", "platform:firstleg:terminal-drayage:pickup:confirm");
assertPermission("recordEmptyReturn", "platform:firstleg:terminal-drayage:empty-return:confirm");
}
@@ -43,18 +42,18 @@ class TerminalDrayageOpenApiContractTest {
assertThat(yaml).contains(
"/api/platform/v1/terminal-drayages/{containerBookingId}/fleet:",
"/api/platform/v1/terminal-drayages/{containerBookingId}/pickup-appointment:",
"/api/platform/v1/terminal-drayages/{containerBookingId}/pickup:",
"/api/platform/v1/terminal-drayages/{containerBookingId}/empty-return:",
"platform:firstleg:terminal-drayage:list",
"platform:firstleg:terminal-drayage:fleet:edit",
"platform:firstleg:terminal-drayage:pickup-appointment:edit",
"platform:firstleg:terminal-drayage:pickup:confirm",
"platform:firstleg:terminal-drayage:empty-return:confirm",
"expectedVersion", "Idempotency-Key", "estimatedEmptyReturnAt",
"emptyReturnWarning", "actualFacts", "deadlineAt", "overdueHours", "overdueDays",
"船节点跟踪任务尚未建立也返回WAITING_FOR_ARRIVAL空详情",
"允许读取空邮件列表,不以船节点跟踪任务是否已经建立作为货柜存在性条件");
assertThat(yaml).contains("description: 卸柜完成时间加72小时");
assertThat(yaml).doesNotContain("/api/platform/v1/terminal-drayages/{containerBookingId}/pickup:",
"platform:firstleg:terminal-drayage:pickup:confirm", "RECORD_PICKUP");
}
@Test
@@ -26,24 +26,19 @@ class TerminalDrayageSqlContractTest {
assertThat(Files.readString(path)).contains(
"CREATE TABLE IF NOT EXISTS fl_terminal_drayage_mail (",
"CHECK(status IN ('PENDING','SENDING','SENT','FAILED'))",
"('SELECT_FLEET','SEND_PICKUP_EMAIL','SCHEDULE_PICKUP','RECORD_PICKUP','RECORD_EMPTY_RETURN')");
"('SELECT_FLEET','SEND_PICKUP_EMAIL','SCHEDULE_PICKUP','RECORD_EMPTY_RETURN')");
}
}
@Test
void bothSqlEntrypointsAllowReturnWithoutPickupAndRetainKnownTimeOrdering() throws Exception {
for (Path path : java.util.List.of(
Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"),
Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"))) {
String sql = Files.readString(path);
assertThat(sql).contains("actual_empty_return_at IS NULL OR actual_pickup_at IS NULL OR actual_empty_return_at >= actual_pickup_at")
.doesNotContain("actual_empty_return_at IS NULL OR actual_pickup_at IS NOT NULL");
}
void bothSqlEntrypointsRemoveTheManualPickupTimestamp() throws Exception {
String baseline = Files.readString(Path.of("src/main/resources/db/phase3-terminal-drayage-schema.sql"));
assertThat(baseline).doesNotContain("actual_pickup_at", "chk_terminal_drayage_times");
String release = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
assertThat(release).contains("DROP CHECK chk_terminal_drayage_times",
"ADD CONSTRAINT chk_terminal_drayage_times CHECK(",
assertThat(release).contains("ALTER TABLE fl_terminal_drayage DROP COLUMN actual_pickup_at",
"DROP CHECK chk_terminal_drayage_history_action",
"ADD CONSTRAINT chk_terminal_drayage_history_action CHECK(action IN");
assertThat(release).doesNotContain("chk_terminal_drayage_times");
}
@Test
@@ -142,7 +142,6 @@ class ContainerWorkbenchServiceTest {
row.setId(51L);
row.setContainerBookingId(11L);
row.setVersion(3);
row.setActualPickupAt(LocalDateTime.of(2026, 9, 4, 9, 0));
when(repository.page(eq("OVERSEAS"), any())).thenReturn(new PageResult<>(List.of(container), 1, 20, 1, 1));
when(repository.find(11L)).thenReturn(container);
when(repository.departed(11L)).thenReturn(true);
@@ -205,9 +204,9 @@ class ContainerWorkbenchServiceTest {
assertThat(result.workbenchStage()).isEqualTo("UNLOADED");
assertThat(result.terminalDrayage().status()).isEqualTo("UNLOADED");
assertThat(result.terminalDrayage().version()).isZero();
assertThat(result.terminalDrayage().actualPickupAt()).isNull();
assertThat(result.allowedActions()).containsExactly("SELECT_FLEET", "RECORD_EMPTY_RETURN");
assertThat(result.terminalDrayage().allowedActions()).containsExactly("SELECT_FLEET", "RECORD_EMPTY_RETURN");
assertThat(result.terminalDrayage().actualPickupAt()).isEqualTo(unloadedAt);
assertThat(result.allowedActions()).containsExactly("RECORD_EMPTY_RETURN");
assertThat(result.terminalDrayage().allowedActions()).containsExactly("RECORD_EMPTY_RETURN");
}
verify(mapper, never()).insert(any(com.baiyue.firstleg.terminaldrayage.entity.TerminalDrayageEntity.class));
}
@@ -104,40 +104,12 @@ class TerminalDrayageServiceTest {
verify(mapper).insertOperation(anyLong(), anyLong(), anyString(), anyString(), anyString(), anyInt(), anyLong());
}
@Test
void recordsPickupOnceAndAdvancesCanonicalNode() {
Context before = context("PICKUP_READY", null);
Context after = context("PICKED_UP", null);
when(mapper.lockContext(11L)).thenReturn(before);
when(mapper.context(11L)).thenReturn(after);
when(mapper.operation(11L, "pickup-1")).thenReturn(null);
TerminalDrayageEntity row = row(2);
row.setPickupAppointmentAt(LocalDateTime.of(2026, 9, 2, 9, 0));
when(mapper.lock(11L)).thenReturn(row);
when(mapper.updateAggregate(any(), anyInt(), anyLong(), anyString())).thenAnswer(invocation -> {
row.setVersion(3);
return 1;
});
when(mapper.find(11L)).thenReturn(row);
var result = service.recordPickup(11L, 7L, "admin", "pickup-1",
new ActualTimeRequest(2, LocalDateTime.of(2026, 9, 2, 10, 0), "码头提柜"));
assertThat(result.status()).isEqualTo("PICKED_UP");
assertThat(result.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 2, 10, 0));
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());
}
@Test
void requiresOverseasUnloadBeforeEmptyReturn() {
Context context = context("PICKED_UP", null);
when(mapper.lockContext(11L)).thenReturn(context);
when(mapper.operation(11L, "return-1")).thenReturn(null);
TerminalDrayageEntity row = row(3);
row.setActualPickupAt(LocalDateTime.of(2026, 9, 2, 10, 0));
when(mapper.lock(11L)).thenReturn(row);
assertThatThrownBy(() -> service.recordEmptyReturn(11L, 7L, "admin", "return-1",
@@ -152,7 +124,6 @@ class TerminalDrayageServiceTest {
LocalDateTime unloadedAt = LocalDateTime.of(2026, 9, 1, 0, 0);
Context context = context("PICKED_UP", unloadedAt);
TerminalDrayageEntity row = row(3);
row.setActualPickupAt(LocalDateTime.of(2026, 8, 31, 18, 0));
when(mapper.context(11L)).thenReturn(context);
when(mapper.find(11L)).thenReturn(row);
@@ -178,7 +149,7 @@ class TerminalDrayageServiceTest {
assertThat(result.status()).isEqualTo("UNLOADED");
assertThat(result.allowedActions()).contains("RECORD_EMPTY_RETURN");
assertThat(result.actualPickupAt()).isNull();
assertThat(result.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 3, 0, 0));
assertThat(result.version()).isZero();
}
@@ -206,7 +177,7 @@ class TerminalDrayageServiceTest {
var result = service.recordEmptyReturn(11L, 7L, "admin", "return-new", request);
assertThat(result.status()).isEqualTo("EMPTY_RETURNED");
assertThat(result.emptyReturnWarning().status()).isEqualTo("RESOLVED");
assertThat(result.actualPickupAt()).isNull();
assertThat(result.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 3, 0, 0));
assertThat(result.fleetSupplierId()).isNull();
assertThat(result.version()).isEqualTo(1);
assertThat(result.allowedActions()).doesNotContain("RECORD_EMPTY_RETURN");
@@ -229,12 +200,12 @@ class TerminalDrayageServiceTest {
void unloadedContainerOffersEmptyReturnOnlyWithDedicatedPermission() {
Context context = context("PICKED_UP", LocalDateTime.of(2026, 9, 3, 0, 0));
TerminalDrayageEntity row = row(3);
row.setActualPickupAt(LocalDateTime.of(2026, 9, 2, 10, 0));
when(mapper.context(11L)).thenReturn(context);
when(mapper.find(11L)).thenReturn(row);
var raw = service.detail(11L);
assertThat(raw.status()).isEqualTo("UNLOADED");
assertThat(raw.actualPickupAt()).isEqualTo(LocalDateTime.of(2026, 9, 2, 8, 0));
assertThat(service.authorize(raw,
Set.of("platform:firstleg:terminal-drayage:empty-return:confirm")).allowedActions())
.containsExactly("RECORD_EMPTY_RETURN");
@@ -265,22 +236,6 @@ class TerminalDrayageServiceTest {
verify(carrierSchedules, times(2)).containerActualFacts(List.of(11L));
}
@Test
void allowedActionsAreReducedByFrozenPermissionMatrix() {
Context context = context("PICKUP_READY", null);
TerminalDrayageEntity row = row(3);
row.setPickupAppointmentAt(LocalDateTime.of(2026, 9, 2, 9, 0));
when(mapper.context(11L)).thenReturn(context);
when(mapper.find(11L)).thenReturn(row);
var raw = service.detail(11L);
var authorized = service.authorize(raw,
Set.of("platform:firstleg:terminal-drayage:pickup:confirm"));
assertThat(raw.allowedActions()).contains("SELECT_FLEET", "SCHEDULE_PICKUP", "RECORD_PICKUP");
assertThat(authorized.allowedActions()).containsExactly("RECORD_PICKUP");
}
@Test
void sameIdempotencyKeyWithDifferentPayloadConflictsWithoutWriting() {
Context context = context("ARRIVED_DESTINATION_PORT", null);
@@ -337,7 +292,6 @@ class TerminalDrayageServiceTest {
when(mapper.operation(11L, "return-2")).thenReturn(null);
TerminalDrayageEntity row = row(3);
row.setPickupAppointmentAt(LocalDateTime.of(2026, 9, 2, 9, 0));
row.setActualPickupAt(LocalDateTime.of(2026, 9, 2, 10, 0));
when(mapper.lock(11L)).thenReturn(row);
when(mapper.updateAggregate(any(), anyInt(), anyLong(), anyString())).thenAnswer(invocation -> {
row.setVersion(4);