import { createHash } from 'node:crypto' const MODULE = 'merchant-qualification-fixture' const REQUIRED_TYPES = new Set(['BUSINESS_LICENSE', 'LEGAL_PERSON_ID']) const QUALIFICATION_IMAGE = Uint8Array.from(Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64', )) function id(value) { return value === undefined || value === null ? '' : String(value) } export function qualificationCreditCode(marker, tenantId) { const customerId = id(tenantId) if (!String(marker || '').trim() || !customerId) { throw new Error('测试资质代码必须使用完整批次标识和本轮真实客户编号') } const suffix = createHash('sha256').update(JSON.stringify([String(marker), customerId])) .digest('hex').slice(0, 10).toUpperCase() return `91440300${suffix}` } async function waitForDimensionInitialization(context, tenantId) { let response = null let status = 'PENDING' let attempt = 0 for (; attempt < 30; attempt += 1) { response = await context.request('GET', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/dimension-initialization`) status = response.http === 200 && String(response.body?.code) === '200' ? response.body?.data?.status : 'REQUEST_FAILED' if (status === 'READY' || status === 'FAILED') break await new Promise((resolve) => setTimeout(resolve, 500)) } context.assert(MODULE, '客户默认尺寸后台初始化已就绪', response, status === 'READY', `tenantId=${tenantId}, status=${status}, attempts=${attempt + 1}`, { deploymentBlocker: true }) if (status !== 'READY') throw new Error(`客户默认尺寸初始化未就绪: ${status}`) } export function normalizeFixedCustomerId(value) { const text = String(value || '').trim().replace(/^TEN/, '') if (!/^[1-9]\d{0,19}$/.test(text)) throw new Error('固定客户编号必须为 TEN 编号或数字 ID') return text } export function requireFixedSecondaryCustomer(options) { const secondary = options.fixedSecondaryCustomer if (!secondary?.customerId || !secondary.username || !secondary.password) throw new Error('固定客户模式未配置第二个客户,跨客户场景阻断;禁止自动建客或用同一客户替代隔离验证') if (normalizeFixedCustomerId(secondary.customerId) === normalizeFixedCustomerId(options.fixedCustomerId)) throw new Error('第二个固定客户必须不同于主客户,禁止替代隔离验证') return secondary } async function reuseFixedCustomer(context, publishEnvironment, requireDistinctCustomer) { const secondary = requireDistinctCustomer ? requireFixedSecondaryCustomer(context.options) : null const tenantId = normalizeFixedCustomerId(secondary?.customerId || context.options.fixedCustomerId) const username = secondary ? secondary.username : context.options.merchantUsername || process.env.MERCHANT_TEST_USERNAME const password = secondary ? secondary.password : context.options.merchantPassword || process.env.MERCHANT_TEST_PASSWORD if (!username || !password) throw new Error('请配置固定客户的 MERCHANT_TEST_USERNAME / MERCHANT_TEST_PASSWORD;不会新建客户或重置密码') const loginResponse = await context.request('POST', '/api/auth/login', {loginId:username,password}, '') const login = context.apiSuccess(MODULE, '登录已配置的固定客户账号', loginResponse) if (!login?.accessToken) throw new Error('固定客户账号登录失败') let profile try { const response = await context.request('GET', '/api/merchant/v1/iam/tenant/profile', undefined, login.accessToken) profile = context.apiSuccess(MODULE, '核对固定客户账号实际归属和状态', response) const valid = id(profile?.id) === tenantId && profile?.status === 'ACTIVE' context.assert(MODULE, '固定客户账号属于指定已审核客户', response, valid, `expected=${tenantId}, actual=${id(profile?.id)}, status=${profile?.status}`, {deploymentBlocker:true}) if (!valid) throw new Error('固定客户账号归属或状态不匹配,停止造数;不会新建客户') await waitForDimensionInitialization(context, tenantId) } finally { await context.request('POST', '/api/auth/logout', {refreshToken:login.refreshToken}, login.accessToken) } if (publishEnvironment) { process.env.MERCHANT_TEST_USERNAME = username process.env.MERCHANT_TEST_PASSWORD = password } const fixture = {tenantId,tenantName:profile.tenantName,username,marker:context.marker,reused:true} context.context.merchantQualificationFixture = {...fixture,password} context.context.customerId = tenantId context.createdResources.merchantQualificationFixture = fixture } export async function run(context, { publishEnvironment = true, requireDistinctCustomer = !publishEnvironment } = {}) { if (context.options?.fixedCustomerId) return reuseFixedCustomer(context, publishEnvironment, requireDistinctCustomer) if (context.options?.environment === 'test') throw new Error('TEST 必须配置固定客户,禁止自动建客') const suffix = context.marker.replace(/[^0-9A-Z]/gi, '').slice(-10).padStart(10, '0') const customerName = String(context.options?.customerName || '').trim() const tenantName = customerName || `全量验收下单客户-${suffix}` const phone = await context.findAvailablePhone(MODULE) const createResponse = await context.request('POST', '/api/platform/v1/iam/tenants', { tenantName, legalName: customerName ? `${customerName}有限公司` : `全量验收下单有限公司-${suffix}`, legalRepresentative: '全量验收法人', contactName: '全量验收管理员', contactPhone: phone, contactEmail: `merchant-fixture-${suffix.toLowerCase()}@example.com`, servicePackage: '全量验收', salesOwnerName: '全量验收', paymentTermDays: 30, enterpriseType: 'LIMITED_COMPANY', establishedDate: '2020-01-02', registeredCapital: 1000000, registeredCapitalCurrency: 'CNY', registeredAddress: '深圳市全量验收地址', internalRemark: `${context.marker}-MERCHANT-QUALIFICATION`, }) const created = context.apiSuccess(MODULE, '创建隔离下单客户', createResponse) const customer = created?.tenant || created?.customer || created const tenantId = id(customer?.id || created?.tenantId) const username = created?.administratorUsername const password = created?.temporaryPassword context.assert(MODULE, '下单客户和一次性管理员凭据完整', createResponse, Boolean(tenantId && username && password), `tenantId=${tenantId || 'missing'}, username=${username || 'missing'}, passwordGenerated=${Boolean(password)}`, { deploymentBlocker: true }) if (!tenantId || !username || !password) throw new Error('创建下单客户未返回完整管理员凭据') const packageForm = new FormData() packageForm.append('businessLicense', new File([QUALIFICATION_IMAGE], `business-${suffix}.png`, { type: 'image/png' })) packageForm.append('legalPersonId', new File([QUALIFICATION_IMAGE], `legal-person-${suffix}.png`, { type: 'image/png' })) const unifiedSocialCreditCode = qualificationCreditCode(context.marker, tenantId) const packageResponse = await context.requestWithBody('POST', `/api/platform/v1/files/qualifications/${encodeURIComponent(tenantId)}/required-package?unifiedSocialCreditCode=${encodeURIComponent(unifiedSocialCreditCode)}`, packageForm) context.apiSuccess(MODULE, '通过 File 上传真实必填资质包', packageResponse, `tenantId=${tenantId}`, { deploymentBlocker: true }) const submitResponse = await context.request('POST', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/submit-review`) const submitted = context.apiSuccess(MODULE, '提交企业资质审核', submitResponse, `tenantId=${tenantId}`, { deploymentBlocker: true }) context.assert(MODULE, '企业进入待审核状态', submitResponse, submitted?.status === 'PENDING', `status=${submitted?.status ?? 'missing'}`, { deploymentBlocker: true }) const reviewsResponse = await context.request('GET', '/api/platform/v1/iam/reviews') const reviews = context.apiSuccess(MODULE, '查询待审核企业', reviewsResponse, '', { deploymentBlocker: true }) const review = Array.isArray(reviews) ? reviews.find((item) => id(item.tenantId) === tenantId) : null context.assert(MODULE, '待审核列表包含隔离下单客户', reviewsResponse, Boolean(review?.id), `tenantId=${tenantId}, reviewId=${id(review?.id) || 'missing'}`, { deploymentBlocker: true }) if (!review?.id) throw new Error('未找到隔离下单客户的待审核单') await waitForDimensionInitialization(context, tenantId) const approveResponse = await context.request('POST', `/api/platform/v1/iam/reviews/${encodeURIComponent(review.id)}/approve`, { comment: `${context.marker} 全量验收正式资质审核通过` }) context.apiSuccess(MODULE, '平台审核通过企业资质', approveResponse, `tenantId=${tenantId}`, { deploymentBlocker: true }) const qualificationsResponse = await context.request('GET', `/api/platform/v1/iam/tenants/${encodeURIComponent(tenantId)}/qualifications`) const qualifications = context.apiSuccess(MODULE, '回查企业资质审核结果', qualificationsResponse) const approvedTypes = new Set((Array.isArray(qualifications) ? qualifications : []) .filter((item) => item.status === 'APPROVED') .map((item) => item.qualificationType)) const eligible = [...REQUIRED_TYPES].every((type) => approvedTypes.has(type)) context.assert(MODULE, '全部必填企业资质已审核通过', qualificationsResponse, eligible, `approved=${[...approvedTypes].join(',') || 'missing'}`, { deploymentBlocker: true }) if (!eligible) throw new Error('隔离下单客户必填资质未全部审核通过') if (publishEnvironment) { process.env.MERCHANT_TEST_USERNAME = username process.env.MERCHANT_TEST_PASSWORD = password } context.context.merchantQualificationFixture = { tenantId, tenantName, username, password, marker: context.marker, } context.createdResources.merchantQualificationFixture = { tenantId, tenantName, username, marker: context.marker, } }