Files
baiyue-firstleg/scripts/procurement-sample-test-env.mjs
T

120 lines
9.3 KiB
JavaScript
Raw Normal View History

import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { parseEnv } from 'node:util'
import { resolve, dirname } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
import { run as prepareCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs'
2026-10-22 11:56:47 +08:00
import { verifyProcurementSample, PROCUREMENT_REQUIRED_AVAILABLE_BALANCE } from './admin-test-env-modules/procurement-sample-fulfillment.mjs'
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')
2026-10-22 11:56:47 +08:00
export function requireExistingProcurementBalance(account) {
if (account?.availableBalance == null || !Number.isFinite(Number(account.availableBalance))
|| Number(account.availableBalance) < Number(PROCUREMENT_REQUIRED_AVAILABLE_BALANCE)) throw new Error(`采购样本需要至少 ${PROCUREMENT_REQUIRED_AVAILABLE_BALANCE} 元现有可用余额;禁止自动充值`)
}
export function options(args) {
const value = name => { const index = args.indexOf(name); return index < 0 ? '' : args[index + 1] || '' }
if (!args.includes('--execute')) return { execute: false }
const runId = value('--run-id'), warehouseId = value('--warehouse-id')
if (value('--environment') !== 'TEST' || !/^[A-Za-z0-9-]{1,32}$/.test(runId) || !/^[1-9]\d*$/.test(warehouseId)) throw new Error('须显式指定 TEST、唯一 run-id 和已核对的有效收货仓')
return { execute: true, runId, warehouseId }
}
export function accountForCustomer(data, customerId) {
if (!Array.isArray(data)) throw new Error('Finance账户响应须为正式数组结构')
const account = data.find(row => String(row.customerId) === String(customerId))
if (!account) throw new Error('账户响应未包含本轮独立客户')
return account
}
export async function execute(args = process.argv.slice(2), execArgs = process.execArgv) {
const config = options(args)
if (!config.execute) return { mode: 'PLAN', writes: ['创建独立客户并审核资质', '仅为本轮客户充值10元', '上传样品凭证和面单', '执行采购两箱完整正常流程'], sql: false, deploy: false, cleanup: '保留样本,退出自身会话;未知结果不自动重跑' }
const flag = execArgs.find(value => value.startsWith('--env-file='))
const envPath = flag ? resolve(flag.slice('--env-file='.length)) : ''
if (!envPath.replaceAll('\\','/').endsWith('/.codex/env/chongchong-global/test-acceptance.env')) throw new Error('脚本前必须显式选择授权 TEST 环境文件')
const env = parseEnv(await readFile(envPath, 'utf8'))
if (env.CHONGCHONG_ENV !== 'test' || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw new Error('TEST身份配置缺失')
const gateway = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
if (!['http:', 'https:'].includes(gateway.protocol) || gateway.pathname.replace(/\/$/,'') !== '/api' || gateway.username || gateway.password || gateway.search || gateway.hash) throw new Error('TEST统一入口格式不符')
const repository = resolve(dirname(fileURLToPath(import.meta.url)), '..')
if (!repository.replaceAll('\\','/').endsWith('/chongchong-global/baiyue-firstleg')) throw new Error('只能在 TEST 工作区执行')
const output = resolve(repository, `reports/procurement-sample-${config.runId}/report.json`)
await mkdir(dirname(output), { recursive: true })
const context = new AcceptanceRun({ gateway: gateway.origin, environment: 'test', quiet: true, modules: [] })
context.marker = `PROC-${config.runId}`
const report = { marker: context.marker, status: 'PREPARING', startedAt: new Date().toISOString(), requests: [], sessions: [], resources: context.createdResources, steps: context.steps }
const secrets = new Set([env.CHONGCHONG_ADMIN_PASSWORD])
const sessions = []
function safe(value) {
if (Array.isArray(value)) return value.map(safe)
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key,item]) => [key, /password|token|loginId/i.test(key) ? '[REDACTED]' : safe(item)]))
if (typeof value === 'string') { for (const secret of secrets) if (secret && secret.length >= 8) value = value.replaceAll(secret, '[REDACTED]') }
return value
}
async function checkpoint(first = false) { await writeFile(output, JSON.stringify(safe(report), null, 2) + '\n', { encoding: 'utf8', flag: first ? 'wx' : 'w' }) }
await checkpoint(true)
context.checkpoint = checkpoint
const rawRequest = context.request.bind(context), rawForm = context.requestWithBody.bind(context)
async function recorded(send, method, path, body, token, headers) {
const entry = { method, path, body: body instanceof FormData ? { multipart: true, fields: [...body.keys()] } : safe(body), status: 'SENDING' }
report.requests.push(entry); await checkpoint()
let response
try { response = await send(method, path, body, token, headers) }
catch (error) { entry.status = 'UNKNOWN_RESULT'; await checkpoint(); throw error }
for (const field of ['accessToken','refreshToken','temporaryPassword']) if (response?.body?.data?.[field]) secrets.add(response.body.data[field])
const unknown = response?.http === 0 || response?.http >= 500 || !response?.body
Object.assign(entry, { status: unknown ? 'UNKNOWN_RESULT' : 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId })
await checkpoint()
if (unknown) throw new Error('请求结果未知,已保留原请求,禁止自动重跑')
return response
}
context.request = (method,path,body,token=context.token,headers={}) => recorded(rawRequest,method,path,body,token,headers)
context.requestWithBody = (method,path,body,token=context.token,headers={}) => recorded(rawForm,method,path,body,token,headers)
const requireSuccess = response => {
if (response?.http !== 200 || String(response?.body?.code) !== '200') throw new Error(`业务请求失败:${response?.http}/${response?.body?.code}`)
return response.body.data
}
async function login(username,password,actor) {
const data = requireSuccess(await context.request('POST','/api/auth/login',{loginId:username,password},''))
if (!data?.accessToken || !data?.refreshToken) throw new Error('登录响应缺少会话引用')
sessions.push({ actor, accessToken: data.accessToken, refreshToken: data.refreshToken })
report.sessions.push({ actor, login: 'SUCCESS', logout: 'NOT_STARTED' }); await checkpoint()
return data.accessToken
}
let failed = false
try {
context.token = await login(env.CHONGCHONG_ADMIN_USERNAME,env.CHONGCHONG_ADMIN_PASSWORD,'platform')
const warehouses = requireSuccess(await context.request('GET','/api/platform/v1/last-leg/overseas-procurements/warehouse-options'))
if (!Array.isArray(warehouses) || !warehouses.some(row => String(row.warehouseId) === config.warehouseId)) throw new Error('指定收货仓不在有效采购仓候选中')
2026-10-21 21:30:24 +08:00
await prepareCustomer(context,{publishEnvironment:false,requireDistinctCustomer:false})
if (context.steps.some(step => !step.passed)) throw new Error('独立客户准备检查未全部通过')
const fixture = context.context.merchantQualificationFixture
const customerId = String(fixture.tenantId), customerToken = await login(fixture.username,fixture.password,'customer')
const readWallet = async () => {
const page = requireSuccess(await context.request('GET',`/api/platform/finance/v1/accounts?page=1&pageSize=20&customerId=${customerId}`))
return accountForCustomer(page, customerId)
}
const before = await readWallet()
2026-10-22 11:56:47 +08:00
requireExistingProcurementBalance(before)
const upload = async (orderNo,category) => requireSuccess(await context.requestMultipart('POST',`/api/platform/v1/last-leg/overseas-procurements/${encodeURIComponent(orderNo)}/files/${category}?customerId=${customerId}`,{ content:PNG,type:'image/png',name:`${category.toLowerCase()}.png` }))
report.status = 'RUNNING'; await checkpoint()
await verifyProcurementSample(context,{ marker:context.marker,customerId,customerName:fixture.tenantName,customerToken,warehouseId:config.warehouseId,readWallet,upload })
report.status = 'PASSED_NORMAL_FLOW'
} catch (error) { failed = true; report.status = 'FAILED'; report.failure = safe(error.message) }
finally {
for (const session of sessions.reverse()) {
const state = report.sessions.find(item => item.actor === session.actor)
try {
const response = await rawRequest('POST','/api/auth/logout',{refreshToken:session.refreshToken},session.accessToken)
state.logout = response?.http === 200 && String(response?.body?.code) === '200' ? 'SUCCESS' : 'FAILED'
state.http = response?.http; state.code = response?.body?.code
if (state.logout !== 'SUCCESS') { failed = true; report.status = 'FAILED' }
} catch { state.logout = 'UNKNOWN_RESULT'; failed = true; report.status = 'FAILED' }
}
report.finishedAt = new Date().toISOString(); await checkpoint()
}
return { output,status:report.status,failed }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
execute().then(result => { console.log(JSON.stringify(result)); process.exitCode = result.failed ? 1 : 0 })
.catch(() => { console.error('采购验收未启动或报告保存失败;请核对TEST配置、唯一批次和既有证据。'); process.exitCode = 1 })
}