fix(file): support optional legal ID and order import files
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.file.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class QualificationAndOrderFileOpenApiContractTest {
|
||||
@Test
|
||||
void qualificationPackageRequiresOnlyTheBusinessLicenseFile() throws Exception {
|
||||
String yaml = Files.readString(Path.of(
|
||||
"src/main/resources/openapi/qualification-required-package-v1.yaml"));
|
||||
|
||||
assertThat(yaml)
|
||||
.contains("required: [businessLicense]")
|
||||
.contains("legalPersonId:", "法人身份证,选填")
|
||||
.contains("legalPersonIdQualificationId:", "legalPersonIdDocumentUrl:")
|
||||
.doesNotContain("required: [businessLicense, legalPersonId]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderBatchCategoriesAreCustomerOwnedXlsxObjectsWithStableBatchNumber() throws Exception {
|
||||
String yaml = Files.readString(Path.of(
|
||||
"src/main/resources/openapi/order-batch-import-file-v1.yaml"));
|
||||
|
||||
assertThat(yaml)
|
||||
.contains("ORDER_BATCH_IMPORT_SOURCE", "ORDER_BATCH_IMPORT_ERROR_REPORT")
|
||||
.contains("ownerScope: {type: string, enum: [CUSTOMER]}")
|
||||
.contains("pattern: '^OBI-[0-9A-F]{20}$'")
|
||||
.contains("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||
.contains("maximum: 5242880")
|
||||
.contains("Cache-Control", "no-store")
|
||||
.doesNotContain("ORDER_IMPORT_SOURCE", "ORDER_IMPORT_ERROR");
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
@@ -81,6 +82,12 @@ class QualificationFileContractTest {
|
||||
"/{tenantId}/required-package"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requiredPackageKeepsBusinessLicenseRequiredAndMakesLegalPersonIdOptional() {
|
||||
assertRequiredPackageParts(QualificationFileController.class);
|
||||
assertRequiredPackageParts(PlatformQualificationFileController.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformQualificationPreviewUsesTheCustomerListPermission() throws Exception {
|
||||
var method = PlatformQualificationFileController.class.getDeclaredMethod(
|
||||
@@ -301,6 +308,18 @@ class QualificationFileContractTest {
|
||||
&& Arrays.asList(mapping.consumes()).contains(MediaType.MULTIPART_FORM_DATA_VALUE));
|
||||
}
|
||||
|
||||
private static void assertRequiredPackageParts(Class<?> controllerType) {
|
||||
var method = Arrays.stream(controllerType.getDeclaredMethods())
|
||||
.filter(candidate -> candidate.getName().equals("uploadRequiredPackage"))
|
||||
.findFirst().orElseThrow();
|
||||
var parts = Arrays.stream(method.getParameters())
|
||||
.map(parameter -> parameter.getAnnotation(RequestPart.class))
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.collect(java.util.stream.Collectors.toMap(RequestPart::value, RequestPart::required));
|
||||
assertEquals(Boolean.TRUE, parts.get("businessLicense"));
|
||||
assertEquals(Boolean.FALSE, parts.get("legalPersonId"));
|
||||
}
|
||||
|
||||
private QualificationFileController merchantController(
|
||||
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
||||
var controller = merchantController;
|
||||
|
||||
@@ -86,6 +86,36 @@ class BusinessFileStorageTest {
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("仅支持");
|
||||
}
|
||||
|
||||
@Test void orderBatchCategoriesAcceptOnlyRealXlsxWithinFiveMegabytes() {
|
||||
BusinessFileStorage storage = storage();
|
||||
var xlsx = new MockMultipartFile("file", "orders.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
new byte[]{0x50, 0x4b, 0x03, 0x04, 1, 2});
|
||||
|
||||
var stored = storage.store(10001L, "OBI-0123456789ABCDEF0123", xlsx,
|
||||
BusinessFileCategory.ORDER_BATCH_IMPORT_SOURCE);
|
||||
|
||||
assertThat(stored.objectKey()).endsWith(".xlsx");
|
||||
assertThat(stored.contentType()).isEqualTo(xlsx.getContentType());
|
||||
|
||||
var wrongContent = new MockMultipartFile("file", "orders.xlsx", "application/octet-stream",
|
||||
new byte[]{0x50, 0x4b, 0x03, 0x04});
|
||||
assertThatThrownBy(() -> storage.store(10001L, "OBI-0123456789ABCDEF0123", wrongContent,
|
||||
BusinessFileCategory.ORDER_BATCH_IMPORT_SOURCE))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("仅支持XLSX");
|
||||
|
||||
var fakeXlsx = new MockMultipartFile("file", "orders.xlsx", xlsx.getContentType(), new byte[]{1, 2, 3, 4});
|
||||
assertThatThrownBy(() -> storage.store(10001L, "OBI-0123456789ABCDEF0123", fakeXlsx,
|
||||
BusinessFileCategory.ORDER_BATCH_IMPORT_ERROR_REPORT))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("不是有效XLSX");
|
||||
|
||||
MultipartFile tooLarge = new SparseMultipartFile("orders.xlsx", xlsx.getContentType(),
|
||||
BusinessFileCategory.ORDER_IMPORT_MAX_SIZE + 1);
|
||||
assertThatThrownBy(() -> storage.store(10001L, "OBI-0123456789ABCDEF0123", tooLarge,
|
||||
BusinessFileCategory.ORDER_BATCH_IMPORT_SOURCE))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("5MB");
|
||||
}
|
||||
|
||||
private BusinessFileStorage storage() {
|
||||
BusinessFileStorage storage = new BusinessFileStorage();
|
||||
ReflectionTestUtils.setField(storage, "configuredRoot", root);
|
||||
|
||||
@@ -25,7 +25,7 @@ class InternalBusinessFileServiceTest {
|
||||
|
||||
@Test void customsDocumentUploadPersistsExactBusinessOwnership() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "invoice.txt", "text/plain", "invoice".getBytes());
|
||||
when(storage.store(10001L, 41L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false)).thenReturn(new BusinessFileStorage.Stored(
|
||||
when(storage.store(10001L, "41", file, BusinessFileCategory.CUSTOMS_DOCUMENT)).thenReturn(new BusinessFileStorage.Stored(
|
||||
"business-files/10001/41/key.txt", "text/plain", 7, "a".repeat(64), "invoice.txt"));
|
||||
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
|
||||
((FileObjectEntity) invocation.getArgument(0)).setId(71L); return 1;
|
||||
@@ -41,10 +41,10 @@ class InternalBusinessFileServiceTest {
|
||||
|
||||
@Test void platformContainerAndLoadingDocumentsSupportZeroTenantScope() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "customs.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||
when(storage.store(eq(0L), anyLong(), same(file), eq(BusinessFileCategory.STANDARD_MAX_SIZE), eq(false))).thenAnswer(invocation ->
|
||||
when(storage.store(eq(0L), anyString(), same(file), any(BusinessFileCategory.class))).thenAnswer(invocation ->
|
||||
new BusinessFileStorage.Stored("business-files/0/" + invocation.getArgument(1) + "/customs.pdf",
|
||||
"application/pdf", file.getSize(), "b".repeat(64), "customs.pdf"));
|
||||
when(storage.store(11L, 51L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false)).thenReturn(new BusinessFileStorage.Stored(
|
||||
when(storage.store(11L, "51", file, BusinessFileCategory.OVERSEAS_UNLOAD_PHOTO)).thenReturn(new BusinessFileStorage.Stored(
|
||||
"business-files/11/51/unload.jpg", "image/jpeg", file.getSize(), "c".repeat(64), "unload.jpg"));
|
||||
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
|
||||
((FileObjectEntity) invocation.getArgument(0)).setId(81L); return 1;
|
||||
@@ -68,15 +68,17 @@ class InternalBusinessFileServiceTest {
|
||||
InternalBusinessFileService.Metadata::businessType,
|
||||
InternalBusinessFileService.Metadata::businessId)
|
||||
.containsExactly(11L, "OVERSEAS_UNLOAD_PHOTO", "51");
|
||||
verify(storage, times(3)).store(0L, 41L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false);
|
||||
verify(storage).store(0L, 51L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false);
|
||||
verify(storage).store(11L, 51L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false);
|
||||
verify(storage).store(0L, "41", file, BusinessFileCategory.CONTAINER_CLEARANCE_DOCUMENT);
|
||||
verify(storage).store(0L, "51", file, BusinessFileCategory.LOADING_CUSTOMS_DECLARATION);
|
||||
verify(storage).store(0L, "41", file, BusinessFileCategory.CONTAINER_DELIVERY_ORDER);
|
||||
verify(storage).store(0L, "41", file, BusinessFileCategory.CONTAINER_PICKUP_ORDER);
|
||||
verify(storage).store(11L, "51", file, BusinessFileCategory.OVERSEAS_UNLOAD_PHOTO);
|
||||
}
|
||||
|
||||
@Test void oceanBookingDocumentUsesCustomerAndBookingOwnership() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "booking.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "xlsx".getBytes());
|
||||
when(storage.store(10001L, 61L, file, BusinessFileCategory.CUSTOMS_MAX_SIZE, true))
|
||||
when(storage.store(10001L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
|
||||
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/61/booking",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
file.getSize(), "f".repeat(64), "booking.xlsx"));
|
||||
@@ -126,7 +128,7 @@ class InternalBusinessFileServiceTest {
|
||||
|
||||
@Test void overseasRelabelPhotoPersistsExactTaskOwnership() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "relabel.jpg", "image/jpeg", "photo".getBytes());
|
||||
when(storage.store(100L, 81L, file, BusinessFileCategory.STANDARD_MAX_SIZE, false)).thenReturn(new BusinessFileStorage.Stored(
|
||||
when(storage.store(100L, "81", file, BusinessFileCategory.OVERSEAS_RELABEL_PHOTO)).thenReturn(new BusinessFileStorage.Stored(
|
||||
"business-files/100/81/relabel.jpg", "image/jpeg", file.getSize(), "d".repeat(64), "relabel.jpg"));
|
||||
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
|
||||
((FileObjectEntity) invocation.getArgument(0)).setId(91L); return 1;
|
||||
@@ -142,7 +144,7 @@ class InternalBusinessFileServiceTest {
|
||||
|
||||
@Test void loadingCustomsSourceAndResultAreCustomerOwnedArbitraryFormatObjects() {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "资料包.zip", "application/zip", "zip".getBytes());
|
||||
when(storage.store(10001L, 51L, file, BusinessFileCategory.CUSTOMS_MAX_SIZE, true))
|
||||
when(storage.store(eq(10001L), eq("51"), same(file), any(BusinessFileCategory.class)))
|
||||
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/51/key", "application/zip",
|
||||
file.getSize(), "e".repeat(64), "资料包.zip"));
|
||||
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
|
||||
@@ -157,7 +159,8 @@ class InternalBusinessFileServiceTest {
|
||||
InternalBusinessFileService.Metadata::businessId)
|
||||
.containsExactly(10001L, "LOADING_LIST_CUSTOMS_SOURCE", "51");
|
||||
assertThat(result.businessType()).isEqualTo("LOADING_LIST_CUSTOMS_RESULT");
|
||||
verify(storage, times(2)).store(10001L, 51L, file, BusinessFileCategory.CUSTOMS_MAX_SIZE, true);
|
||||
verify(storage).store(10001L, "51", file, BusinessFileCategory.LOADING_LIST_CUSTOMS_SOURCE);
|
||||
verify(storage).store(10001L, "51", file, BusinessFileCategory.LOADING_LIST_CUSTOMS_RESULT);
|
||||
}
|
||||
|
||||
@Test void loadingCustomsCategoriesRejectPlatformOwnerScope() {
|
||||
@@ -177,6 +180,57 @@ class InternalBusinessFileServiceTest {
|
||||
verifyNoInteractions(files, storage);
|
||||
}
|
||||
|
||||
@Test void orderBatchSourceAndErrorReportUseCustomerAndBatchNumberOwnership() {
|
||||
String batchNo = "OBI-0123456789ABCDEF0123";
|
||||
var file = new MockMultipartFile("file", "orders.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
new byte[]{0x50, 0x4b, 0x03, 0x04, 1});
|
||||
when(storage.store(eq(10001L), eq(batchNo), same(file), any(BusinessFileCategory.class)))
|
||||
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/" + batchNo + "/object.xlsx",
|
||||
file.getContentType(), file.getSize(), "9".repeat(64), file.getOriginalFilename()));
|
||||
AtomicLong ids = new AtomicLong(300);
|
||||
when(files.insert(any(FileObjectEntity.class))).thenAnswer(invocation -> {
|
||||
((FileObjectEntity) invocation.getArgument(0)).setId(ids.incrementAndGet());
|
||||
return 1;
|
||||
});
|
||||
|
||||
var source = service.upload(10001L, "ORDER_BATCH_IMPORT_SOURCE", batchNo, 7L, file);
|
||||
var error = service.upload(10001L, "ORDER_BATCH_IMPORT_ERROR_REPORT", batchNo, 7L, file);
|
||||
|
||||
assertThat(source).extracting(InternalBusinessFileService.Metadata::ownerScope,
|
||||
InternalBusinessFileService.Metadata::tenantId, InternalBusinessFileService.Metadata::businessId)
|
||||
.containsExactly("CUSTOMER", 10001L, batchNo);
|
||||
assertThat(error.businessType()).isEqualTo("ORDER_BATCH_IMPORT_ERROR_REPORT");
|
||||
verify(storage).store(10001L, batchNo, file, BusinessFileCategory.ORDER_BATCH_IMPORT_SOURCE);
|
||||
verify(storage).store(10001L, batchNo, file, BusinessFileCategory.ORDER_BATCH_IMPORT_ERROR_REPORT);
|
||||
}
|
||||
|
||||
@Test void orderBatchFilesRejectPlatformOwnerMalformedBatchAndWrongCompositeOwnership() {
|
||||
String batchNo = "OBI-0123456789ABCDEF0123";
|
||||
var file = new MockMultipartFile("file", "orders.xlsx",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
new byte[]{0x50, 0x4b, 0x03, 0x04});
|
||||
assertThatThrownBy(() -> service.upload(0L, "ORDER_BATCH_IMPORT_SOURCE", batchNo, 7L, file))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("明确客户");
|
||||
assertThatThrownBy(() -> service.upload(10001L, "ORDER_BATCH_IMPORT_SOURCE", "41", 7L, file))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("批量导入业务编号");
|
||||
|
||||
FileObjectEntity value = new FileObjectEntity(); value.setId(401L); value.setTenantId(10001L);
|
||||
value.setBusinessType("ORDER_BATCH_IMPORT_SOURCE"); value.setBusinessId(batchNo);
|
||||
value.setOriginalName("orders.xlsx"); value.setObjectKey("business-files/10001/" + batchNo + "/object.xlsx");
|
||||
value.setContentType(file.getContentType()); value.setSha256("8".repeat(64)); value.setSizeBytes(file.getSize());
|
||||
when(files.selectOne(any())).thenReturn(value, null, null, null);
|
||||
|
||||
assertThat(service.metadata(10001L, "ORDER_BATCH_IMPORT_SOURCE", batchNo, 401L).fileId()).isEqualTo(401L);
|
||||
assertThatThrownBy(() -> service.metadata(10002L, "ORDER_BATCH_IMPORT_SOURCE", batchNo, 401L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||
assertThatThrownBy(() -> service.metadata(10001L, "ORDER_BATCH_IMPORT_ERROR_REPORT", batchNo, 401L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||
assertThatThrownBy(() -> service.metadata(10001L, "ORDER_BATCH_IMPORT_SOURCE",
|
||||
"OBI-FFFFFFFFFFFFFFFFFFFF", 401L))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("归属不匹配");
|
||||
}
|
||||
|
||||
@Test void downloadUsesValidatedMetadataObjectKey() {
|
||||
FileObjectEntity value = new FileObjectEntity(); value.setId(71L); value.setTenantId(10001L);
|
||||
value.setBusinessType("CUSTOMS_RECEIPT"); value.setBusinessId("61"); value.setOriginalName("release.txt");
|
||||
@@ -215,7 +269,7 @@ class InternalBusinessFileServiceTest {
|
||||
@Test void concurrentRepeatedUploadsCreateDistinctImmutableObjectsForFirstlegToVersion() throws Exception {
|
||||
MockMultipartFile file = new MockMultipartFile("file", "booking.bin",
|
||||
"application/x-booking", new byte[]{1, 2, 3});
|
||||
when(storage.store(10001L, 61L, file, BusinessFileCategory.CUSTOMS_MAX_SIZE, true))
|
||||
when(storage.store(10001L, "61", file, BusinessFileCategory.OCEAN_BOOKING_DOCUMENT))
|
||||
.thenReturn(new BusinessFileStorage.Stored("business-files/10001/61/object",
|
||||
"application/x-booking", 3, "a".repeat(64), "booking.bin"));
|
||||
AtomicLong ids = new AtomicLong(200);
|
||||
|
||||
@@ -14,8 +14,10 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -59,6 +61,39 @@ class QualificationPackageServiceTest {
|
||||
assertEquals(LEGAL_PERSON_URL, result.legalPersonIdDocumentUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
void legalPersonIdMayBeOmittedWithoutRelaxingTheBusinessLicense() {
|
||||
var fixture = fixture();
|
||||
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
|
||||
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||
new IamQualificationClient.RequiredPackageResponse(31L, null, "PROFILE_REQUIRED", null, null)));
|
||||
|
||||
var result = fixture.service.upload(TENANT_ID, fixture.businessLicense, null,
|
||||
"91310101abcde12345", null, null, false, false, false);
|
||||
|
||||
var captor = ArgumentCaptor.forClass(IamQualificationClient.RequiredPackageRequest.class);
|
||||
verify(fixture.iamClient).submitRequiredPackage(eq("internal-token"), captor.capture());
|
||||
assertNull(captor.getValue().legalPersonIdDocumentUrl());
|
||||
assertNull(captor.getValue().legalPersonIdValidTo());
|
||||
assertNull(result.legalPersonIdQualificationId());
|
||||
assertNull(result.legalPersonIdDocumentUrl());
|
||||
verify(fixture.storage, never()).store(TENANT_ID, fixture.legalPersonId);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingBusinessLicenseAndOrphanedLegalPersonParametersStillFailClosed() {
|
||||
var fixture = fixture();
|
||||
|
||||
assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID, null, null,
|
||||
"91310101ABCDE12345", null, null, false, false, false));
|
||||
assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID, fixture.businessLicense, null,
|
||||
"91310101ABCDE12345", null, LocalDate.of(2030, 1, 1), false, false, false));
|
||||
assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID, fixture.businessLicense, null,
|
||||
"91310101ABCDE12345", null, null, false, true, false));
|
||||
|
||||
verify(fixture.iamClient, never()).submitRequiredPackage(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void secondStorageFailureDeletesTheFirstFileAndNeverCallsIam() {
|
||||
var fixture = fixture();
|
||||
|
||||
Reference in New Issue
Block a user