import { findPricedOrderRouteAcrossOrigins, findPricedOrderRoute, freezeOrderCommand } from './orders.mjs' const MODULE = 'order-inbound-status-fixtures' const OPTIONS = { scenario: 'manual-order-inbound-status-matrix', deploymentBlocker: true } const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || '' const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || '' const ORDER_TARGETS = [ ['DRAFT', []], ['SUBMITTED', ['submit']], ['REJECTED', ['submit', 'reject']], ['APPROVED', ['submit', 'approve']], ['ACCEPTED', ['submit', 'approve', 'accept']], ['PROCESSING', ['submit', 'approve', 'accept', 'start']], ['COMPLETED', []], ['CANCELLED', ['cancel']], ] const INBOUND_FILTER_STATUSES = [ 'WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', 'WAIT_PRICING', 'FEE_CONFIRMING', 'INBOUNDING', 'WAIT_SPLIT', 'VOIDED', ] const UNIFIED_FILTER_STATUSES = [ 'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED', ] function id(value) { return value == null ? '' : String(value) } function rows(value) { return Array.isArray(value?.records) ? value.records : [] } function ok(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function data(response) { return response?.body?.data } function key(context, operation) { return `${context.marker}-manual-${operation}`.slice(0, 128) } function query(value) { return encodeURIComponent(String(value)) } function wait(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) } async function merchantRequest(context, method, path, body, token, headers = {}) { const requestHeaders = { Accept: 'application/json', Authorization: `Bearer ${token}`, ...headers } const payload = body === undefined ? undefined : JSON.stringify(body) if (payload !== undefined) requestHeaders['Content-Type'] = 'application/json; charset=utf-8' try { const response = await fetch(`${context.options.gateway}${path}`, { method, headers: requestHeaders, body: payload, signal: AbortSignal.timeout(30_000), }) const content = await response.text() let parsed = null try { parsed = content ? JSON.parse(content) : null } catch { /* Keep bounded raw evidence. */ } return { method, path, http: response.status, body: parsed, raw: parsed ? null : content.slice(0, 500), requestId: parsed?.requestId || response.headers.get('x-request-id'), traceId: parsed?.traceId || response.headers.get('x-trace-id') } } catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } } } async function merchantGet(context, path, token) { return merchantRequest(context, 'GET', path, undefined, token) } async function loadFixtureInputs(context, token) { const profileResponse = await merchantGet(context, '/api/merchant/v1/iam/tenant/profile', token) const profile = context.apiSuccess(MODULE, '读取 IT 测试客户资料', profileResponse) const domesticResponse = await merchantGet(context, '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', token) const domesticPage = context.apiSuccess(MODULE, '读取国内收货仓', domesticResponse) const receivings = rows(domesticPage).filter((item) => item?.status === 'ACTIVE') const destinationResponse = await merchantGet(context, '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA', token) const destinations = rows(context.apiSuccess(MODULE, '读取 FBA 目的仓', destinationResponse)) if (!profile?.id || receivings.length === 0 || destinations.length === 0) throw new Error('状态样本缺少客户或仓库基础数据') const priced = await findPricedOrderRouteAcrossOrigins({ receivings, destinations, loadChannels: (receiving) => merchantGet(context, `/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${query(receiving.id)}`, token), estimate: (body) => merchantRequest(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, token), }) if (!priced.receiving || !priced.priced) throw new Error('状态样本缺少真实可计价的仓库/渠道/目的仓组合') return { tenantId: id(profile.id), customerName: profile.tenantName || 'IT 测试客户', domestic: priced.receiving, destination: priced.priced.destination, channel: priced.priced.channel, pricingTaxMode: priced.priced.pricingTaxMode } } function orderPayload(context, fixture, target) { const sample = `${context.marker}-${target}` const shipmentId = `MANUAL-${target}-${context.marker}`.slice(0, 64) const referenceId = `STATUS-${target}-${context.marker}`.slice(0, 64) const upcNo = `${String(context.marker).replace(/\D/g, '').slice(-11)}${ORDER_TARGETS.findIndex(([status]) => status === target) + 1}` .slice(-12).padStart(12, '0') return { customerId: null, customerName: fixture.customerName, transportMode: 'SEA', tradeTerm: 'FOB', destinationCountryCode: fixture.destination.countryCode || 'US', destinationWarehouseId: fixture.destination.id, destinationWarehouseName: fixture.destination.warehouseName, expectedShipDate: new Date(Date.now() + 30 * 86400000).toISOString().slice(0, 10), customerRemark: `${sample} 人工列表状态样本`, pickupMethod: 'SELF_EXPRESS', receivingWarehouseId: fixture.domestic.id, receivingWarehouseName: fixture.domestic.warehouseName, channelCode: fixture.channel.channelCode, channelName: fixture.channel.channelName, carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION', pricingTaxMode: fixture.pricingTaxMode, containerMode: 'LCL', customsInspection: false, destinationType: 'FBA', amazonWarehouseCode: fixture.destination.warehouseCode, shipmentId, referenceId, sender: { companyName: fixture.customerName, contactName: 'IT 测试联系人', phone: '13800138000', email: 'it-test@example.com', countryCode: 'CN', province: '广东省', city: '深圳市', district: '南山区', addressLine: `${sample} 测试地址`, postalCode: '518000' }, receiver: null, cargoes: [{ cargoName: `${target} 状态样本货物`, cargoNameEn: `${target} fixture cargo`, hsCode: '8518210000', customsHsCode: '8518210000', packageType: 'CARTON', packageCount: 1, grossWeightKg: 10, volumeCbm: 0.024, lengthCm: 40, widthCm: 30, heightCm: 20, declaredValue: 100, currency: 'USD', dangerousGoods: false, woodenProduct: false, cargoRemark: sample, skuNo: `SKU-${target}-${context.marker}`.slice(0, 64), upcNo, shipmentId, referenceId, unitsPerPackage: 10, totalQuantity: 10, netWeightKg: 9, unitDeclaredValue: 10, material: '塑料', usage: '人工验收', model: `MODEL-${target}`, brand: 'IT-TEST' }], } } async function createOrder(context, token, fixture, target) { const response = await merchantRequest(context, 'POST', '/api/merchant/v1/orders/first-leg', orderPayload(context, fixture, target), token, { 'Idempotency-Key': key(context, `${target}-create`) }) const orderId = id(context.apiSuccess(MODULE, `创建 ${target} 订单状态样本`, response)) if (!orderId) throw new Error(`${target} 状态样本未返回订单 ID`) return orderId } export async function transition(context, token, fixture, orderId, target, action) { const merchantPath = `/api/merchant/v1/orders/first-leg/${query(orderId)}` if (action === 'submit') { const detail = context.apiSuccess(MODULE, `${target} 冻结提交版本`, await merchantRequest(context, 'GET', merchantPath, undefined, token)) const command = freezeOrderCommand(detail, key(context, `${target}-submit`)) const response = await merchantRequest(context, 'POST', `${merchantPath}/submit`, command.body, token, command.headers) context.apiSuccess(MODULE, `${target} 样本提交订单`, response) return } const platformPath = `/api/platform/v1/orders/first-leg/${query(orderId)}/${action}?tenantId=${query(fixture.tenantId)}` const body = action === 'reject' || action === 'cancel' ? { reason: `${context.marker} 保留 ${target} 人工样本` } : undefined const command = action === 'cancel' ? freezeOrderCommand(context.apiSuccess(MODULE, `${target} 冻结取消版本`, await context.request('GET', `/api/platform/v1/orders/first-leg/${query(orderId)}?tenantId=${query(fixture.tenantId)}`)), key(context, `${target}-cancel`), body) : null const response = await context.request('POST', platformPath, command?.body ?? body, context.token, command?.headers ?? {}) context.apiSuccess(MODULE, `${target} 样本执行 ${action}`, response) } export async function createCompletedOrder(context, runFulfillment = async scope => { const { run } = await import('./phase5-root-fulfillment.mjs') return run(scope) }, prepareCustomer = async scope => { const { run } = await import('./merchant-qualification-fixture.mjs') return run(scope, { publishEnvironment: false, requireDistinctCustomer: false }) }) { const scope = Object.create(context) scope.marker = `${context.marker}-COMPLETED` scope.context = { ...context.context, f40SignatureOrder: 'ROOT_FIRST' } scope.createdResources = { ...context.createdResources } await prepareCustomer(scope) try { await runFulfillment(scope) } finally { context.createdResources.manualCompletedOrder = scope.createdResources.phase5RootFulfillment } const proof = scope.createdResources.phase5RootFulfillment if (proof?.finalStatus !== 'VERIFIED' || id(proof.customerId) !== id(scope.context.merchantQualificationFixture?.tenantId) || !id(proof.rootOrderId)) throw new Error('COMPLETED 样本必须由同客户真实签收及完成Outbox验证产生') return { orderId: id(proof.rootOrderId), tenantId: id(proof.customerId) } } async function readOrder(context, fixture, orderId) { const response = await context.request('GET', `/api/platform/v1/orders/first-leg/${query(orderId)}?tenantId=${query(fixture.tenantId)}`) return { response, detail: context.apiSuccess(MODULE, '回读订单状态样本', response) } } async function waitForInbound(context, fixture, orderId, allowCompletedOrder = false) { const path = `/api/platform/v1/orders/${query(orderId)}?tenantId=${query(fixture.tenantId)}` // 审核/接受/完成会经过跨服务履约事件;测试环境高负载时可能超过10秒才落入库单。 // 保持轮询只读并延长观察窗口,避免把异步延迟误判为“未生成入库单”。 for (let attempt = 0; attempt < 240; attempt += 1) { const response = await context.request('GET', path) const summary = data(response)?.summary if (ok(response) && summary?.inboundId && (allowCompletedOrder || ['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', 'WAIT_SPLIT'].includes(summary.mainStatus))) { return { response, summary } } if (attempt < 239) await wait(500) } return { response: null, summary: null } } export async function retainFeeConfirmingFixture(context, { inboundId, tenantId }) { const suffix = `${query(inboundId)}?tenantId=${query(tenantId)}` const detailPath = `/api/platform/inbounds/${suffix}` const beforeResponse = await context.request('GET', detailPath) const before = context.apiSuccess(MODULE, '试算前核对独立待审核测量样本', beforeResponse, '', OPTIONS) if (id(before?.summary?.id) !== id(inboundId) || id(before?.summary?.tenantId) !== id(tenantId) || before?.summary?.processStatus !== 'MEASURE_PENDING' || !before?.summary?.inboundCode) { throw new Error('FEE_CONFIRMING 必须从本轮独立 MEASURE_PENDING 入库单生成') } const calculatedResponse = await context.request('POST', `/api/platform/v1/inbounds/${query(inboundId)}/fees/calculate?tenantId=${query(tenantId)}`, { weightSource: 'ACTUAL', volumeSource: 'ACTUAL' }, undefined, { 'Idempotency-Key': key(context, `retain-fee-confirming-${inboundId}`) }) const calculated = context.apiSuccess(MODULE, '审核前真实试算并保留待费用确认样本', calculatedResponse, '', OPTIONS) if (!calculated?.feeId || calculated?.feeStatus !== 'CALCULATED' || calculated?.nextProcessStatus !== 'FEE_CONFIRMING') { throw new Error('真实费用试算未生成 FEE_CONFIRMING/CALCULATED') } const latestResponse = await context.request('GET', `/api/platform/v1/inbounds/${query(inboundId)}/fees/latest?tenantId=${query(tenantId)}`) const latest = context.apiSuccess(MODULE, '回读待确认费用的同一快照', latestResponse, '', OPTIONS) const detailResponse = await context.request('GET', detailPath) const detail = context.apiSuccess(MODULE, '回读待费用确认样本持久状态', detailResponse, '', OPTIONS) const valid = id(detail?.summary?.id) === id(inboundId) && id(detail?.summary?.tenantId) === id(tenantId) && detail?.summary?.processStatus === 'FEE_CONFIRMING' && detail?.summary?.measureReviewStatus === 'PENDING' && id(latest?.feeId) === id(calculated.feeId) && latest?.feeVersion === calculated.feeVersion && latest?.feeStatus === 'CALCULATED' context.assert(MODULE, '费用已持久化且测量尚未审核,保留 FEE_CONFIRMING', detailResponse, valid, `status=${detail?.summary?.processStatus || 'missing'}, feeId=${id(latest?.feeId)}`, OPTIONS) if (!valid) throw new Error('FEE_CONFIRMING 持久状态或费用快照不一致') const listResponse = await context.request('GET', `/api/platform/inbounds?tenantId=${query(tenantId)}&inboundCode=${query(before.summary.inboundCode)}&processStatus=FEE_CONFIRMING&page=1&size=20`) const page = context.apiSuccess(MODULE, '按 FEE_CONFIRMING 查询保留样本', listResponse, '', OPTIONS) const visible = rows(page).some(row => id(row.id) === id(inboundId) && row.processStatus === 'FEE_CONFIRMING') context.assert(MODULE, '待费用确认筛选包含本轮真实样本', listResponse, visible, `inboundId=${inboundId}`, OPTIONS) if (!visible) throw new Error('FEE_CONFIRMING 列表未包含本轮样本') return { feeId: id(latest.feeId), feeVersion: latest.feeVersion } } export async function retainWaitPricingFixture(context, { inboundId, tenantId, orderId }) { const detailPath = `/api/platform/inbounds/${query(inboundId)}?tenantId=${query(tenantId)}` const before = context.apiSuccess(MODULE, '渠道变更前核对独立费用确认样本', await context.request('GET', detailPath), '', OPTIONS) const orderDetail = context.apiSuccess(MODULE, '渠道变更前读取当前订单版本与申报', await context.request('GET', `/api/platform/v1/orders/first-leg/${query(orderId)}?tenantId=${query(tenantId)}`), '', OPTIONS) const order = orderDetail?.order if (id(before?.summary?.id) !== id(inboundId) || id(before?.summary?.tenantId) !== id(tenantId) || id(before?.summary?.orderId) !== id(orderId) || before?.summary?.processStatus !== 'FEE_CONFIRMING' || id(order?.id) !== id(orderId) || order?.status !== 'APPROVED' || !orderDetail?.cargoes?.length) { throw new Error('WAIT_PRICING 必须从本轮 APPROVED 订单及 FEE_CONFIRMING 入库单生成') } const fixture = context.context?.merchantQualificationFixture const username = fixture?.username || context.options?.merchantUsername || process.env.MERCHANT_TEST_USERNAME const password = fixture?.password || context.options?.merchantPassword || process.env.MERCHANT_TEST_PASSWORD const login = context.apiSuccess(MODULE, '渠道样本客户独立登录', await context.request('POST', '/api/auth/login', { loginId: username, password }, ''), '', OPTIONS) if (!login?.accessToken) throw new Error('WAIT_PRICING 客户登录缺少会话') const token = login.accessToken try { const profile = context.apiSuccess(MODULE, '核对渠道目录所属客户', await context.request('GET', '/api/merchant/v1/iam/tenant/profile', undefined, token), '', OPTIONS) if (id(profile?.id) !== id(tenantId)) throw new Error('WAIT_PRICING 渠道目录客户身份不一致') const channels = context.apiSuccess(MODULE, '查询同仓可替换报价渠道', await context.request('GET', `/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${query(order.receivingWarehouseId)}`, undefined, token), '', OPTIONS) const route = await findPricedOrderRoute({ receiving: { id: order.receivingWarehouseId }, destinations: [{ id: order.destinationWarehouseId, warehouseCode: order.amazonWarehouseCode, warehouseName: order.destinationWarehouseName }], channels: (Array.isArray(channels) ? channels : []).filter(channel => channel.channelCode !== order.channelCode && channel.pricingTaxModes?.includes(order.pricingTaxMode)) .map(channel => ({ ...channel, pricingTaxModes: [order.pricingTaxMode] })), destinationType: order.destinationType, concurrency: 1, estimate: body => context.request('POST', '/api/merchant/v1/first-leg/pricing/estimate', body, token), }) if (!route?.priced?.channel) throw new Error('WAIT_PRICING 没有同仓同目的仓同税制的实际可计价替换渠道') const channel = route.priced.channel const changedResponse = await context.request('PUT', `/api/platform/v1/orders/${query(orderId)}/declaration?tenantId=${query(tenantId)}`, { expectedVersion: Number(order.version), customerRemark: order.customerRemark, expectedArrivalAt: order.expectedArrivalAt || null, estimatedVolumeCbm: order.estimatedVolumeCbm ?? null, channelCode: channel.channelCode, channelName: channel.channelName, cargoes: orderDetail.cargoes.map(cargo => Object.fromEntries([ 'itemNo', 'cargoName', 'cargoNameEn', 'skuNo', 'model', 'packageCount', 'totalQuantity', 'lengthCm', 'widthCm', 'heightCm', 'grossWeightKg', 'volumeCbm', 'netWeightKg', 'hsCode', 'customsHsCode', 'unitsPerPackage', 'unitDeclaredValue', 'declaredValue', 'currency', 'material', 'usage', 'brand', 'cargoRemark', ].map(field => [field, cargo[field] ?? (field === 'cargoRemark' ? cargo.remark : null) ?? null]))) }) const changed = context.apiSuccess(MODULE, '通过正式申报接口变更本轮未发布费用订单渠道', changedResponse, '', OPTIONS) if (Number(changed?.version) !== Number(order.version) + 1 || changed?.channelChanged !== true) { throw new Error('WAIT_PRICING 渠道变更未确认当前订单版本') } let detail let response for (let attempt = 0; attempt < 30; attempt += 1) { response = await context.request('GET', detailPath) detail = context.apiSuccess(MODULE, '等待渠道变更及费用失效事实收敛', response, '', OPTIONS) if (detail?.summary?.processStatus === 'WAIT_PRICING' && detail?.summary?.channelCode === channel.channelCode) break if (attempt < 29) await wait(500) } const valid = id(detail?.summary?.id) === id(inboundId) && id(detail?.summary?.tenantId) === id(tenantId) && detail?.summary?.processStatus === 'WAIT_PRICING' && detail?.summary?.feeStatus === 'UNPRICED' && detail?.summary?.channelCode === channel.channelCode context.assert(MODULE, '真实渠道变更保留 WAIT_PRICING 且旧费用失效', response, valid, `status=${detail?.summary?.processStatus}, feeStatus=${detail?.summary?.feeStatus}`, OPTIONS) if (!valid) throw new Error('WAIT_PRICING 渠道变更事实未收敛') const listResponse = await context.request('GET', `/api/platform/inbounds?tenantId=${query(tenantId)}&inboundCode=${query(before.summary.inboundCode)}&processStatus=WAIT_PRICING&page=1&size=20`) const page = context.apiSuccess(MODULE, '按 WAIT_PRICING 筛选本轮样本', listResponse, '', OPTIONS) const visible = rows(page).some(row => id(row.id) === id(inboundId) && row.processStatus === 'WAIT_PRICING') context.assert(MODULE, '待计费筛选包含本轮真实样本', listResponse, visible, `inboundId=${inboundId}`, OPTIONS) if (!visible) throw new Error('WAIT_PRICING 列表未包含本轮样本') } finally { context.apiSuccess(MODULE, '退出渠道目录客户独立会话', await context.request('POST', '/api/auth/logout', { refreshToken: login.refreshToken }, token), '', OPTIONS) } } async function prepareInboundStatus(context, target) { const originalMarker = context.marker const originalRequest = context.request const fixtureMarker = `${originalMarker}-IN-${target}` const stop = Symbol(target) const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement', 'inboundWarehouseResources'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'orderStopAtApproved'] const createdSnapshot = new Map(createdKeys.map((name) => [name, { present: Object.hasOwn(context.createdResources, name), value: context.createdResources[name] }])) const contextSnapshot = new Map(contextKeys.map((name) => [name, { present: Object.hasOwn(context.context, name), value: context.context[name] }])) try { context.marker = fixtureMarker if (target === 'WAIT_PRICING') context.context.orderStopAtApproved = true const orders = await import(new URL('./orders.mjs', import.meta.url)) await orders.run(context) const order = { ...context.createdResources.orders } const inboundId = id(order.inboundId) const tenantId = id(order.tenantId) if (!inboundId || !tenantId) throw new Error(`${target} 未生成独立待测量入库单`) const detailPath = `/api/platform/inbounds/${query(inboundId)}?tenantId=${query(tenantId)}` if (target === 'MEASURING') { const weightOcr = await import(new URL('./inbound-weight-ocr.mjs', import.meta.url)) await weightOcr.run(context) } context.request = async (method, path, body, token, headers) => { const isSubmit = path.includes(`/inbounds/${inboundId}/measurement/submit`) if (['MEASURE_PENDING', 'FEE_CONFIRMING', 'WAIT_PRICING'].includes(target) && isSubmit) { const response = await originalRequest.call(context, method, path, body, token, headers) if (!ok(response)) throw new Error(`MEASURE_PENDING 提交失败: HTTP=${response?.http}, code=${response?.body?.code}`) throw stop } return originalRequest.call(context, method, path, body, token, headers) } if (target !== 'MEASURING') { try { const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url)) await measurement.run(context) } catch (error) { if (error !== stop) throw error } finally { context.request = originalRequest } } if (['FEE_CONFIRMING', 'WAIT_PRICING'].includes(target)) { await retainFeeConfirmingFixture(context, { inboundId, tenantId }) } if (target === 'WAIT_PRICING') { await retainWaitPricingFixture(context, { inboundId, tenantId, orderId: id(order.orderId) }) } let response = null let detail = null for (let attempt = 1; attempt <= 8; attempt += 1) { response = await originalRequest.call(context, 'GET', `/api/platform/inbounds/${query(inboundId)}?tenantId=${query(tenantId)}`) detail = response?.http === 200 && String(response?.body?.code) === '200' ? response.body.data : null if (detail?.summary?.processStatus === target || attempt === 8) break await new Promise((resolve) => setTimeout(resolve, 500 * attempt)) } detail = context.apiSuccess(MODULE, `回读 ${target} 独立入库单`, response, '', OPTIONS) context.assert(MODULE, `${target} 入库状态样本可从详情打开`, response, detail?.summary?.processStatus === target, `inboundId=${inboundId}, actual=${detail?.summary?.processStatus || 'missing'}`, OPTIONS) return { inboundId, orderId: id(order.orderId), tenantId, status: target, inboundCode: detail?.summary?.inboundCode || null } } finally { context.request = originalRequest context.marker = originalMarker for (const [name, snapshot] of createdSnapshot) { if (snapshot.present) context.createdResources[name] = snapshot.value else Reflect.deleteProperty(context.createdResources, name) } for (const [name, snapshot] of contextSnapshot) { if (snapshot.present) context.context[name] = snapshot.value else Reflect.deleteProperty(context.context, name) } } } export async function run(context, matrix = context.createdResources.manualStateMatrix) { if (!matrix) { matrix = { marker: context.marker, generatedAt: new Date().toISOString(), pages: {} } context.createdResources.manualStateMatrix = matrix } matrix.pages ||= {} const loginResponse = await context.request('POST', '/api/auth/login', { loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD }, '') const token = context.apiSuccess(MODULE, 'IT 测试客户管理员登录', loginResponse)?.accessToken if (!token) throw new Error('客户管理员登录未返回 accessToken') const fixture = await loadFixtureInputs(context, token) const result = { marker: context.marker, tenantId: fixture.tenantId, orders: {}, inbounds: {}, unifiedOrders: {} } matrix.pages.firstLegOrders = { requiredStatuses: ORDER_TARGETS.map(([status]) => status), fixtures: {} } matrix.pages.inbounds = { requiredStatuses: INBOUND_FILTER_STATUSES, fixtures: {} } matrix.pages.unifiedOrders = { requiredStatuses: UNIFIED_FILTER_STATUSES, fixtures: {} } for (const [target, actions] of ORDER_TARGETS) { const completed = target === 'COMPLETED' ? await createCompletedOrder(context) : null const activeFixture = completed ? { ...fixture, tenantId: completed.tenantId } : fixture const orderId = completed?.orderId || await createOrder(context, token, activeFixture, target) for (const action of actions) await transition(context, token, activeFixture, orderId, target, action) const { response, detail } = await readOrder(context, activeFixture, orderId) context.assert(MODULE, `${target} 订单状态样本可从详情打开`, response, id(detail?.order?.id) === orderId && detail?.order?.status === target, `orderId=${orderId}, status=${detail?.order?.status ?? 'missing'}`) result.orders[target] = { orderId, tenantId: activeFixture.tenantId, orderNo: detail?.order?.orderNo || null, status: target, listPath: `/admin/first-leg-orders?status=${target}` } matrix.pages.firstLegOrders.fixtures[target] = { id: orderId, tenantId: activeFixture.tenantId, status: target, detailPath: `/orders/${orderId}`, } if (actions.includes('approve')) { const inbound = await waitForInbound(context, fixture, orderId, target === 'COMPLETED') const inboundId = id(inbound.summary?.inboundId) context.assert(MODULE, `${target} 订单生成待测量入库单`, inbound.response, Boolean(inboundId), `orderId=${orderId}, inboundId=${inboundId || 'missing'}`) if (inboundId && !result.inbounds.WAIT_MEASURE) result.inbounds.WAIT_MEASURE = { inboundId, inboundCode: inbound.summary?.inboundCode || null, orderId, processStatus: 'WAIT_MEASURE', listPath: '/admin/inbounds?processStatus=WAIT_MEASURE', } } } for (const [status, sample] of Object.entries(result.orders)) { const response = await context.request('GET', `/api/platform/v1/orders/first-leg?tenantId=${query(sample.tenantId)}&status=${status}&keyword=${query(sample.orderNo)}&page=1&size=20`) const page = context.apiSuccess(MODULE, `${status} 订单可按列表状态筛选`, response) context.assert(MODULE, `${status} 筛选结果包含人工样本`, response, rows(page).some((item) => id(item.id) === sample.orderId), `orderId=${sample.orderId}, total=${page?.total ?? 'n/a'}`) } const waitMeasure = result.inbounds.WAIT_MEASURE if (waitMeasure) matrix.pages.inbounds.fixtures.WAIT_MEASURE = { id: waitMeasure.inboundId, status: 'WAIT_MEASURE', detailPath: `/orders/${waitMeasure.orderId}`, } const submitted = result.orders.SUBMITTED result.unifiedOrders.PENDING_REVIEW = { orderId: submitted.orderId, orderNo: submitted.orderNo, mainStatus: 'PENDING_REVIEW', listPath: '/admin/unified-orders?mainStatus=PENDING_REVIEW' } matrix.pages.unifiedOrders.fixtures.PENDING_REVIEW = { id: submitted.orderId, status: 'PENDING_REVIEW', detailPath: `/orders/${submitted.orderId}`, } if (waitMeasure) result.unifiedOrders.WAIT_MEASURE = { orderId: waitMeasure.orderId, inboundId: waitMeasure.inboundId, mainStatus: 'WAIT_MEASURE', listPath: '/admin/unified-orders?mainStatus=WAIT_MEASURE' } if (waitMeasure) matrix.pages.unifiedOrders.fixtures.WAIT_MEASURE = { id: waitMeasure.orderId, status: 'WAIT_MEASURE', detailPath: `/orders/${waitMeasure.orderId}`, } for (const status of ['MEASURING', 'MEASURE_PENDING', 'FEE_CONFIRMING', 'WAIT_PRICING', 'WAIT_SPLIT']) { const sample = await prepareInboundStatus(context, status) result.inbounds[status] = sample matrix.pages.inbounds.fixtures[status] = { id: sample.inboundId, status, detailPath: `/orders/${sample.orderId}`, } if (status === 'WAIT_SPLIT') { result.unifiedOrders.WAIT_SPLIT = sample matrix.pages.unifiedOrders.fixtures.WAIT_SPLIT = { id: sample.orderId, status, detailPath: `/orders/${sample.orderId}`, } } } if (!result.inbounds.WAIT_SPLIT) { throw new Error('本轮 WAIT_SPLIT 独立样本缺失,不允许借用历史数据') } context.addStep(MODULE, '登记订单/入库人工状态矩阵', null, true, `orders=${Object.keys(result.orders).length}, inbounds=${Object.keys(result.inbounds).length}`, OPTIONS) }