test(admin): cover optional legal-person qualification

This commit is contained in:
2026-09-02 10:12:52 +08:00
parent 4cdaa1d1ad
commit 132d0c22d7
3 changed files with 206 additions and 17 deletions
@@ -117,6 +117,9 @@ describe('CustomerDetailView', () => {
expect(document.querySelector('.overview-strip')).toBeNull()
expect(document.body.textContent).toContain('merchant_admin')
expect(document.body.textContent).toContain('企业管理员')
expect(document.body.textContent).toContain('公司联系电话')
expect(document.body.textContent).toContain('登录手机号')
expect(document.body.textContent).toContain('修改会影响登录')
expect(listPlatformTenantMembers).toHaveBeenCalledWith(tenant.id)
expect(document.body.textContent).toContain('已替换')
+182 -16
View File
@@ -1,4 +1,4 @@
import ElementPlus from 'element-plus'
import ElementPlus, { ElMessageBox } from 'element-plus'
import { createPinia, setActivePinia } from 'pinia'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
approveEnterpriseReview,
archivePlatformTenant,
createPlatformTenant,
downloadTenantQualification,
createPlatformTenantImpersonation,
getPlatformTenant,
@@ -15,6 +16,7 @@ import {
listTenantQualifications,
rejectEnterpriseReview,
resetPlatformTenantOwnerPassword,
submitPlatformTenantReview,
updatePlatformTenant,
uploadPlatformTenantQualification,
uploadPlatformTenantRequiredQualificationPackage,
@@ -104,6 +106,12 @@ function tableButtonByText(text: string) {
.find((button) => button.textContent?.trim() === text)
}
function selectQualificationFile(index: number, file: File) {
const input = document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')[index]
Object.defineProperty(input, 'files', { configurable: true, value: [file] })
input.dispatchEvent(new Event('change', { bubbles: true }))
}
async function clickMoreAction(text: string) {
document.querySelector<HTMLButtonElement>('.el-table__body button[aria-label^="更多客户操作:"]')?.click()
await flushPromises()
@@ -139,6 +147,10 @@ describe('CustomersView', () => {
vi.mocked(getPlatformTenant).mockResolvedValue(fullTenant)
vi.mocked(listTenantQualifications).mockResolvedValue([])
vi.mocked(listEnterpriseReviews).mockResolvedValue([])
vi.mocked(createPlatformTenant).mockResolvedValue({
tenant: fullTenant, administratorUsername: 'merchant_owner', temporaryPassword: 'Temporary#2026',
})
vi.mocked(submitPlatformTenantReview).mockResolvedValue({ ...fullTenant, status: 'PENDING' })
vi.mocked(updatePlatformTenant).mockResolvedValue(fullTenant)
vi.mocked(uploadPlatformTenantQualification).mockResolvedValue({
qualificationId: 'qualification-1', reviewId: null, tenantStatus: 'PROFILE_REQUIRED', documentUrl: '/files/license.png',
@@ -187,7 +199,7 @@ describe('CustomersView', () => {
await flushPromises()
expect(getPlatformTenant).toHaveBeenCalledWith(fullTenant.id)
const phone = document.querySelector<HTMLInputElement>('input[placeholder="请输入手机号或固定电话"]')
const phone = document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')
const email = document.querySelector<HTMLInputElement>('input[placeholder="如:contact@example.com"]')
expect(phone?.value).toBe(fullTenant.contactPhone)
expect(email?.value).toBe(fullTenant.contactEmail)
@@ -212,19 +224,83 @@ describe('CustomersView', () => {
expect(preview?.src).toBe('blob:qualification-preview')
})
it('marks only the business license and legal-person ID as required qualifications', async () => {
it('marks only the business license as a required file when creating a customer', async () => {
const auth = useAuthStore()
auth.permissions = [...auth.permissions, 'platform:customer:create']
useAccessPreviewStore().initializeAfterLogin()
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.trim() === '新增客户')?.click()
await flushPromises()
const labels = [...document.querySelectorAll<HTMLElement>('.qualification-label')]
expect(labels.map((label) => label.textContent?.replace(/\s/g, ''))).toEqual([
'*营业执照',
'*法人身份证',
'法人身份证选填',
'货代资质证书选填',
'银行开户许可证选填',
])
expect(document.body.textContent).toContain('营业执照和18位统一社会信用代码为必填业务项')
})
it('creates a customer with a business license and no legal-person ID', async () => {
const auth = useAuthStore()
auth.permissions = [...auth.permissions, 'platform:customer:create']
useAccessPreviewStore().initializeAfterLogin()
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.trim() === '新增客户')?.click()
await flushPromises()
await wrapper.get('input[placeholder="请输入工商登记公司名称"]').setValue(fullTenant.tenantName)
await wrapper.get('input[placeholder="请输入营业执照上的企业名称"]').setValue(fullTenant.legalName)
await wrapper.get('input[placeholder="请输入法定代表人姓名"]').setValue(fullTenant.legalRepresentative)
await wrapper.get('input[placeholder="请输入主要联系人姓名"]').setValue(fullTenant.contactName)
await wrapper.get('input[placeholder="请输入公司联系电话"]').setValue(fullTenant.contactPhone)
await wrapper.get('input[placeholder="请输入18位大写字母或数字"]').setValue(fullTenant.unifiedSocialCreditCode)
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
selectQualificationFile(0, licenseFile)
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
await flushPromises()
expect(createPlatformTenant).toHaveBeenCalledWith(expect.objectContaining({
tenantName: fullTenant.tenantName,
contactPhone: fullTenant.contactPhone,
}))
expect(createPlatformTenant).toHaveBeenCalledWith(expect.not.objectContaining({ unifiedSocialCreditCode: expect.anything() }))
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
fullTenant.id, licenseFile, null, fullTenant.unifiedSocialCreditCode, false, false,
)
})
it('keeps the company contact phone editable without changing a member login phone', async () => {
const activeTenant = { ...fullTenant, status: 'ACTIVE' as const }
vi.mocked(listPlatformTenants).mockResolvedValue({ records: [activeTenant], page: 1, size: 10, total: 1, pages: 1 })
vi.mocked(getPlatformTenant).mockResolvedValue(activeTenant)
vi.mocked(updatePlatformTenant).mockResolvedValue(activeTenant)
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'APPROVED', reviewComment: null, validTo: null,
}])
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
const phone = wrapper.get('input[placeholder="请输入公司联系电话"]')
expect(phone.attributes('disabled')).toBeUndefined()
await phone.setValue('0755-12345678')
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
await flushPromises()
expect(updatePlatformTenant).toHaveBeenCalledWith(fullTenant.id, expect.objectContaining({ contactPhone: '0755-12345678' }))
expect(document.body.textContent).toContain('不会修改企业管理员登录手机号')
expect(customersSource).not.toContain('联系电话(登录用户名)')
})
it('does not count rejected or superseded history as a current qualification when resubmitting', async () => {
@@ -272,11 +348,11 @@ describe('CustomersView', () => {
await flushPromises()
expect(getPlatformTenant).toHaveBeenCalledWith(fullTenant.id)
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入手机号或固定电话"]')?.value).toBe(fullTenant.contactPhone)
expect(document.querySelector<HTMLInputElement>('input[placeholder="请输入公司联系电话"]')?.value).toBe(fullTenant.contactPhone)
expect(routerReplace).toHaveBeenCalledWith({ name: 'customers' })
})
it('saves both required files and the credit code through one package action', async () => {
it('saves the business license, credit code and optional legal-person ID through one package action', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID',
certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null,
@@ -288,13 +364,8 @@ describe('CustomersView', () => {
const licenseFile = new File(['license'], 'license.png', { type: 'image/png' })
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
const qualificationInputs = document.querySelectorAll<HTMLInputElement>('.qualification-file input[type="file"]')
const licenseInput = qualificationInputs[0]
const legalPersonInput = qualificationInputs[1]
Object.defineProperty(licenseInput, 'files', { configurable: true, value: [licenseFile] })
licenseInput?.dispatchEvent(new Event('change', { bubbles: true }))
Object.defineProperty(legalPersonInput, 'files', { configurable: true, value: [legalPersonFile] })
legalPersonInput?.dispatchEvent(new Event('change', { bubbles: true }))
selectQualificationFile(0, licenseFile)
selectQualificationFile(1, legalPersonFile)
await flushPromises()
const buttons = [...document.querySelectorAll<HTMLButtonElement>('button')]
@@ -314,6 +385,101 @@ describe('CustomersView', () => {
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
})
it('replaces only the business license while preserving an existing legal-person ID', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([
{ id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE', certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null },
{ id: 'qualification-legal', tenantId: fullTenant.id, qualificationType: 'LEGAL_PERSON_ID', certificateNo: null, documentUrl: '/files/legal-person.png', status: 'PENDING', reviewComment: null, validTo: null },
])
vi.mocked(uploadPlatformTenantRequiredQualificationPackage).mockResolvedValueOnce({
businessLicenseQualificationId: 'qualification-business-new', legalPersonIdQualificationId: null,
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/files/license-new.png', legalPersonIdDocumentUrl: null,
})
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
const licenseFile = new File(['license-new'], 'license-new.png', { type: 'image/png' })
selectQualificationFile(0, licenseFile)
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
await flushPromises()
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledWith(
fullTenant.id, licenseFile, null, fullTenant.unifiedSocialCreditCode, true, false,
)
expect(uploadPlatformTenantQualification).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('法人身份证可按需补充')
})
it('uploads an optional legal-person ID independently when the required business license already exists', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null,
}])
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
const legalPersonFile = new File(['legal-person'], 'legal-person.png', { type: 'image/png' })
selectQualificationFile(1, legalPersonFile)
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('保存资料和资质'))?.click()
await flushPromises()
expect(uploadPlatformTenantRequiredQualificationPackage).not.toHaveBeenCalled()
expect(uploadPlatformTenantQualification).toHaveBeenCalledWith(
fullTenant.id, 'LEGAL_PERSON_ID', legalPersonFile, undefined, undefined, false,
)
})
it('retains a selected business license after an upload failure so the operator can retry', async () => {
vi.mocked(uploadPlatformTenantRequiredQualificationPackage)
.mockRejectedValueOnce(new Error('upload unavailable'))
.mockResolvedValueOnce({
businessLicenseQualificationId: 'qualification-business', legalPersonIdQualificationId: null,
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/files/license-new.png', legalPersonIdDocumentUrl: null,
})
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
const licenseFile = new File(['license-new'], 'retry-license.png', { type: 'image/png' })
selectQualificationFile(0, licenseFile)
await flushPromises()
const saveButton = [...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.includes('保存资料和资质'))
saveButton?.click()
await flushPromises()
expect(document.body.textContent).toContain('retry-license.png')
saveButton?.click()
await flushPromises()
expect(uploadPlatformTenantRequiredQualificationPackage).toHaveBeenCalledTimes(2)
})
it('submits a customer for review with a business license and no legal-person ID', async () => {
vi.mocked(listTenantQualifications).mockResolvedValue([{
id: 'qualification-license', tenantId: fullTenant.id, qualificationType: 'BUSINESS_LICENSE',
certificateNo: fullTenant.unifiedSocialCreditCode, documentUrl: '/files/license.png', status: 'PENDING', reviewComment: null, validTo: null,
}])
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
tableButtonByText('编辑')?.click()
await flushPromises()
;[...document.querySelectorAll<HTMLButtonElement>('button')]
.find((button) => button.textContent?.trim() === '提交审核')?.click()
await flushPromises()
expect(ElMessageBox.confirm).toHaveBeenCalled()
expect(submitPlatformTenantReview).toHaveBeenCalledWith(fullTenant.id)
})
it('submits the current filters to the backend export service without loading every customer page', async () => {
const wrapper = mount(CustomersView, { attachTo: document.body, global: { plugins: [pinia, ElementPlus] } })
await flushPromises()
@@ -398,7 +564,7 @@ describe('CustomersView', () => {
.find((button) => button.textContent?.trim() === '审核通过')?.click()
await flushPromises()
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料与必填资质完整、清晰,审核通过')
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料、营业执照和统一社会信用代码完整、清晰,审核通过(法人身份证为选填项)')
expect(rejectEnterpriseReview).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('请立即保存以下一次性凭证')
expect(document.body.textContent).toContain('merchant_8_owner')
@@ -459,7 +625,7 @@ describe('CustomersView', () => {
.find((button) => button.textContent?.trim() === '审核通过')?.click()
await flushPromises()
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料与必填资质完整、清晰,审核通过')
expect(approveEnterpriseReview).toHaveBeenCalledWith(pendingReview.id, '企业资料、营业执照和统一社会信用代码完整、清晰,审核通过(法人身份证为选填项)')
expect(rejectEnterpriseReview).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('注册账号继续作为企业管理员')
expect(document.body.textContent).not.toContain('请立即保存以下一次性凭证')
+21 -1
View File
@@ -377,7 +377,7 @@ describe('platform IAM roles', () => {
expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/iam/tenants/8/submit-review', {})
})
it('uploads both required files and the credit code in one package request', async () => {
it('uploads the business license, credit code and optional legal-person ID in one package request', async () => {
const result = {
businessLicenseQualificationId: '91', legalPersonIdQualificationId: '92',
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/license.pdf',
@@ -400,6 +400,26 @@ describe('platform IAM roles', () => {
expect(form.get('replaceLegalPersonId')).toBe('true')
})
it('omits the optional legal-person ID parts when only the business license is uploaded', async () => {
const result = {
businessLicenseQualificationId: '91', legalPersonIdQualificationId: null,
tenantStatus: 'PROFILE_REQUIRED', businessLicenseDocumentUrl: '/license.pdf',
legalPersonIdDocumentUrl: null,
}
vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: result })
const businessLicense = new File(['%PDF-1.4'], '营业执照.pdf', { type: 'application/pdf' })
await expect(uploadPlatformTenantRequiredQualificationPackage(
'8', businessLicense, null, '91440300MA5ABCDE12', true, false,
)).resolves.toEqual(result)
const form = vi.mocked(http.post).mock.calls[0]?.[1] as FormData
expect(form.get('businessLicense')).toBe(businessLicense)
expect(form.get('legalPersonId')).toBeNull()
expect(form.get('replaceBusinessLicense')).toBe('true')
expect(form.get('replaceLegalPersonId')).toBeNull()
})
it('preserves long review identifiers and returns one-time administrator credentials', async () => {
const reviewId = '190000000000000001'
const approval = {