Test #1
+1
-1
@@ -88,7 +88,7 @@ public class PlatformQualificationFileController {
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable long tenantId,
|
||||
@RequestPart("businessLicense") MultipartFile businessLicense,
|
||||
@RequestPart("legalPersonId") MultipartFile legalPersonId,
|
||||
@RequestPart(value = "legalPersonId", required = false) MultipartFile legalPersonId,
|
||||
@RequestParam String unifiedSocialCreditCode,
|
||||
@RequestParam(required = false) LocalDate businessLicenseValidTo,
|
||||
@RequestParam(required = false) LocalDate legalPersonIdValidTo,
|
||||
|
||||
@@ -79,7 +79,7 @@ public class QualificationFileController {
|
||||
public ApiResponse<QualificationPackageService.PackageUploadResult> uploadRequiredPackage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestPart("businessLicense") MultipartFile businessLicense,
|
||||
@RequestPart("legalPersonId") MultipartFile legalPersonId,
|
||||
@RequestPart(value = "legalPersonId", required = false) MultipartFile legalPersonId,
|
||||
@RequestParam String unifiedSocialCreditCode,
|
||||
@RequestParam(required = false) LocalDate businessLicenseValidTo,
|
||||
@RequestParam(required = false) LocalDate legalPersonIdValidTo,
|
||||
|
||||
@@ -7,29 +7,37 @@ import java.util.Arrays;
|
||||
|
||||
/** File-owned storage policy. Business visibility and lifecycle remain in the calling service. */
|
||||
enum BusinessFileCategory {
|
||||
TRUCK_APPOINTMENT_ATTACHMENT(false, false),
|
||||
OCEAN_BOOKING_DOCUMENT(true, true),
|
||||
CUSTOMS_DOCUMENT(false, false),
|
||||
CUSTOMS_RECEIPT(false, false),
|
||||
FINANCE_VOUCHER(false, false),
|
||||
CONTAINER_CLEARANCE_DOCUMENT(false, false),
|
||||
LOADING_CUSTOMS_DECLARATION(false, false),
|
||||
CONTAINER_DELIVERY_ORDER(false, false),
|
||||
CONTAINER_PICKUP_ORDER(false, false),
|
||||
OVERSEAS_UNLOAD_PHOTO(true, false),
|
||||
OVERSEAS_RELABEL_PHOTO(true, false),
|
||||
LOADING_LIST_CUSTOMS_SOURCE(true, true),
|
||||
LOADING_LIST_CUSTOMS_RESULT(true, true);
|
||||
TRUCK_APPOINTMENT_ATTACHMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
OCEAN_BOOKING_DOCUMENT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG),
|
||||
CUSTOMS_DOCUMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
CUSTOMS_RECEIPT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
FINANCE_VOUCHER(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
CONTAINER_CLEARANCE_DOCUMENT(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
LOADING_CUSTOMS_DECLARATION(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
CONTAINER_DELIVERY_ORDER(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
CONTAINER_PICKUP_ORDER(false, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
OVERSEAS_UNLOAD_PHOTO(true, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
OVERSEAS_RELABEL_PHOTO(true, FormatPolicy.STANDARD, Limits.STANDARD, BusinessIdPolicy.POSITIVE_LONG),
|
||||
LOADING_LIST_CUSTOMS_SOURCE(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG),
|
||||
LOADING_LIST_CUSTOMS_RESULT(true, FormatPolicy.ARBITRARY, Limits.CUSTOMS, BusinessIdPolicy.POSITIVE_LONG),
|
||||
ORDER_BATCH_IMPORT_SOURCE(true, FormatPolicy.XLSX, Limits.ORDER_IMPORT, BusinessIdPolicy.ORDER_BATCH_NO),
|
||||
ORDER_BATCH_IMPORT_ERROR_REPORT(true, FormatPolicy.XLSX, Limits.ORDER_IMPORT, BusinessIdPolicy.ORDER_BATCH_NO);
|
||||
|
||||
static final long STANDARD_MAX_SIZE = 10L * 1024 * 1024;
|
||||
static final long CUSTOMS_MAX_SIZE = 50L * 1024 * 1024;
|
||||
static final long ORDER_IMPORT_MAX_SIZE = 5L * 1024 * 1024;
|
||||
|
||||
private final boolean customerOwned;
|
||||
private final boolean arbitraryFormat;
|
||||
private final FormatPolicy formatPolicy;
|
||||
private final long maxSizeBytes;
|
||||
private final BusinessIdPolicy businessIdPolicy;
|
||||
|
||||
BusinessFileCategory(boolean customerOwned, boolean arbitraryFormat) {
|
||||
BusinessFileCategory(boolean customerOwned, FormatPolicy formatPolicy, long maxSizeBytes,
|
||||
BusinessIdPolicy businessIdPolicy) {
|
||||
this.customerOwned = customerOwned;
|
||||
this.arbitraryFormat = arbitraryFormat;
|
||||
this.formatPolicy = formatPolicy;
|
||||
this.maxSizeBytes = maxSizeBytes;
|
||||
this.businessIdPolicy = businessIdPolicy;
|
||||
}
|
||||
|
||||
static BusinessFileCategory require(String value) {
|
||||
@@ -44,10 +52,35 @@ enum BusinessFileCategory {
|
||||
}
|
||||
|
||||
long maxSizeBytes() {
|
||||
return arbitraryFormat ? CUSTOMS_MAX_SIZE : STANDARD_MAX_SIZE;
|
||||
return maxSizeBytes;
|
||||
}
|
||||
|
||||
boolean arbitraryFormat() {
|
||||
return arbitraryFormat;
|
||||
FormatPolicy formatPolicy() {
|
||||
return formatPolicy;
|
||||
}
|
||||
|
||||
String storageBusinessId(String businessId) {
|
||||
if (businessIdPolicy == BusinessIdPolicy.ORDER_BATCH_NO) {
|
||||
if (businessId == null || !businessId.matches("OBI-[0-9A-F]{20}")) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "批量导入业务编号格式无效");
|
||||
}
|
||||
return businessId;
|
||||
}
|
||||
final long value;
|
||||
try {
|
||||
value = Long.parseLong(businessId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "业务ID格式无效");
|
||||
}
|
||||
if (value <= 0) throw new BizException(ResultCode.BAD_REQUEST, "业务ID必须有效");
|
||||
return Long.toString(value);
|
||||
}
|
||||
|
||||
enum FormatPolicy { STANDARD, ARBITRARY, XLSX }
|
||||
private enum BusinessIdPolicy { POSITIVE_LONG, ORDER_BATCH_NO }
|
||||
private static final class Limits {
|
||||
private static final long STANDARD = 10L * 1024 * 1024;
|
||||
private static final long CUSTOMS = 50L * 1024 * 1024;
|
||||
private static final long ORDER_IMPORT = 5L * 1024 * 1024;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.UUID;
|
||||
|
||||
@Component
|
||||
public class BusinessFileStorage {
|
||||
private static final String XLSX_TYPE =
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
||||
private static final Map<String, String> TYPES = Map.of(
|
||||
"text/plain", ".txt", "application/pdf", ".pdf",
|
||||
"image/jpeg", ".jpg", "image/png", ".png");
|
||||
@@ -36,12 +38,32 @@ public class BusinessFileStorage {
|
||||
}
|
||||
|
||||
Stored store(long tenantId, long businessId, MultipartFile file, long maxSize, boolean arbitraryFormat) {
|
||||
return store(tenantId, Long.toString(businessId), file, maxSize,
|
||||
arbitraryFormat ? BusinessFileCategory.FormatPolicy.ARBITRARY : BusinessFileCategory.FormatPolicy.STANDARD);
|
||||
}
|
||||
|
||||
Stored store(long tenantId, String businessId, MultipartFile file, BusinessFileCategory category) {
|
||||
return store(tenantId, businessId, file, category.maxSizeBytes(), category.formatPolicy());
|
||||
}
|
||||
|
||||
private Stored store(long tenantId, String businessId, MultipartFile file, long maxSize,
|
||||
BusinessFileCategory.FormatPolicy formatPolicy) {
|
||||
if (file == null || file.isEmpty()) throw bad("附件不能为空");
|
||||
if (file.getSize() > maxSize) throw bad("附件不能超过" + maxSize / 1024 / 1024 + "MB");
|
||||
if (businessId == null || !businessId.matches("[A-Za-z0-9-]+")) throw bad("业务ID格式无效");
|
||||
String type = safeContentType(file.getContentType());
|
||||
String extension = arbitraryFormat ? "" : TYPES.get(type);
|
||||
if (!arbitraryFormat && extension == null) throw bad("附件仅支持TXT、PDF、JPG、PNG");
|
||||
if (!arbitraryFormat) verifyContent(file, type);
|
||||
String extension;
|
||||
if (formatPolicy == BusinessFileCategory.FormatPolicy.ARBITRARY) {
|
||||
extension = "";
|
||||
} else if (formatPolicy == BusinessFileCategory.FormatPolicy.XLSX) {
|
||||
if (!XLSX_TYPE.equals(type)) throw bad("附件仅支持XLSX");
|
||||
verifyXlsx(file);
|
||||
extension = ".xlsx";
|
||||
} else {
|
||||
extension = TYPES.get(type);
|
||||
if (extension == null) throw bad("附件仅支持TXT、PDF、JPG、PNG");
|
||||
verifyContent(file, type);
|
||||
}
|
||||
String name = safeName(file);
|
||||
String objectKey = "business-files/" + tenantId + "/" + businessId + "/" + UUID.randomUUID() + extension;
|
||||
Path target = root.resolve(objectKey).normalize();
|
||||
@@ -55,6 +77,18 @@ public class BusinessFileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyXlsx(MultipartFile file) {
|
||||
try (InputStream input = file.getInputStream()) {
|
||||
byte[] prefix = input.readNBytes(4);
|
||||
if (prefix.length < 4 || prefix[0] != 0x50 || prefix[1] != 0x4b
|
||||
|| prefix[2] != 0x03 || prefix[3] != 0x04) {
|
||||
throw bad("上传内容不是有效XLSX");
|
||||
}
|
||||
} catch (IOException exception) {
|
||||
throw bad("附件内容读取失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeContentType(String contentType) {
|
||||
if (contentType == null || contentType.isBlank()) return MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
try {
|
||||
|
||||
@@ -22,8 +22,8 @@ public class InternalBusinessFileService {
|
||||
}
|
||||
BusinessFileCategory category = BusinessFileCategory.require(businessType);
|
||||
category.validateOwner(tenantId);
|
||||
long parsedBusinessId = parseBusinessId(businessId);
|
||||
var stored = storage.store(tenantId, parsedBusinessId, file, category.maxSizeBytes(), category.arbitraryFormat());
|
||||
String storageBusinessId = category.storageBusinessId(businessId);
|
||||
var stored = storage.store(tenantId, storageBusinessId, file, category);
|
||||
FileObjectEntity value = new FileObjectEntity();
|
||||
value.setTenantId(tenantId);
|
||||
value.setBusinessType(businessType);
|
||||
@@ -78,18 +78,7 @@ public class InternalBusinessFileService {
|
||||
}
|
||||
BusinessFileCategory category = BusinessFileCategory.require(businessType);
|
||||
category.validateOwner(tenantId);
|
||||
parseBusinessId(businessId);
|
||||
}
|
||||
|
||||
private static long parseBusinessId(String businessId) {
|
||||
final long value;
|
||||
try {
|
||||
value = Long.parseLong(businessId);
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "业务ID格式无效");
|
||||
}
|
||||
if (value <= 0) throw new BizException(ResultCode.BAD_REQUEST, "业务ID必须有效");
|
||||
return value;
|
||||
category.storageBusinessId(businessId);
|
||||
}
|
||||
|
||||
private static Metadata metadata(FileObjectEntity value) {
|
||||
|
||||
@@ -28,18 +28,25 @@ public class QualificationPackageService {
|
||||
if (!normalizedCreditCode.matches("[0-9A-Z]{18}")) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "请输入有效的18位统一社会信用代码");
|
||||
}
|
||||
if (businessLicense == null || businessLicense.isEmpty()) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "营业执照文件不能为空");
|
||||
}
|
||||
if (legalPersonId == null && (legalPersonIdValidTo != null || replaceLegalPersonId)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "未上传法人身份证时不能填写其有效期或替换标记");
|
||||
}
|
||||
LocalQualificationStorage.StoredFile storedBusinessLicense = storage.store(tenantId, businessLicense);
|
||||
LocalQualificationStorage.StoredFile storedLegalPersonId;
|
||||
LocalQualificationStorage.StoredFile storedLegalPersonId = null;
|
||||
try {
|
||||
storedLegalPersonId = storage.store(tenantId, legalPersonId);
|
||||
if (legalPersonId != null) storedLegalPersonId = storage.store(tenantId, legalPersonId);
|
||||
} catch (RuntimeException exception) {
|
||||
storage.delete(storedBusinessLicense);
|
||||
throw exception;
|
||||
}
|
||||
String legalPersonIdDocumentUrl = storedLegalPersonId == null ? null : storedLegalPersonId.documentUrl();
|
||||
|
||||
var request = new IamQualificationClient.RequiredPackageRequest(
|
||||
tenantId, normalizedCreditCode, storedBusinessLicense.documentUrl(), businessLicenseValidTo,
|
||||
replaceBusinessLicense, storedLegalPersonId.documentUrl(), legalPersonIdValidTo, replaceLegalPersonId);
|
||||
replaceBusinessLicense, legalPersonIdDocumentUrl, legalPersonIdValidTo, replaceLegalPersonId);
|
||||
try {
|
||||
var response = platform
|
||||
? iamClient.submitPlatformRequiredPackage(internalToken, request)
|
||||
@@ -50,7 +57,7 @@ public class QualificationPackageService {
|
||||
String message = response.message() == null || response.message().isBlank()
|
||||
? code.message() : response.message();
|
||||
storage.delete(storedBusinessLicense);
|
||||
storage.delete(storedLegalPersonId);
|
||||
deleteIfStored(storedLegalPersonId);
|
||||
throw new BizException(code, message);
|
||||
}
|
||||
if (response.data() == null) {
|
||||
@@ -58,20 +65,24 @@ public class QualificationPackageService {
|
||||
}
|
||||
var data = response.data();
|
||||
deleteReplacedFile(tenantId, data.replacedBusinessLicenseDocumentUrl(), storedBusinessLicense.documentUrl());
|
||||
deleteReplacedFile(tenantId, data.replacedLegalPersonIdDocumentUrl(), storedLegalPersonId.documentUrl());
|
||||
deleteReplacedFile(tenantId, data.replacedLegalPersonIdDocumentUrl(), legalPersonIdDocumentUrl);
|
||||
return new PackageUploadResult(data.businessLicenseQualificationId(), data.legalPersonIdQualificationId(),
|
||||
data.tenantStatus(), storedBusinessLicense.documentUrl(), storedLegalPersonId.documentUrl());
|
||||
data.tenantStatus(), storedBusinessLicense.documentUrl(), legalPersonIdDocumentUrl);
|
||||
} catch (BizException exception) {
|
||||
throw exception;
|
||||
} catch (RuntimeException exception) {
|
||||
LOGGER.error("IAM 必填资质包登记结果未知,保留已写入文件等待重试或对账,tenantId={}, businessLicenseUrl={}, legalPersonIdUrl={}",
|
||||
tenantId, storedBusinessLicense.documentUrl(), storedLegalPersonId.documentUrl(), exception);
|
||||
tenantId, storedBusinessLicense.documentUrl(), legalPersonIdDocumentUrl, exception);
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM qualification service is unavailable");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteReplacedFile(long tenantId, String replacedUrl, String currentUrl) {
|
||||
if (!currentUrl.equals(replacedUrl)) storage.deleteByDocumentUrl(tenantId, replacedUrl);
|
||||
if (currentUrl != null && !currentUrl.equals(replacedUrl)) storage.deleteByDocumentUrl(tenantId, replacedUrl);
|
||||
}
|
||||
|
||||
private void deleteIfStored(LocalQualificationStorage.StoredFile storedFile) {
|
||||
if (storedFile != null) storage.delete(storedFile);
|
||||
}
|
||||
|
||||
public record PackageUploadResult(Long businessLicenseQualificationId,
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Order Batch Import File Internal API
|
||||
version: 1.0.0
|
||||
description: >-
|
||||
Order批量导入源文件与错误报告均为客户所有的不可变XLSX对象;businessId固定为Order生成的batchNo。
|
||||
File只保存并校验对象归属,不复制导入任务状态。
|
||||
x-lifecycle-owner: ORDER
|
||||
x-file-object-semantics: IMMUTABLE_CREATE_ONLY
|
||||
paths:
|
||||
/internal/file/v1/business-files/{tenantId}/{businessType}/{batchNo}:
|
||||
post:
|
||||
summary: Order上传批量导入源文件或错误报告
|
||||
security: [{internalServiceToken: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/OperatorId'}
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
- {$ref: '#/components/parameters/BusinessType'}
|
||||
- {$ref: '#/components/parameters/BatchNo'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [file]
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
description: 有效XLSX文件,最大5MiB
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Metadata'}
|
||||
/internal/file/v1/business-files/{tenantId}/{businessType}/{batchNo}/{fileId}:
|
||||
get:
|
||||
summary: Order精确校验批量导入文件元数据
|
||||
security: [{internalServiceToken: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
- {$ref: '#/components/parameters/BusinessType'}
|
||||
- {$ref: '#/components/parameters/BatchNo'}
|
||||
- {$ref: '#/components/parameters/FileId'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Metadata'}
|
||||
/internal/file/v1/business-files/{tenantId}/{businessType}/{batchNo}/{fileId}/content:
|
||||
get:
|
||||
summary: Order下载批量导入源文件或错误报告
|
||||
security: [{internalServiceToken: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
- {$ref: '#/components/parameters/BusinessType'}
|
||||
- {$ref: '#/components/parameters/BatchNo'}
|
||||
- {$ref: '#/components/parameters/FileId'}
|
||||
responses:
|
||||
'200':
|
||||
description: 精确归属的XLSX附件;错误客户、类别、batchNo或fileId不得命中
|
||||
headers:
|
||||
Cache-Control: {schema: {type: string, enum: [no-store]}}
|
||||
Content-Disposition: {schema: {type: string}}
|
||||
content:
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
|
||||
schema: {type: string, format: binary}
|
||||
components:
|
||||
securitySchemes:
|
||||
internalServiceToken: {type: apiKey, in: header, name: X-Internal-Service-Token}
|
||||
parameters:
|
||||
OperatorId:
|
||||
name: X-Operator-Id
|
||||
in: header
|
||||
required: true
|
||||
schema: {$ref: '#/components/schemas/Id'}
|
||||
TenantId:
|
||||
name: tenantId
|
||||
in: path
|
||||
required: true
|
||||
description: 当前客户ID,必须大于0
|
||||
schema: {$ref: '#/components/schemas/Id'}
|
||||
BusinessType:
|
||||
name: businessType
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, enum: [ORDER_BATCH_IMPORT_SOURCE, ORDER_BATCH_IMPORT_ERROR_REPORT]}
|
||||
BatchNo:
|
||||
name: batchNo
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string, pattern: '^OBI-[0-9A-F]{20}$'}
|
||||
FileId:
|
||||
name: fileId
|
||||
in: path
|
||||
required: true
|
||||
schema: {$ref: '#/components/schemas/Id'}
|
||||
responses:
|
||||
Metadata:
|
||||
description: 客户批量导入文件元数据
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [code, data]
|
||||
properties:
|
||||
code: {type: string, enum: ['200']}
|
||||
data: {$ref: '#/components/schemas/Metadata'}
|
||||
schemas:
|
||||
Id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
Metadata:
|
||||
type: object
|
||||
required: [fileId, ownerScope, tenantId, businessType, businessId, fileName, sha256, contentType, sizeBytes]
|
||||
properties:
|
||||
fileId: {$ref: '#/components/schemas/Id'}
|
||||
ownerScope: {type: string, enum: [CUSTOMER]}
|
||||
tenantId: {$ref: '#/components/schemas/Id'}
|
||||
businessType: {type: string, enum: [ORDER_BATCH_IMPORT_SOURCE, ORDER_BATCH_IMPORT_ERROR_REPORT]}
|
||||
businessId: {type: string, pattern: '^OBI-[0-9A-F]{20}$'}
|
||||
fileName: {type: string}
|
||||
sha256: {type: string, pattern: '^[0-9a-f]{64}$'}
|
||||
contentType: {type: string, enum: [application/vnd.openxmlformats-officedocument.spreadsheetml.sheet]}
|
||||
sizeBytes: {type: integer, format: int64, minimum: 1, maximum: 5242880}
|
||||
@@ -0,0 +1,113 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Qualification Required Package API
|
||||
version: 1.0.0
|
||||
description: 营业执照为必填资质;法人身份证为选填资质,缺省时不创建、不替换法人身份证记录。
|
||||
paths:
|
||||
/api/merchant/v1/files/qualifications/required-package:
|
||||
post:
|
||||
summary: 客户上传必需资质包
|
||||
description: 需要 merchant:qualification:upload;营业执照必填,法人身份证选填。
|
||||
security: [{bearerAuth: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/UnifiedSocialCreditCode'}
|
||||
- {$ref: '#/components/parameters/BusinessLicenseValidTo'}
|
||||
- {$ref: '#/components/parameters/LegalPersonIdValidTo'}
|
||||
- {$ref: '#/components/parameters/ReplaceBusinessLicense'}
|
||||
- {$ref: '#/components/parameters/ReplaceLegalPersonId'}
|
||||
requestBody: {$ref: '#/components/requestBodies/RequiredPackage'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/PackageResult'}
|
||||
/api/platform/v1/files/qualifications/{tenantId}/required-package:
|
||||
post:
|
||||
summary: 平台为客户上传必需资质包
|
||||
description: 需要 platform:customer:create 或 platform:customer:edit;营业执照必填,法人身份证选填。
|
||||
security: [{bearerAuth: []}]
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
- {$ref: '#/components/parameters/UnifiedSocialCreditCode'}
|
||||
- {$ref: '#/components/parameters/BusinessLicenseValidTo'}
|
||||
- {$ref: '#/components/parameters/LegalPersonIdValidTo'}
|
||||
- {$ref: '#/components/parameters/ReplaceBusinessLicense'}
|
||||
- {$ref: '#/components/parameters/ReplaceLegalPersonId'}
|
||||
requestBody: {$ref: '#/components/requestBodies/RequiredPackage'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/PackageResult'}
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||
parameters:
|
||||
TenantId:
|
||||
name: tenantId
|
||||
in: path
|
||||
required: true
|
||||
schema: {$ref: '#/components/schemas/Id'}
|
||||
UnifiedSocialCreditCode:
|
||||
name: unifiedSocialCreditCode
|
||||
in: query
|
||||
required: true
|
||||
schema: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
|
||||
BusinessLicenseValidTo:
|
||||
name: businessLicenseValidTo
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: string, format: date}
|
||||
LegalPersonIdValidTo:
|
||||
name: legalPersonIdValidTo
|
||||
in: query
|
||||
required: false
|
||||
description: 仅上传legalPersonId时允许填写
|
||||
schema: {type: string, format: date}
|
||||
ReplaceBusinessLicense:
|
||||
name: replaceBusinessLicense
|
||||
in: query
|
||||
required: false
|
||||
schema: {type: boolean, default: false}
|
||||
ReplaceLegalPersonId:
|
||||
name: replaceLegalPersonId
|
||||
in: query
|
||||
required: false
|
||||
description: 未上传legalPersonId时必须为false
|
||||
schema: {type: boolean, default: false}
|
||||
requestBodies:
|
||||
RequiredPackage:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required: [businessLicense]
|
||||
properties:
|
||||
businessLicense:
|
||||
type: string
|
||||
format: binary
|
||||
description: 营业执照,必填;PDF/JPG/PNG,最大5MB
|
||||
legalPersonId:
|
||||
type: string
|
||||
format: binary
|
||||
nullable: true
|
||||
description: 法人身份证,选填;PDF/JPG/PNG,最大5MB
|
||||
responses:
|
||||
PackageResult:
|
||||
description: 资质包上传结果
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [code, data]
|
||||
properties:
|
||||
code: {type: string, enum: ['200']}
|
||||
data: {$ref: '#/components/schemas/PackageUploadResult'}
|
||||
schemas:
|
||||
Id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
PackageUploadResult:
|
||||
type: object
|
||||
required: [businessLicenseQualificationId, tenantStatus, businessLicenseDocumentUrl]
|
||||
properties:
|
||||
businessLicenseQualificationId: {$ref: '#/components/schemas/Id'}
|
||||
legalPersonIdQualificationId:
|
||||
allOf: [{$ref: '#/components/schemas/Id'}]
|
||||
nullable: true
|
||||
tenantStatus: {type: string}
|
||||
businessLicenseDocumentUrl: {type: string, minLength: 1}
|
||||
legalPersonIdDocumentUrl: {type: string, nullable: true}
|
||||
+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