fix(firstleg): expose container documents and aggregate departure gates
This commit is contained in:
+5
@@ -33,6 +33,7 @@ class ContainerControllerContractTest {
|
||||
assertPermission("logisticsDocument", "platform:firstleg:container:list");
|
||||
assertPermission("uploadLogisticsDocument", "platform:firstleg:container:edit");
|
||||
assertPermission("downloadDocument", "platform:firstleg:container:list");
|
||||
assertPermission("downloadClearance", "platform:firstleg:customs:manage");
|
||||
}
|
||||
|
||||
@Test void overviewOpenApiFreezesContainerIdBasedPdaContract() throws Exception {
|
||||
@@ -40,6 +41,8 @@ class ContainerControllerContractTest {
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
assertThat(openapi).contains("/api/platform/v1/containers/{id}/overview:",
|
||||
"/documents/{documentType}/{fileId}/download:", "ExceptionRequest",
|
||||
"/clearance/document/{fileId}/download:",
|
||||
"DESTINATION_CLEARANCE、CUSTOMS_DECLARATION、DELIVERY_ORDER、PICKUP_ORDER",
|
||||
"enum: [DELIVERY_ORDER, PICKUP_ORDER]", "Idempotency-Key", "photoCounts:",
|
||||
"/pda/photos/{fileId}/download:", "downloadPath:", "workflowStatus:",
|
||||
"AWAITING_REVIEW", "version: {type: integer, minimum: 0}",
|
||||
@@ -77,6 +80,8 @@ class ContainerControllerContractTest {
|
||||
.doesNotContain("tenantId")
|
||||
.doesNotContain("accountId");
|
||||
assertThat(openapi)
|
||||
.contains("/api/platform/v1/containers/{id}/clearance/document/{fileId}/download:")
|
||||
.contains("Content-Disposition 与 Cache-Control no-store")
|
||||
.contains("required: [oceanBookingId, requirementId, containerNo, expectedVersion]")
|
||||
.contains("required: [id, bookingNo, cabinetNo, sourceType, recordStatus")
|
||||
.contains("sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING]")
|
||||
|
||||
+19
@@ -16,6 +16,8 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -41,6 +43,8 @@ class LoadingListControllerContractTest {
|
||||
ConfirmedQuery.class);
|
||||
authority("confirmedDetail", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("declaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class, long.class, long.class);
|
||||
authority("downloadDeclaration", "platform:firstleg:loading-list:list", AuthenticatedUser.class,
|
||||
long.class, long.class, long.class);
|
||||
authority("attachDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
|
||||
long.class, long.class, String.class, DocumentAttachRequest.class);
|
||||
authority("uploadDeclaration", "platform:firstleg:customs:manage", AuthenticatedUser.class,
|
||||
@@ -88,6 +92,8 @@ class LoadingListControllerContractTest {
|
||||
.contains("oceanBookingStatus")
|
||||
.contains("pdaTaskId")
|
||||
.contains("cabinetNo")
|
||||
.contains("LOADING_CUSTOMS_DECLARATION")
|
||||
.contains("Cache-Control 为 no-store")
|
||||
.doesNotContain("internalContainerNo")
|
||||
.doesNotContain("rollback")
|
||||
.doesNotContain("delete:")
|
||||
@@ -118,6 +124,18 @@ class LoadingListControllerContractTest {
|
||||
assertThat(method.getParameters()[6].getAnnotation(RequestPart.class)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customsDownloadUsesStablePlanPathAndBinaryResponse() throws Exception {
|
||||
Method method = PlatformLoadingListController.class.getDeclaredMethod("downloadDeclaration",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class);
|
||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly(
|
||||
"/loading-lists/{planId}/customs/document/{fileId}/download");
|
||||
assertThat(method.getReturnType()).isEqualTo(ResponseEntity.class);
|
||||
assertThat(method.getParameters()[1].getName()).isEqualTo("planId");
|
||||
assertThat(method.getParameters()[2].getName()).isEqualTo("fileId");
|
||||
assertThat(method.getParameters()[3].getName()).isEqualTo("warehouseId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void yamlParses() throws Exception {
|
||||
String yaml = new ClassPathResource("openapi/loading-lists-v1.yaml")
|
||||
@@ -134,6 +152,7 @@ class LoadingListControllerContractTest {
|
||||
"/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/document/{fileId}/download",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/document/upload",
|
||||
"/api/platform/v1/first-leg/loading-lists/{planId}/customs/complete");
|
||||
Map<String, Object> components = stringMap(document.get("components"));
|
||||
|
||||
+3
-2
@@ -24,15 +24,16 @@ class CustomsDispatchGateContributorTest {
|
||||
|
||||
@Test void passesOnlyCanonicalClearanceCompletedCase() {
|
||||
CustomsCaseEntity value = new CustomsCaseEntity();
|
||||
value.setStatus("RECEIPT_AVAILABLE");
|
||||
when(repository.gateCase(10001L, 21L, 31L, 81L)).thenReturn(value);
|
||||
assertThat(contributor.assess(context()).passed()).isFalse();
|
||||
value.setStatus("CLEARANCE_COMPLETED");
|
||||
when(repository.clearedContainerCase(81L)).thenReturn(value);
|
||||
assertThat(contributor.assess(context()))
|
||||
.satisfies(item -> {
|
||||
assertThat(item.passed()).isTrue();
|
||||
assertThat(item.requirement()).isEqualTo("CLEARANCE_COMPLETED");
|
||||
});
|
||||
verify(repository, times(2)).clearedContainerCase(81L);
|
||||
verify(repository, never()).gateCase(anyLong(), anyLong(), anyLong(), anyLong());
|
||||
}
|
||||
|
||||
private static Context context() { return new Context(10001L, 10002L, 21L, 31L, 81L, 91L, null); }
|
||||
|
||||
+14
-1
@@ -47,10 +47,23 @@ class OutboundDispatchReadMapperContractTest {
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql)
|
||||
.contains("seal_no<>''")
|
||||
.contains("evidence.seal_no<>''")
|
||||
.doesNotContain("<", ">");
|
||||
}
|
||||
|
||||
@Test
|
||||
void evidenceGateAggregatesOnePhysicalEvidenceSetAcrossTheWholeLoadingPlan() throws Exception {
|
||||
String sql = OutboundDispatchReadMapper.class.getMethod("evidence", long.class, long.class)
|
||||
.getAnnotation(Select.class).value()[0];
|
||||
|
||||
assertThat(sql)
|
||||
.contains("plan.id=#{loadingPlanId}", "plan.container_booking_id=#{containerBookingId}")
|
||||
.contains("item.loading_plan_id=plan.id", "item.deleted=0")
|
||||
.contains("evidence.inbound_id=item.inbound_id", "evidence.tenant_id=item.account_id")
|
||||
.contains("COUNT(DISTINCT CASE", "evidence.evidence_type")
|
||||
.doesNotContain("WHERE tenant_id=#{tenantId}", "inbound_id=#{inboundId}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void oceanAndTruckQueriesAreResolvedOnceFromTheContainerRoot() throws Exception {
|
||||
String oceanSql = OutboundDispatchReadMapper.class.getMethod("ocean", long.class)
|
||||
|
||||
+46
-9
@@ -14,6 +14,7 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.LongStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
@@ -30,18 +31,38 @@ class OutboundDispatchServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void gateAggregatesEveryRealInboundAndFailsClosedWhenOneBindingFails() {
|
||||
void containerEvidenceFailureFailsEveryBindingConsistently() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
stubContainer();
|
||||
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
|
||||
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
|
||||
|
||||
ContainerGateResult result = service.checkGate(22L);
|
||||
|
||||
assertThat(result.passed()).isFalse();
|
||||
assertThat(result.inbounds()).hasSize(2);
|
||||
assertThat(result.inbounds().get(1).gate().items())
|
||||
assertThat(result.inbounds()).allSatisfy(inbound -> assertThat(inbound.gate().items())
|
||||
.filteredOn(item -> "LOADING_EVIDENCE_COMPLETE".equals(item.code()))
|
||||
.extracting(GateItem::passed).containsExactly(false);
|
||||
.extracting(GateItem::passed).containsExactly(false));
|
||||
verify(repository, times(1)).evidence(22L, 44L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void tenInboundBindingsShareOneCompleteEvidenceSetAndOneContainerCustomsResult() {
|
||||
OutboundDispatchGateContributor customs = mock(OutboundDispatchGateContributor.class);
|
||||
when(customs.code()).thenReturn("CUSTOMS_CLEARANCE");
|
||||
when(customs.assess(any())).thenReturn(new GateItem(
|
||||
"CUSTOMS_CLEARANCE", "报关完成", true, "CLEARANCE_COMPLETED", "CLEARANCE_COMPLETED"));
|
||||
OutboundDispatchService service = new OutboundDispatchService(repository, List.of(customs), null);
|
||||
List<OutboundDispatchReadMapper.Binding> tenBindings = LongStream.rangeClosed(11L, 20L)
|
||||
.mapToObj(id -> binding(id, "I-" + id)).toList();
|
||||
stubContainer(tenBindings);
|
||||
|
||||
ContainerGateResult result = service.checkGate(22L);
|
||||
|
||||
assertThat(result.passed()).isTrue();
|
||||
assertThat(result.inbounds()).hasSize(10).allSatisfy(value -> assertThat(value.gate().passed()).isTrue());
|
||||
verify(repository, times(1)).evidence(22L, 44L);
|
||||
verify(customs, times(1)).assess(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -72,6 +93,19 @@ class OutboundDispatchServiceTest {
|
||||
verify(repository, times(2)).confirm(anyLong(), eq(0), eq(9L), eq("admin"), isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmAllowsMissingSailingScheduleContributor() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
stubContainer();
|
||||
stubConfirmMutation("no-schedule");
|
||||
|
||||
Detail result = service.confirm(22L, 9L, "admin", "no-schedule", new ActionRequest(0, null));
|
||||
|
||||
assertThat(result.aggregate().gate().passed()).isTrue();
|
||||
assertThat(result.aggregate().gate().inbounds()).allSatisfy(inbound -> assertThat(inbound.gate().items())
|
||||
.extracting(GateItem::code).doesNotContain("SAILING_SCHEDULE"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsOneVersionAndAllInboundBindings() {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
@@ -117,7 +151,7 @@ class OutboundDispatchServiceTest {
|
||||
OutboundDispatchService service = serviceWithoutCarrier();
|
||||
stubContainer();
|
||||
when(repository.lockBindings(22L)).thenReturn(bindings());
|
||||
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
|
||||
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(4, 1));
|
||||
when(repository.claim(eq(9L), eq("CONFIRM_DISPATCH"), eq("blocked"), anyString())).thenReturn(true);
|
||||
|
||||
assertThatThrownBy(() -> service.confirm(22L, 9L, "admin", "blocked", new ActionRequest(0, null)))
|
||||
@@ -201,16 +235,19 @@ class OutboundDispatchServiceTest {
|
||||
|
||||
private OutboundDispatchService service(CarrierScheduleQueryService carrierSchedules) {
|
||||
return new OutboundDispatchService(repository,
|
||||
List.of(passing("CUSTOMS_CLEARANCE"), passing("SAILING_SCHEDULE")), carrierSchedules);
|
||||
List.of(passing("CUSTOMS_CLEARANCE")), carrierSchedules);
|
||||
}
|
||||
|
||||
private void stubContainer() {
|
||||
when(repository.bindings(22L)).thenReturn(bindings());
|
||||
stubContainer(bindings());
|
||||
}
|
||||
|
||||
private void stubContainer(List<OutboundDispatchReadMapper.Binding> values) {
|
||||
when(repository.bindings(22L)).thenReturn(values);
|
||||
when(repository.pick(44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(55L, "COMPLETED"));
|
||||
when(repository.ocean(22L)).thenReturn(new OutboundDispatchReadMapper.OceanBinding(66L, "RELEASED", 7L));
|
||||
when(repository.truck(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.IdStatus(77L, "DEPARTED"));
|
||||
when(repository.evidence(7L, 11L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
|
||||
when(repository.evidence(7L, 12L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
|
||||
when(repository.evidence(22L, 44L)).thenReturn(new OutboundDispatchReadMapper.Evidence(5, 1));
|
||||
}
|
||||
|
||||
private void stubConfirmMutation(String key) {
|
||||
|
||||
+3
@@ -50,6 +50,9 @@ class OutboundDispatchSqlContractTest {
|
||||
.contains("Once an actual DEPARTED_WAREHOUSE node exists")
|
||||
.contains("absence never blocks manual outbound confirmation")
|
||||
.contains("PENDING or any non-terminal state never affects gate.passed")
|
||||
.contains("Loading evidence is evaluated once for the physical container/loading plan")
|
||||
.contains("Customs clearance is also resolved once at container scope")
|
||||
.contains("missing standalone sailing schedule is reference-only")
|
||||
.contains("expectedVersion")
|
||||
.contains("Idempotency-Key")
|
||||
.doesNotContain("confirm-dispatch", "reverse-dispatch", "check-gate", "inboundId, containerBookingId");
|
||||
|
||||
+8
@@ -12,6 +12,14 @@ import java.nio.file.Path;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ContainerOverviewSqlContractTest {
|
||||
@Test void overviewIncludesExactlyTheFourCurrentBusinessDocumentTypes() throws Exception {
|
||||
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("documents", long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
assertThat(sql)
|
||||
.contains("'DESTINATION_CLEARANCE'", "'CUSTOMS_DECLARATION'", "'DELIVERY_ORDER'", "'PICKUP_ORDER'")
|
||||
.contains("ORDER BY document_type");
|
||||
}
|
||||
|
||||
@Test void pdaSummarySelectsRecordFieldsExplicitlyIncludingTaskVersion() throws Exception {
|
||||
String sql = String.join(" ", ContainerOverviewMapper.class.getDeclaredMethod("pickTask", long.class)
|
||||
.getAnnotation(Select.class).value());
|
||||
|
||||
+16
-1
@@ -47,7 +47,13 @@ class ContainerOverviewServiceTest {
|
||||
71L, 81L, 91L, "LOADING_DOOR_OPEN", 101L, "door.jpg", "ACTIVE", LocalDateTime.now()),
|
||||
new ContainerOverviewMapper.PhotoRow(
|
||||
72L, 81L, 91L, "LOADING_DOOR_OPEN", 102L, "void.jpg", "VOID", LocalDateTime.now())));
|
||||
when(mapper.documents(41L)).thenReturn(java.util.List.of());
|
||||
com.baiyue.firstleg.entity.ContainerDocumentEntity customs = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
|
||||
customs.setId(103L); customs.setDocumentType("CUSTOMS_DECLARATION"); customs.setFileId(104L);
|
||||
customs.setFileName("customs.pdf"); customs.setVersion(3);
|
||||
com.baiyue.firstleg.entity.ContainerDocumentEntity clearance = new com.baiyue.firstleg.entity.ContainerDocumentEntity();
|
||||
clearance.setId(105L); clearance.setDocumentType("DESTINATION_CLEARANCE"); clearance.setFileId(106L);
|
||||
clearance.setFileName("clearance.pdf"); clearance.setVersion(1);
|
||||
when(mapper.documents(41L)).thenReturn(java.util.List.of(customs, clearance));
|
||||
when(mapper.exceptions(41L)).thenReturn(java.util.List.of());
|
||||
when(mapper.transport(41L)).thenReturn(java.util.List.of());
|
||||
when(mapper.pickTimeline(61L)).thenReturn(java.util.List.of());
|
||||
@@ -66,6 +72,15 @@ class ContainerOverviewServiceTest {
|
||||
assertThat(count.evidenceType()).isEqualTo("LOADING_DOOR_OPEN");
|
||||
assertThat(count.count()).isEqualTo(1);
|
||||
});
|
||||
assertThat(overview.documents()).extracting(document -> document.documentType())
|
||||
.containsExactly("CUSTOMS_DECLARATION", "DESTINATION_CLEARANCE");
|
||||
assertThat(overview.documents()).filteredOn(document -> "CUSTOMS_DECLARATION".equals(document.documentType()))
|
||||
.singleElement().satisfies(document -> assertThat(document.version()).isEqualTo(3));
|
||||
assertThat(overview.documents()).filteredOn(document -> "DESTINATION_CLEARANCE".equals(document.documentType()))
|
||||
.singleElement().satisfies(document -> {
|
||||
assertThat(document.fileId()).isEqualTo(106L);
|
||||
assertThat(document.version()).isEqualTo(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test void downloadsOnlyActivePhotoOwnedByContainerLoadingPlan() {
|
||||
|
||||
@@ -450,16 +450,16 @@ class LoadingListServiceTest {
|
||||
String contentHash = sha256Bytes(content);
|
||||
var file = new MockMultipartFile("file", "customs.pdf", "application/pdf", content);
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(101L, 0L,
|
||||
"LOADING_CUSTOMS_DOCUMENT", "10", "customs.pdf", contentHash, "application/pdf",
|
||||
"LOADING_CUSTOMS_DECLARATION", "10", "customs.pdf", contentHash, "application/pdf",
|
||||
(long) content.length);
|
||||
ContainerDocumentEntity saved = document(41L, LoadingListService.CUSTOMS_DECLARATION, 0, false);
|
||||
saved.setFileId(101L); saved.setFileName("customs.pdf"); saved.setFileHash(contentHash);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.history(anyString())).thenReturn(null);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(null, saved);
|
||||
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
|
||||
when(documentFiles.upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(file))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(101L))).thenReturn(ApiResponse.success(metadata));
|
||||
when(repository.createDocument(org.mockito.ArgumentMatchers.any())).thenReturn(1);
|
||||
|
||||
@@ -467,10 +467,108 @@ class LoadingListServiceTest {
|
||||
10L, 100L, 9L, "operator", "customs-upload", 0, null, file);
|
||||
|
||||
assertThat(result.documentType()).isEqualTo(LoadingListService.CUSTOMS_DECLARATION);
|
||||
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DOCUMENT"),
|
||||
verify(documentFiles).upload(anyString(), eq(9L), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(file));
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadsCompletedLoadingDeclarationWithExactLongFileOwnership() {
|
||||
long planId = 9_007_199_254_740_993L;
|
||||
long fileId = 9_007_199_254_740_995L;
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
plan.setPlanId(planId);
|
||||
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
|
||||
document.setFileId(fileId);
|
||||
document.setFileName("出口报关单.pdf");
|
||||
byte[] content = new byte[]{1, 2, 3};
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(fileId, 0L,
|
||||
"LOADING_CUSTOMS_DECLARATION", Long.toString(planId), "出口报关单.pdf", hash(),
|
||||
"application/pdf", (long) content.length);
|
||||
when(repository.aggregate(planId)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq(Long.toString(planId)), eq(fileId))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq(Long.toString(planId)), eq(fileId))).thenReturn(org.springframework.http.ResponseEntity.ok(content));
|
||||
|
||||
var result = service.downloadLoadingDeclaration(planId, 100L, fileId);
|
||||
|
||||
assertThat(result.fileName()).isEqualTo("出口报关单.pdf");
|
||||
assertThat(result.content()).containsExactly(1, 2, 3);
|
||||
verify(documentFiles).download(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("9007199254740993"), eq(fileId));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCurrentLoadingDeclarationFileIdBeforeFileService() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION))
|
||||
.thenReturn(document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true));
|
||||
|
||||
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 100L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
|
||||
verify(documentFiles, never()).metadata(anyString(), anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsLoadingDeclarationFileWithMismatchedBusinessOwnership() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
ContainerDocumentEntity document = document(41L, LoadingListService.CUSTOMS_DECLARATION, 4, true);
|
||||
when(repository.aggregate(10L)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.CUSTOMS_DECLARATION)).thenReturn(document);
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(99L, 0L,
|
||||
"LOADING_CUSTOMS_DECLARATION", "other-plan", "customs.pdf", hash(),
|
||||
"application/pdf", 3L);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("LOADING_CUSTOMS_DECLARATION"),
|
||||
eq("10"), eq(99L))).thenReturn(ApiResponse.success(metadata));
|
||||
|
||||
assertThatThrownBy(() -> service.downloadLoadingDeclaration(10L, 100L, 99L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
|
||||
verify(documentFiles, never()).download(anyString(), anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadsCurrentDestinationClearanceWithExactContainerOwnership() {
|
||||
long bookingId = 9_007_199_254_740_993L;
|
||||
long fileId = 9_007_199_254_740_995L;
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
ContainerDocumentEntity document = document(41L, LoadingListService.DESTINATION_CLEARANCE, 1, true);
|
||||
document.setContainerBookingId(bookingId);
|
||||
document.setFileId(fileId);
|
||||
document.setFileName("destination-clearance.pdf");
|
||||
byte[] content = new byte[]{4, 5, 6};
|
||||
var metadata = new LoadingDocumentFileClient.Metadata(fileId, 0L,
|
||||
"CONTAINER_CLEARANCE_DOCUMENT", Long.toString(bookingId), "destination-clearance.pdf",
|
||||
hash(), "application/pdf", (long) content.length);
|
||||
when(repository.aggregateByBooking(bookingId)).thenReturn(plan);
|
||||
when(repository.document(bookingId, LoadingListService.DESTINATION_CLEARANCE)).thenReturn(document);
|
||||
when(documentFiles.metadata(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq(Long.toString(bookingId)), eq(fileId))).thenReturn(ApiResponse.success(metadata));
|
||||
when(documentFiles.download(anyString(), eq(0L), eq("CONTAINER_CLEARANCE_DOCUMENT"),
|
||||
eq(Long.toString(bookingId)), eq(fileId))).thenReturn(org.springframework.http.ResponseEntity.ok(content));
|
||||
|
||||
var result = service.downloadContainerClearance(bookingId, fileId);
|
||||
|
||||
assertThat(result.fileName()).isEqualTo("destination-clearance.pdf");
|
||||
assertThat(result.content()).containsExactly(4, 5, 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsNonCurrentDestinationClearanceFileIdAsNotFound() {
|
||||
AggregateRow plan = row("COMPLETED", 4);
|
||||
when(repository.aggregateByBooking(30L)).thenReturn(plan);
|
||||
when(repository.document(30L, LoadingListService.DESTINATION_CLEARANCE))
|
||||
.thenReturn(document(41L, LoadingListService.DESTINATION_CLEARANCE, 1, true));
|
||||
|
||||
assertThatThrownBy(() -> service.downloadContainerClearance(30L, 100L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.extracting(error -> ((BizException) error).getResultCode()).isEqualTo(ResultCode.NOT_FOUND);
|
||||
verify(documentFiles, never()).metadata(anyString(), anyLong(), anyString(), anyString(), anyLong());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReusedIdempotencyKeyWithDifferentAttachRequest() {
|
||||
AggregateRow plan = row("LOADING", 2);
|
||||
|
||||
Reference in New Issue
Block a user