Files
2026-10-21 21:30:24 +08:00

475 lines
29 KiB
JavaScript

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)
}