This commit is contained in:
2026-09-09 12:57:59 +08:00
parent e5d826b4a2
commit a3354c92c2
11 changed files with 179 additions and 19 deletions
@@ -15,6 +15,8 @@ class TerminalDrayageSqlContractTest {
"src/main/java/com/baiyue/firstleg/terminaldrayage/mapper/TerminalDrayageMapper.java"));
assertThat(sql).contains(
"CREATE TABLE IF NOT EXISTS fl_terminal_drayage (",
"fleet_supplier_id BIGINT UNSIGNED NULL",
"fleet_supplier_name VARCHAR(128) NULL",
"UNIQUE KEY uk_terminal_drayage_container(container_booking_id)",
"CREATE TABLE IF NOT EXISTS fl_terminal_drayage_history (",
"CREATE TABLE IF NOT EXISTS fl_terminal_drayage_operation (",
@@ -95,7 +95,7 @@ class ContainerWorkbenchServiceTest {
}
@Test
void unloadedListWithoutDrayageAggregateDoesNotOfferUnexecutableReturn() throws Exception {
void unloadedListWithoutDrayageAggregateOffersEmptyReturn() throws Exception {
verifyRealEmptyReturnProjection(true, false);
}
@@ -129,7 +129,7 @@ class ContainerWorkbenchServiceTest {
var page = service.overseasPage(query(WorkbenchScope.ACTIVE), permissions);
var listed = page.records().get(0);
var detail = service.overseasDetail(11L, permissions);
List<String> expected = hasPermission && hasAggregate ? List.of("RECORD_EMPTY_RETURN") : List.of();
List<String> expected = hasPermission ? List.of("RECORD_EMPTY_RETURN") : List.of();
assertThat(listed.workbenchStage()).isEqualTo("UNLOADED");
assertThat(listed.terminalDrayage().status()).isEqualTo("UNLOADED");
assertThat(listed.allowedActions()).isEqualTo(expected);
@@ -143,6 +143,46 @@ class ContainerWorkbenchServiceTest {
verify(mapper).contexts(List.of(11L));
}
@Test
void unloadedWorkbenchWithoutPickupOffersEmptyReturn() {
var mapper = mock(com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.class);
var realDrayages = new TerminalDrayageService(mapper,
mock(com.baiyue.firstleg.service.WarehouseSupplierSnapshotService.class), carrierSchedules,
new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules());
ReflectionTestUtils.setField(service, "terminalDrayages", realDrayages);
ContainerBookingEntity container = container();
LocalDateTime unloadedAt = LocalDateTime.of(2026, 9, 5, 8, 0);
container.setUnloadingCompletedAt(unloadedAt);
var context = new com.baiyue.firstleg.terminaldrayage.mapper.TerminalDrayageMapper.Context(
11L, container.getContainerNo(), 21L, "ACTIVE", unloadedAt);
LocalDateTime readyAt = unloadedAt.minusDays(1);
var actualFacts = new ContainerActualFacts(11L, 41L, 8, "READY", "PICKUP_READY", readyAt,
readyAt.minusDays(5), readyAt.minusDays(4), readyAt.minusDays(1), readyAt, null, null);
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);
when(mapper.contexts(List.of(11L))).thenReturn(List.of(context));
when(mapper.context(11L)).thenReturn(context);
when(mapper.findAll(List.of(11L))).thenReturn(List.of());
when(mapper.find(11L)).thenReturn(null);
when(carrierSchedules.containerActualFacts(List.of(11L))).thenReturn(List.of(actualFacts));
Set<String> permissions = Set.of("platform:firstleg:terminal-drayage:fleet:edit",
"platform:firstleg:terminal-drayage:empty-return:confirm");
var listed = service.overseasPage(query(WorkbenchScope.ACTIVE), permissions).records().get(0);
var detail = service.overseasDetail(11L, permissions);
for (var result : List.of(listed, detail)) {
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");
}
verify(mapper, never()).insert(any(com.baiyue.firstleg.terminaldrayage.entity.TerminalDrayageEntity.class));
}
@Test
void domesticHistoryDetailKeepsTheDepartedContainerAndItsUniqueLoadingPlan() {
ContainerBookingEntity container = container();
@@ -165,16 +165,61 @@ class TerminalDrayageServiceTest {
}
@Test
void doesNotOfferEmptyReturnWithoutExecutableDrayageAggregate() {
void offersEmptyReturnAfterUnloadWithoutDrayageAggregateOrPickupFact() {
LocalDateTime unloadedAt = LocalDateTime.of(2026, 9, 3, 0, 0);
Context context = context("PICKED_UP", unloadedAt);
Context context = context("ARRIVED_DESTINATION_PORT", unloadedAt);
when(mapper.context(11L)).thenReturn(context);
when(mapper.find(11L)).thenReturn(null);
var result = service.detail(11L);
assertThat(result.status()).isEqualTo("UNLOADED");
assertThat(result.allowedActions()).contains("RECORD_EMPTY_RETURN");
assertThat(result.actualPickupAt()).isNull();
assertThat(result.version()).isZero();
}
@Test
void createsReturnAggregateWithoutInventingPickupAndReplaysWithoutWritingAgain() {
Context context = context("ARRIVED_DESTINATION_PORT", LocalDateTime.of(2026, 9, 3, 0, 0));
when(mapper.lockContext(11L)).thenReturn(context);
when(mapper.context(11L)).thenReturn(context);
AtomicReference<TerminalDrayageEntity> stored = new AtomicReference<>();
AtomicReference<Operation> operation = new AtomicReference<>();
when(mapper.lock(11L)).thenAnswer(ignored -> stored.get());
when(mapper.find(11L)).thenAnswer(ignored -> stored.get());
when(mapper.insert(any(TerminalDrayageEntity.class))).thenAnswer(invocation -> {
stored.set(invocation.getArgument(0));
return 1;
});
when(mapper.operation(11L, "return-new")).thenAnswer(ignored -> operation.get());
when(mapper.insertOperation(anyLong(), anyLong(), anyString(), anyString(), anyString(), anyInt(), anyLong()))
.thenAnswer(invocation -> {
operation.set(new Operation(invocation.getArgument(2), invocation.getArgument(4), invocation.getArgument(5)));
return 1;
});
ActualTimeRequest request = new ActualTimeRequest(0, LocalDateTime.of(2026, 9, 4, 9, 0), "手动还柜");
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.fleetSupplierId()).isNull();
assertThat(result.version()).isEqualTo(1);
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());
}
@Test
void rejectsReturnBeforeUnloadWithoutAggregateOrPickup() {
when(mapper.lockContext(11L)).thenReturn(context("ARRIVED_DESTINATION_PORT", LocalDateTime.of(2026, 9, 3, 0, 0)));
assertThatThrownBy(() -> service.recordEmptyReturn(11L, 7L, "admin", "return-early",
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());
}
@Test