import { readFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const SCENARIO = 'mojia-measurement' const DEFAULT_IMAGE = resolve( dirname(fileURLToPath(import.meta.url)), '..', '..', 'apps', 'pda-uniapp', 'static', 'c7.png') function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function data(response) { return response?.body?.data ?? null } function record(run, name, response, passed, evidence = '') { return run.addStep(SCENARIO, name, response, passed, evidence) } function responseOf(http, body, raw = null) { return { method: 'POST', path: null, http, body, raw, requestId: body?.requestId ?? null, traceId: body?.traceId ?? null, } } async function multipartRequest(run, path, fields, imagePath) { const form = new FormData() form.set('data', JSON.stringify(fields)) form.set('image', new Blob([await readFile(imagePath)], { type: 'image/png' }), 'mojia-e2e.png') try { const response = await fetch(`${run.options.gateway}${path}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${run.token}` }, body: form, signal: AbortSignal.timeout(30_000), }) const text = await response.text() let body = null try { body = text ? JSON.parse(text) : null } catch { /* Keep raw response for the report. */ } return { method: 'POST', path, http: response.status, body, raw: body === null ? text.slice(0, 500) : null, requestId: body?.requestId ?? response.headers.get('x-request-id'), traceId: body?.traceId ?? response.headers.get('x-trace-id'), } } catch (error) { return responseOf(0, null, error instanceof Error ? error.message : String(error)) } } function requireData(run, name, response, evidence = '') { record(run, name, response, success(response) && data(response) !== null, evidence) if (!success(response) || data(response) === null) { throw new Error(`${name} failed: code=${response?.body?.code}, message=${response?.body?.message}`) } return data(response) } /** * Run the internal platform simulation for the MoJia carton measurement flow. * The final external MoJia key/signature contract is intentionally not defined here. * * @param {object} run Existing AcceptanceRun instance from admin-test-env-acceptance.mjs. * @param {object} [options] * @param {string|number} [options.tenantId] Customer scope for selecting the test inbound. * @param {string|number} [options.inboundId] Explicit WAIT_MEASURE inbound, preferred for CI. * @param {string} [options.imagePath] PNG/JPG evidence image path. */ export async function runMojiaMeasurementScenario(run, options = {}) { const tenantId = options.tenantId ?? process.env.MOJIA_TEST_TENANT_ID const imagePath = options.imagePath ?? process.env.MOJIA_TEST_IMAGE ?? DEFAULT_IMAGE let inboundId = options.inboundId ?? process.env.MOJIA_TEST_INBOUND_ID if (!tenantId && !inboundId) { const response = responseOf(0, { code: 'DEPLOYMENT_BLOCKED', message: '缺少墨家测试 tenantId 或 inboundId' }) record(run, '墨家测量测试数据配置', response, false, '请传入 MOJIA_TEST_TENANT_ID 或 MOJIA_TEST_INBOUND_ID') return false } try { if (!inboundId) { const response = await run.request( 'GET', '/api/platform/inbounds?processStatus=WAIT_MEASURE&page=1&size=200') const rows = requireData(run, '查找墨家 WAIT_MEASURE 入库单', response) const candidate = rows.records?.find((row) => String(row.tenantId) === String(tenantId) && row.totalPackageCount == null) if (!candidate) { record(run, '墨家测量测试数据配置', response, false, `tenantId=${tenantId} 下没有未冻结总箱数的 WAIT_MEASURE 入库单`) return false } inboundId = String(candidate.id) } const detailPath = `/api/platform/inbounds/${inboundId}?tenantId=${tenantId}` const detailResponse = await run.request('GET', detailPath) const detail = requireData(run, '查询墨家测试入库单', detailResponse, `inboundId=${inboundId}`) const summary = detail.summary if (summary.processStatus !== 'WAIT_MEASURE' || summary.totalPackageCount != null) { record(run, '墨家测试入库单状态校验', detailResponse, false, `processStatus=${summary.processStatus}, totalPackageCount=${summary.totalPackageCount}`) return false } const resolvedTenantId = String(summary.tenantId ?? tenantId) const printPath = `/api/platform/inbounds/${inboundId}/carton-label-pdf?tenantId=${resolvedTenantId}` const printResponse = await run.requestBinary('POST', printPath) record(run, '墨家测试生成稳定箱唛 PDF', printResponse, printResponse.http === 200 && printResponse.bytes?.length > 0 && printResponse.contentType?.toLowerCase().includes('application/pdf'), `inboundId=${inboundId}, bytes=${printResponse.bytes?.length ?? 0}`) const afterPrintResponse = await run.request('GET', detailPath) const afterPrint = requireData(run, '回查墨家稳定箱号总数', afterPrintResponse) const printedTotal = Number(afterPrint.summary.totalPackageCount) if (!Number.isInteger(printedTotal) || printedTotal < 1) { throw new Error('打印后没有冻结原始总箱数') } const boxCode = `${afterPrint.summary.inboundCode}#${printedTotal}-1` const scanPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/carton-scans?tenantId=${resolvedTenantId}` const firstFields = { boxCode, length: 59.1, width: 40.2, height: 30.3, machineNo: 'MJ-ADMIN-E2E-01', measuredAt: '2026-08-08T10:00:00', } const firstResponse = await multipartRequest(run, scanPath, firstFields, imagePath) const first = requireData(run, '墨家单箱首次测量上传', firstResponse, `boxCode=${boxCode}`) const firstPassed = first.created === true && first.confirmedPackageCount === 1 && String(first.boxCode) === boxCode record(run, '墨家单箱首次记录语义', firstResponse, firstPassed, `itemId=${first.itemId}, measurementId=${first.measurementId}`) if (!firstPassed) throw new Error('首次扫描没有创建单箱记录或默认箱数不是 1') const countPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${first.itemId}/confirmed-package-count?tenantId=${resolvedTenantId}` const countResponse = await run.request('PUT', countPath, { measurementVersion: first.measurementVersion, confirmedPackageCount: printedTotal, }) const counted = requireData(run, '人工确认墨家单箱代表箱数', countResponse, `confirmedPackageCount=${printedTotal}`) if (counted.confirmedPackageCount !== printedTotal) { throw new Error('人工确认箱数回显不一致') } const rescanResponse = await multipartRequest(run, scanPath, { boxCode, length: 59.5, width: 41.2, height: 31.3, machineNo: 'MJ-ADMIN-E2E-02', measuredAt: '2026-08-08T10:01:00', }, imagePath) const rescan = requireData(run, '墨家同箱复测覆盖', rescanResponse) const rescanPassed = rescan.created === false && String(rescan.itemId) === String(first.itemId) && rescan.confirmedPackageCount === printedTotal && Number(rescan.lengthCm) === 59.5 record(run, '墨家同箱复测保留人工箱数', rescanResponse, rescanPassed, `itemId=${rescan.itemId}, confirmedPackageCount=${rescan.confirmedPackageCount}`) if (!rescanPassed) throw new Error('同箱复测没有覆盖设备值或覆盖人工箱数') const submitPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/submit?tenantId=${resolvedTenantId}` const submitResponse = await run.request('POST', submitPath, { measurementId: rescan.measurementId, version: rescan.measurementVersion, }) const submitted = requireData(run, '提交墨家测量审核', submitResponse) const submittedPassed = submitted.processStatus === 'MEASURE_PENDING' && submitted.measureReviewStatus === 'PENDING' record(run, '墨家测量进入待审核', submitResponse, submittedPassed, `processStatus=${submitted.processStatus}, reviewStatus=${submitted.measureReviewStatus}`) if (!submittedPassed) throw new Error('墨家测量提交后状态不正确') const blockedResponse = await multipartRequest(run, scanPath, { boxCode, length: 99.1, width: 99.2, height: 99.3, machineNo: 'MJ-ADMIN-E2E-BLOCKED', measuredAt: '2026-08-08T10:02:00', }, imagePath) const blockedPassed = blockedResponse.body?.code === 'A0409' && blockedResponse.body?.message?.includes('测量已提交') record(run, '提交后拒绝墨家设备覆盖', blockedResponse, blockedPassed, `code=${blockedResponse.body?.code}, message=${blockedResponse.body?.message ?? ''}`) if (!blockedPassed) throw new Error('提交后没有拒绝墨家设备覆盖') run.context ??= {} run.context.mojiaMeasurement = { tenantId: resolvedTenantId, inboundId: String(inboundId), boxCode, measurementId: String(rescan.measurementId), itemId: String(rescan.itemId), } return true } catch (error) { const response = responseOf(0, null, error instanceof Error ? error.message : String(error)) record(run, '墨家测量场景执行', response, false, response.raw) return false } }