This commit is contained in:
2026-08-12 20:36:11 +08:00
parent 0380c0553b
commit 0ed1531683
26 changed files with 1690 additions and 15 deletions
@@ -5,6 +5,8 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
import com.baiyue.firstleg.service.LoadingListService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@@ -32,6 +34,11 @@ class LoadingListControllerContractTest {
authority("confirmedPage", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
ConfirmedQuery.class);
authority("confirmedDetail", "platform:firstleg:loading-list:view", AuthenticatedUser.class, long.class, long.class);
authority("declaration", "platform:firstleg:loading-list:view", AuthenticatedUser.class, long.class, long.class);
authority("attachDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
long.class, long.class, String.class, DocumentAttachRequest.class);
authority("completeDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
long.class, long.class, String.class, DocumentCompleteRequest.class);
}
@Test
@@ -91,13 +98,17 @@ class LoadingListControllerContractTest {
"/api/platform/v1/first-leg/loading-plans/{planId}",
"/api/platform/v1/first-leg/loading-plans/{planId}/transport",
"/api/platform/v1/first-leg/loading-lists",
"/api/platform/v1/first-leg/loading-lists/{planId}");
"/api/platform/v1/first-leg/loading-lists/{planId}",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document",
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/complete");
Map<String, Object> components = stringMap(document.get("components"));
Map<String, Object> schemas = stringMap(components.get("schemas"));
assertThat(schemas).containsKeys(
"LongId", "PreloadPage", "ConfirmedPage", "Customer", "PreloadSummary",
"ConfirmedSummary", "LoadingItem", "OperationHistory", "PreloadDetail",
"ConfirmedDetail", "TransportUpdateRequest");
"ConfirmedDetail", "TransportUpdateRequest", "BusinessDocumentEnvelope",
"DocumentAttachRequest", "DocumentCompleteRequest", "BusinessDocument");
assertThat(paths)
.doesNotContainKey("/api/platform/v1/first-leg/loading-plans/{planId}/complete");
assertThat(stringMap(schemas.get("LongId"))).containsEntry("type", "string");
@@ -6,6 +6,11 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentCompleteRequest;
import com.baiyue.firstleg.client.LoadingDocumentFileClient;
import com.baiyue.firstleg.entity.ContainerDocumentEntity;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
import com.baiyue.firstleg.repository.LoadingListRepository;
@@ -38,6 +43,8 @@ class LoadingListServiceTest {
private LoadingListRepository repository;
@MockBean
private LoadingPickRepository pickRepository;
@MockBean
private LoadingDocumentFileClient documentFiles;
@Resource
private LoadingListService service;
@@ -189,6 +196,116 @@ class LoadingListServiceTest {
.isEqualTo(ResultCode.NOT_FOUND);
}
@Test
void attachesRealContainerClearanceFileAndWritesImmutableHistory() {
AggregateRow plan = row("LOADING", 2);
plan.setContainerBookingId(30L);
when(repository.aggregateByBooking(30L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE))
.thenReturn(null, document(40L, LoadingListService.DESTINATION_CLEARANCE, 0, false));
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
eq("30"), eq(99L))).thenReturn(ApiResponse.success(new LoadingDocumentFileClient.Metadata(
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "30", "clearance.pdf", "abc", "application/pdf", 20L)));
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
var result = service.attachContainerClearance(30L, 9L, "operator", "key-clearance",
new DocumentAttachRequest(99L, 0, "上传清关资料"));
assertThat(result.fileName()).isEqualTo("clearance.pdf");
verify(repository).appendHistory(eq(10L), anyString(), eq("ATTACH_DESTINATION_CLEARANCE"),
eq("LOADING"), eq("LOADING"), anyString(), anyString(), eq(9L), eq("operator"),
eq("上传清关资料"));
}
@Test
void rejectsDeclarationBeforeLoadingListIsCompleted() {
AggregateRow plan = row("LOADING", 2);
plan.setContainerBookingId(30L);
when(repository.aggregate(10L)).thenReturn(plan);
assertThatThrownBy(() -> service.attachLoadingDeclaration(10L, 100L, 9L, "operator", "key",
new DocumentAttachRequest(99L, 0, null)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
verify(documentFiles, never()).metadata(anyString(), eq(0L), anyString(), anyString(), eq(99L));
}
@Test
void rejectsFileMetadataFromAnotherBusinessResource() {
AggregateRow plan = row("LOADING", 2);
plan.setContainerBookingId(30L);
when(repository.aggregateByBooking(30L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(null);
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
eq("30"), eq(99L))).thenReturn(ApiResponse.success(new LoadingDocumentFileClient.Metadata(
99L, 0L, "CONTAINER_CLEARANCE_DOCUMENT", "different-booking", "clearance.pdf", "abc",
"application/pdf", 20L)));
assertThatThrownBy(() -> service.attachContainerClearance(30L, 9L, "operator", "key",
new DocumentAttachRequest(99L, 0, null)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.BAD_REQUEST);
verify(repository, never()).createDocument(org.mockito.ArgumentMatchers.any());
}
@Test
void completesDeclarationOnlyWhenDocumentVersionMatches() {
AggregateRow plan = row("COMPLETED", 2);
plan.setContainerBookingId(30L);
ContainerDocumentEntity before = document(40L, LoadingListService.CUSTOMS_DECLARATION, 3, false);
ContainerDocumentEntity after = document(40L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
when(repository.aggregate(10L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(before, after);
when(repository.completeDocument(40L, 3, 9L, "operator")).thenReturn(1);
var result = service.completeLoadingDeclaration(10L, 100L, 9L, "operator", "complete-key",
new DocumentCompleteRequest(3, "点击报关"));
assertThat(result.completedAt()).isNotNull();
verify(repository).appendHistory(eq(10L), anyString(), eq("COMPLETE_CUSTOMS_DECLARATION"),
eq("COMPLETED"), eq("COMPLETED"), anyString(), anyString(), eq(9L), eq("operator"),
eq("点击报关"));
}
@Test
void rejectsCompleteWithoutUploadedFile() {
AggregateRow plan = row("LOADING", 2);
plan.setContainerBookingId(30L);
when(repository.aggregateByBooking(30L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(null);
assertThatThrownBy(() -> service.completeContainerClearance(30L, 9L, "operator", "key",
new DocumentCompleteRequest(0, null)))
.isInstanceOf(BizException.class)
.extracting(error -> ((BizException) error).getResultCode())
.isEqualTo(ResultCode.CONFLICT);
verify(repository, never()).completeDocument(org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyInt(), eq(9L), anyString());
}
@Test
void repeatedCompleteWithCurrentVersionIsNaturallyIdempotent() {
AggregateRow plan = row("LOADING", 2);
plan.setContainerBookingId(30L);
ContainerDocumentEntity completed = document(40L, LoadingListService.DESTINATION_CLEARANCE, 4, true);
when(repository.aggregateByBooking(30L)).thenReturn(plan);
when(repository.history(anyString())).thenReturn(null);
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(completed);
var result = service.completeContainerClearance(30L, 9L, "operator", "new-key",
new DocumentCompleteRequest(4, null));
assertThat(result.completedAt()).isNotNull();
verify(repository, never()).completeDocument(org.mockito.ArgumentMatchers.anyLong(),
org.mockito.ArgumentMatchers.anyInt(), eq(9L), anyString());
}
private static AggregateRow row(String status, int version) {
AggregateRow row = new AggregateRow();
row.setPlanId(10L);
@@ -198,4 +315,14 @@ class LoadingListServiceTest {
row.setVersion(version);
return row;
}
private static ContainerDocumentEntity document(long id, String type, int version, boolean completed) {
ContainerDocumentEntity value = new ContainerDocumentEntity();
value.setId(id); value.setContainerBookingId(30L); value.setDocumentType(type);
value.setFileId(99L); value.setFileName("clearance.pdf"); value.setFileHash("abc");
value.setUploadedBy(9L); value.setUploadedByName("operator"); value.setVersion(version);
value.setCreatedAt(java.time.LocalDateTime.now());
if (completed) value.setCompletedAt(java.time.LocalDateTime.now());
return value;
}
}