This commit is contained in:
2026-08-05 09:11:26 +08:00
parent d544bbd0c5
commit 3791fee285
45 changed files with 1187 additions and 126 deletions
@@ -10,6 +10,8 @@ import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageResponse;
import com.baiyue.iam.service.MerchantOnboardingService;
import com.baiyue.iam.service.ProfileResourceService;
import com.baiyue.iam.service.TenantService;
@@ -73,6 +75,22 @@ public class InternalTenantController {
return ApiResponse.success(profileResourceService.registerPlatformQualification(request));
}
@PostMapping("/required-qualification-packages")
public ApiResponse<RequiredQualificationPackageResponse> submitRequiredQualificationPackage(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody RequiredQualificationPackageRequest request) {
verify(token);
return ApiResponse.success(profileResourceService.submitRequiredQualificationPackage(request));
}
@PostMapping("/platform-required-qualification-packages")
public ApiResponse<RequiredQualificationPackageResponse> registerPlatformRequiredQualificationPackage(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody RequiredQualificationPackageRequest request) {
verify(token);
return ApiResponse.success(profileResourceService.registerPlatformRequiredQualificationPackage(request));
}
@GetMapping("/tenants/{tenantId}/order-eligibility")
public ApiResponse<OrderEligibilityResponse> orderEligibility(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@@ -50,5 +50,24 @@ public final class OnboardingDtos {
String replacedDocumentUrl
) { }
public record RequiredQualificationPackageRequest(
@NotNull Long tenantId,
@NotBlank @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
@NotBlank String businessLicenseDocumentUrl,
LocalDate businessLicenseValidTo,
boolean replaceBusinessLicense,
@NotBlank String legalPersonIdDocumentUrl,
LocalDate legalPersonIdValidTo,
boolean replaceLegalPersonId
) { }
public record RequiredQualificationPackageResponse(
Long businessLicenseQualificationId,
Long legalPersonIdQualificationId,
String tenantStatus,
String replacedBusinessLicenseDocumentUrl,
String replacedLegalPersonIdDocumentUrl
) { }
public record OrderEligibilityResponse(boolean eligible, String tenantStatus, String reason) { }
}
@@ -26,7 +26,6 @@ public final class TenantDtos {
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
@Schema(description = "Optional while creating a draft; required before review / 创建草稿时选填,送审前必填", example = "91440300MA5EXAMPLE", nullable = true) @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
@@ -7,6 +7,8 @@ import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageResponse;
import com.baiyue.iam.dto.ProfileDtos.AddressRequest;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.entity.IamTenantAddress;
@@ -65,6 +67,7 @@ public class ProfileResourceService {
throw new BizException(ResultCode.CONFLICT,
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
}
requireOptionalQualificationType(request.qualificationType());
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
.filter(item -> request.qualificationType().equals(item.getQualificationType())
@@ -86,11 +89,6 @@ public class ProfileResourceService {
request.qualificationType(), certificateNo, request.documentUrl());
qualification.setValidTo(request.validTo());
repository.saveQualification(qualification);
if ("BUSINESS_LICENSE".equals(request.qualificationType())
&& !certificateNo.equals(tenant.getUnifiedSocialCreditCode())) {
tenant.setUnifiedSocialCreditCode(certificateNo);
tenantRepository.saveTenant(tenant);
}
return new QualificationSubmissionResponse(qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
}
@@ -105,6 +103,7 @@ public class ProfileResourceService {
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
}
requireOptionalQualificationType(request.qualificationType());
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
.filter(item -> request.qualificationType().equals(item.getQualificationType())
@@ -126,15 +125,93 @@ public class ProfileResourceService {
certificateNo, request.documentUrl());
qualification.setValidTo(request.validTo());
repository.saveQualification(qualification);
if ("BUSINESS_LICENSE".equals(request.qualificationType())
&& !certificateNo.equals(tenant.getUnifiedSocialCreditCode())) {
tenant.setUnifiedSocialCreditCode(certificateNo);
tenantRepository.saveTenant(tenant);
}
return new PlatformQualificationRegistrationResponse(
qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
}
@Transactional
@BusinessAudit(event = "TENANT_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD", resourceType = "TENANT_QUALIFICATION",
resourceId = "#request.tenantId", description = "上传企业必填资质包", captureAfter = false)
public RequiredQualificationPackageResponse submitRequiredQualificationPackage(
RequiredQualificationPackageRequest request) {
return registerRequiredQualificationPackage(request, "商户在送审前替换文件");
}
@Transactional
@BusinessAudit(event = "PLATFORM_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD", resourceType = "TENANT_QUALIFICATION",
resourceId = "#request.tenantId", description = "平台代客户上传必填资质包", captureAfter = false)
public RequiredQualificationPackageResponse registerPlatformRequiredQualificationPackage(
RequiredQualificationPackageRequest request) {
return registerRequiredQualificationPackage(request, "平台在送审前替换文件");
}
private RequiredQualificationPackageResponse registerRequiredQualificationPackage(
RequiredQualificationPackageRequest request, String replacementComment) {
IamTenant tenant = tenantRepository.findTenantForUpdate(request.tenantId());
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
}
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
throw new BizException(ResultCode.CONFLICT,
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
}
String creditCode = resolveCertificateNo(tenant, "BUSINESS_LICENSE", request.unifiedSocialCreditCode());
List<IamTenantQualification> locked = repository.findQualificationsForUpdate(tenant.getId());
List<IamTenantQualification> businessLicenses = currentQualifications(locked, "BUSINESS_LICENSE");
List<IamTenantQualification> legalPersonIds = currentQualifications(locked, "LEGAL_PERSON_ID");
requireReplaceAllowed(businessLicenses, request.replaceBusinessLicense(), "营业执照");
requireReplaceAllowed(legalPersonIds, request.replaceLegalPersonId(), "法人身份证");
String replacedBusinessLicenseUrl = supersede(businessLicenses, replacementComment);
String replacedLegalPersonIdUrl = supersede(legalPersonIds, replacementComment);
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
creditCode, request.businessLicenseDocumentUrl());
businessLicense.setValidTo(request.businessLicenseValidTo());
repository.saveQualification(businessLicense);
IamTenantQualification legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
null, request.legalPersonIdDocumentUrl());
legalPersonId.setValidTo(request.legalPersonIdValidTo());
repository.saveQualification(legalPersonId);
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
tenant.setUnifiedSocialCreditCode(creditCode);
tenantRepository.saveTenant(tenant);
}
return new RequiredQualificationPackageResponse(
businessLicense.getId(), legalPersonId.getId(), tenant.getStatus(),
replacedBusinessLicenseUrl, replacedLegalPersonIdUrl);
}
private List<IamTenantQualification> currentQualifications(
List<IamTenantQualification> qualifications, String type) {
return qualifications.stream()
.filter(item -> type.equals(item.getQualificationType()) && isCurrentQualification(item))
.toList();
}
private void requireReplaceAllowed(List<IamTenantQualification> current, boolean replace, String label) {
if (!current.isEmpty() && !replace) {
throw new BizException(ResultCode.CONFLICT, label + "已上传,请明确选择替换");
}
}
private String supersede(List<IamTenantQualification> current, String comment) {
String replacedUrl = null;
for (IamTenantQualification existing : current) {
if (replacedUrl == null) replacedUrl = existing.getDocumentUrl();
existing.setStatus("SUPERSEDED");
existing.setReviewComment(comment);
repository.saveQualification(existing);
}
return replacedUrl;
}
private void requireOptionalQualificationType(String type) {
if ("BUSINESS_LICENSE".equals(type) || "LEGAL_PERSON_ID".equals(type)) {
throw new BizException(ResultCode.BAD_REQUEST,
"Required qualifications must be uploaded as one package / 营业执照、统一社会信用代码和法人身份证必须整包上传");
}
}
private String resolveCertificateNo(IamTenant tenant, String qualificationType, String submittedCertificateNo) {
if (!"BUSINESS_LICENSE".equals(qualificationType)) {
return submittedCertificateNo;
@@ -24,7 +24,6 @@ import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportRow;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.List;
@@ -54,11 +53,6 @@ public class TenantService {
)
@Transactional
public TenantResponse register(RegisterRequest request, long operatorId) {
String creditCode = normalizeCreditCode(request.unifiedSocialCreditCode());
if (creditCode != null && repository.existsByCreditCode(creditCode)) {
throw new BizException(ResultCode.CONFLICT, "Enterprise already exists / 企业已存在");
}
IamTenant tenant = new IamTenant();
tenant.setId(IdWorker.getId());
tenant.setTenantCode("TEN" + tenant.getId());
@@ -67,7 +61,7 @@ public class TenantService {
tenant.setStatus("PROFILE_REQUIRED");
tenant.setLegalName(request.legalName());
tenant.setLegalRepresentative(request.legalRepresentative());
tenant.setUnifiedSocialCreditCode(creditCode);
tenant.setUnifiedSocialCreditCode(null);
tenant.setContactName(request.contactName());
tenant.setContactPhone(request.contactPhone());
tenant.setContactEmail(request.contactEmail());
@@ -334,10 +328,6 @@ public class TenantService {
return tenant;
}
private String normalizeCreditCode(String creditCode) {
return StringUtils.hasText(creditCode) ? creditCode.trim().toUpperCase() : null;
}
private TenantResponse response(IamTenant tenant) {
return new TenantResponse(
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
+72 -1
View File
@@ -608,6 +608,46 @@ paths:
'400': {$ref: '#/components/responses/BadRequest'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
/internal/iam/required-qualification-packages:
post:
tags: [IAM Resources / IAM 资源]
summary: 商户必填资质包登记
description: 仅供 File Service 调用;在一个 IAM 事务中登记营业执照、统一社会信用代码和法人身份证。
security: []
parameters:
- {$ref: '#/components/parameters/InternalServiceToken'}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RequiredQualificationPackageRequest'}
responses:
'200':
description: 统一响应;仅 data 非空且 code 为 200 表示资质包已完整登记
content:
application/json:
schema: {$ref: '#/components/schemas/RequiredQualificationPackageApiResponse'}
'403': {$ref: '#/components/responses/Forbidden'}
/internal/iam/platform-required-qualification-packages:
post:
tags: [IAM Resources / IAM 资源]
summary: 平台代客户必填资质包登记
description: 仅供 File Service 调用;与商户入口使用相同的三项事务保存规则。
security: []
parameters:
- {$ref: '#/components/parameters/InternalServiceToken'}
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/RequiredQualificationPackageRequest'}
responses:
'200':
description: 统一响应;仅 data 非空且 code 为 200 表示资质包已完整登记
content:
application/json:
schema: {$ref: '#/components/schemas/RequiredQualificationPackageApiResponse'}
'403': {$ref: '#/components/responses/Forbidden'}
/internal/exports/v1/CUSTOMER_LIST/batches:
post:
tags: [Internal Export Provider]
@@ -650,6 +690,11 @@ components:
scheme: bearer
bearerFormat: JWT
parameters:
InternalServiceToken:
name: X-Internal-Service-Token
in: header
required: true
schema: {type: string}
Id:
name: id
in: path
@@ -838,6 +883,33 @@ components:
validTo: {type: string, format: date, nullable: true}
status: {type: string, enum: [PENDING, APPROVED, REJECTED, SUPERSEDED]}
reviewComment: {type: string}
RequiredQualificationPackageRequest:
type: object
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, replaceBusinessLicense, legalPersonIdDocumentUrl, replaceLegalPersonId]
properties:
tenantId: {type: string, pattern: '^\d+$'}
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
businessLicenseDocumentUrl: {type: string, minLength: 1}
businessLicenseValidTo: {type: string, format: date, nullable: true}
replaceBusinessLicense: {type: boolean}
legalPersonIdDocumentUrl: {type: string, minLength: 1}
legalPersonIdValidTo: {type: string, format: date, nullable: true}
replaceLegalPersonId: {type: boolean}
RequiredQualificationPackageResponse:
type: object
required: [businessLicenseQualificationId, legalPersonIdQualificationId, tenantStatus]
properties:
businessLicenseQualificationId: {type: string, pattern: '^\d+$'}
legalPersonIdQualificationId: {type: string, pattern: '^\d+$'}
tenantStatus: {type: string, enum: [PROFILE_REQUIRED, REJECTED]}
replacedBusinessLicenseDocumentUrl: {type: string, nullable: true}
replacedLegalPersonIdDocumentUrl: {type: string, nullable: true}
RequiredQualificationPackageApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data: {$ref: '#/components/schemas/RequiredQualificationPackageResponse'}
RegisterRequest:
type: object
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone, servicePackage]
@@ -845,7 +917,6 @@ components:
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
legalRepresentative: {type: string, maxLength: 64, example: 张三}
unifiedSocialCreditCode: {type: string, nullable: true, pattern: '^[0-9A-Za-z]{18}$', example: 91440300MA5ABCDE12, description: 创建草稿时选填,送审前必填}
contactName: {type: string, example: 李四}
contactPhone: {type: string, example: '13800138000'}
contactEmail: {type: string, format: email, example: contact@example.com}
@@ -38,4 +38,28 @@ class QualificationUploadContractTest {
assertTrue(yaml.contains(" /api/merchant/v1/iam/tenant/qualifications/submit-review:"));
}
}
@Test
void internalControllerExposesBothRequiredPackageRoutes() {
var routes = java.util.Arrays.stream(InternalTenantController.class.getDeclaredMethods())
.map(method -> method.getAnnotation(PostMapping.class))
.filter(java.util.Objects::nonNull)
.flatMap(mapping -> java.util.Arrays.stream(mapping.value()))
.toList();
assertTrue(routes.contains("/required-qualification-packages"));
assertTrue(routes.contains("/platform-required-qualification-packages"));
}
@Test
void staticOpenApiDocumentsTheRequiredPackageContract() throws IOException {
try (var input = getClass().getClassLoader().getResourceAsStream("openapi/iam-v1.yaml")) {
assertNotNull(input);
String yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
assertTrue(yaml.contains("/internal/iam/required-qualification-packages:"));
assertTrue(yaml.contains("/internal/iam/platform-required-qualification-packages:"));
assertTrue(yaml.contains("RequiredQualificationPackageRequest:"));
assertTrue(yaml.contains("replaceBusinessLicense"));
assertTrue(yaml.contains("replaceLegalPersonId"));
}
}
}
@@ -1,6 +1,7 @@
package com.baiyue.iam.service;
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
import com.baiyue.iam.entity.IamTenant;
import com.baiyue.iam.repository.ProfileResourceRepository;
import com.baiyue.iam.repository.TenantRepository;
@@ -18,12 +19,13 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;
import com.baiyue.common.core.exception.BizException;
class MerchantQualificationUploadTest {
@Test
void uploadsOneDraftQualificationWithoutCreatingAReview() {
void uploadsOneOptionalDraftQualificationWithoutCreatingAReview() {
TenantRepository tenantRepository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
ProfileResourceService service = new ProfileResourceService();
@@ -37,8 +39,8 @@ class MerchantQualificationUploadTest {
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
var response = service.submitQualification(new QualificationSubmissionRequest(
10001L, "LEGAL_PERSON_ID", null,
"/api/merchant/v1/files/qualifications/legal-person.png", LocalDate.of(2030, 1, 1), false));
10001L, "FREIGHT_FORWARDER_CERTIFICATE", null,
"/api/merchant/v1/files/qualifications/freight-forwarder.png", LocalDate.of(2030, 1, 1), false));
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
assertNull(response.reviewId());
@@ -47,7 +49,26 @@ class MerchantQualificationUploadTest {
}
@Test
void businessLicenseRequiresAndPersistsAUniqueUnifiedSocialCreditCode() {
void requiredQualificationsRejectTheLegacySingleFilePath() {
TenantRepository tenantRepository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
ProfileResourceService service = new ProfileResourceService();
ReflectionTestUtils.setField(service, "tenantRepository", tenantRepository);
ReflectionTestUtils.setField(service, "repository", profileRepository);
IamTenant tenant = new IamTenant();
tenant.setId(10001L);
tenant.setTenantType("MERCHANT");
tenant.setStatus("PROFILE_REQUIRED");
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
10001L, "BUSINESS_LICENSE", "91310101abcde12345",
"/api/merchant/v1/files/qualifications/license.png", null, false)));
verify(profileRepository, never()).insertQualification(any());
}
@Test
void requiredPackagePersistsBothFilesAndCreditCodeTogether() {
TenantRepository tenantRepository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
ProfileResourceService service = new ProfileResourceService();
@@ -60,21 +81,19 @@ class MerchantQualificationUploadTest {
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
10001L, "BUSINESS_LICENSE", null,
"/api/merchant/v1/files/qualifications/license.png", null, false)));
var response = service.submitQualification(new QualificationSubmissionRequest(
10001L, "BUSINESS_LICENSE", "91310101abcde12345",
"/api/merchant/v1/files/qualifications/license.png", null, false));
var response = service.submitRequiredQualificationPackage(new RequiredQualificationPackageRequest(
10001L, "91310101abcde12345",
"/api/merchant/v1/files/qualifications/license.png", null, false,
"/api/merchant/v1/files/qualifications/legal-person.png", null, false));
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
assertEquals("91310101ABCDE12345", tenant.getUnifiedSocialCreditCode());
verify(profileRepository, times(2)).insertQualification(any());
verify(tenantRepository).saveTenant(tenant);
}
@Test
void businessLicenseCannotOverwriteAnEstablishedCreditCode() {
void requiredPackageCannotOverwriteAnEstablishedCreditCode() {
TenantRepository tenantRepository = mock(TenantRepository.class);
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
ProfileResourceService service = new ProfileResourceService();
@@ -87,11 +106,11 @@ class MerchantQualificationUploadTest {
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
10001L, "BUSINESS_LICENSE", "91310101ABCDE12345",
"/api/merchant/v1/files/qualifications/license.png", null, true)));
assertThrows(BizException.class, () -> service.submitRequiredQualificationPackage(
new RequiredQualificationPackageRequest(10001L, "91310101ABCDE12345",
"/api/merchant/v1/files/qualifications/license.png", null, true,
"/api/merchant/v1/files/qualifications/legal-person.png", null, true)));
verify(profileRepository, never()).insertQualification(any());
verify(tenantRepository, never()).saveTenant(tenant);
}
}
@@ -21,7 +21,7 @@ import static org.mockito.Mockito.when;
class PlatformQualificationRegistrationTest {
@Test
void platformUploadRegistersMetadataWithoutCreatingReviewTicket() {
void platformUploadRegistersOptionalMetadataWithoutCreatingReviewTicket() {
ProfileResourceRepository repository = mock(ProfileResourceRepository.class);
TenantRepository tenantRepository = mock(TenantRepository.class);
ProfileResourceService service = new ProfileResourceService();
@@ -33,13 +33,13 @@ class PlatformQualificationRegistrationTest {
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
10001L, "LEGAL_PERSON_ID", null, "/api/merchant/v1/files/qualifications/file.pdf",
10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/file.pdf",
LocalDate.of(2030, 12, 31), false));
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
verify(repository).insertQualification(captor.capture());
verify(repository).saveQualification(captor.getValue());
assertEquals("LEGAL_PERSON_ID", captor.getValue().getQualificationType());
assertEquals("BANK_ACCOUNT_LICENSE", captor.getValue().getQualificationType());
assertEquals(LocalDate.of(2030, 12, 31), captor.getValue().getValidTo());
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
assertNull(response.reviewId());
@@ -56,14 +56,14 @@ class PlatformQualificationRegistrationTest {
IamTenant tenant = new IamTenant();
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("PROFILE_REQUIRED");
IamTenantQualification existing = new IamTenantQualification();
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("LEGAL_PERSON_ID");
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BANK_ACCOUNT_LICENSE");
existing.setStatus("PENDING");
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf");
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
10001L, "LEGAL_PERSON_ID", null, "/api/merchant/v1/files/qualifications/new-file.pdf",
10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/new-file.pdf",
LocalDate.of(2031, 12, 31), true));
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
@@ -120,7 +120,7 @@ class TenantServiceTest {
ReflectionTestUtils.setField(service, "repository", repository);
var created = service.register(new RegisterRequest(
"Example Merchant", "Example Merchant Ltd", "Bob", "91330100EXAMPLE001", "Alice", "13800138000",
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
"alice@example.com", "基础版", "alice_wechat", "Sales Zhang", 30, "LIMITED_COMPANY",
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
9001L);
@@ -134,13 +134,13 @@ class TenantServiceTest {
}
@Test
void platformRegistrationAllowsCreditCodeToBeCompletedWithQualificationsLater() {
void platformRegistrationAlwaysCreatesTheDraftWithoutACreditCode() {
TenantRepository repository = mock(TenantRepository.class);
TenantService service = new TenantService();
ReflectionTestUtils.setField(service, "repository", repository);
var created = service.register(new RegisterRequest(
"Example Merchant", "Example Merchant Ltd", "Bob", null, "Alice", "13800138000",
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
"alice@example.com", "基础版", null, null, 30, null,
null, null, "CNY", null, null), 9001L);