const MODULE = 'orders' const CONTRACT_OPTIONS = { scenario: 'isolated-order-chain', deploymentBlocker: true } const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin' const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!' const BOXES = [ { boxType: 'SMALL', lengthCm: 30, widthCm: 20, heightCm: 20 }, { boxType: 'MEDIUM', lengthCm: 40, widthCm: 30, heightCm: 30 }, { boxType: 'LARGE', lengthCm: 50, widthCm: 40, heightCm: 40 }, { boxType: 'EXTRA_LARGE', lengthCm: 60, widthCm: 50, heightCm: 50 }, ] const MEASURABLE_STATUSES = new Set(['WAIT_MEASURE', 'MEASURING', 'MEASURE_REJECTED']) const PRICING_PROBE_CONCURRENCY = 6 const ORDER_ESTIMATE_WEIGHT_KG = BOXES.reduce((total, _box, index) => total + 10 + index, 0) const ORDER_ESTIMATE_VOLUME_CBM = BOXES.reduce((total, box) => total + box.lengthCm * box.widthCm * box.heightCm / 1_000_000, 0) function id(value) { return value == null ? '' : String(value) } function records(value) { return Array.isArray(value?.records) ? value.records : [] } function pageOk(value) { return Array.isArray(value?.records) && Number.isFinite(Number(value?.total)) } function success(response) { return response?.http === 200 && String(response.body?.code) === '200' } function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) } function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) } export function approvedInboundReady(orderDetail, taskResponse, tenantId, orderId) { const summary = orderDetail?.summary const task = success(taskResponse) ? taskResponse.body?.data : null return Boolean(summary?.inboundId) && MEASURABLE_STATUSES.has(summary?.mainStatus) && id(task?.orderId) === id(orderId) && id(task?.tenantId) === id(tenantId) } function tomorrow(days = 30) { const date = new Date(); date.setUTCDate(date.getUTCDate() + days) return date.toISOString().slice(0, 10) } function pricingEstimateRequest(receiving, channel, pricingTaxMode, destination) { return { originWarehouseId: receiving.id, channelCode: channel.channelCode, destinationCode: destination.warehouseCode, destinationType: 'FBA', destinationWarehouseId: destination.id, destinationName: destination.warehouseName, pricingTaxMode, actualWeightKg: ORDER_ESTIMATE_WEIGHT_KG, actualVolumeCbm: ORDER_ESTIMATE_VOLUME_CBM, } } function estimateMatches(response, candidate) { const estimate = success(response) ? response.body?.data : null return Boolean(estimate) && id(estimate.priceBookVersionId) === id(candidate.channel.priceBookVersionId) && id(estimate.channelCode) === id(candidate.channel.channelCode) && id(estimate.pricingTaxMode) === id(candidate.pricingTaxMode) } export async function findPricedOrderRoute({ receiving, destinations, channels, estimate, concurrency = PRICING_PROBE_CONCURRENCY }) { const candidates = [] for (const channel of channels) { if (!channel?.priceBookVersionId || !channel?.channelCode || !Array.isArray(channel.pricingTaxModes)) continue for (const pricingTaxMode of channel.pricingTaxModes) { if (!pricingTaxMode) continue for (const destination of destinations) { if (!destination?.id || !destination?.warehouseCode) continue candidates.push({ channel, pricingTaxMode, destination, request: pricingEstimateRequest(receiving, channel, pricingTaxMode, destination) }) } } } const failures = [] let lastResponse = null const batchSize = Math.max(1, Math.min(Number(concurrency) || 1, PRICING_PROBE_CONCURRENCY)) let attempts = 0 for (let offset = 0; offset < candidates.length; offset += batchSize) { const batch = candidates.slice(offset, offset + batchSize) const results = await Promise.all(batch.map(async (candidate) => ({ candidate, response: await estimate(candidate.request), }))) attempts += results.length lastResponse = results.at(-1)?.response || lastResponse const match = results.find(({ candidate, response }) => estimateMatches(response, candidate)) if (match) return { priced: match.candidate, response: match.response, attempts, failures } for (const { candidate, response } of results) { const returned = success(response) ? response.body?.data : null failures.push({ tuple: `${candidate.channel.channelCode}/${candidate.pricingTaxMode}/${candidate.destination.warehouseCode}`, http: response?.http ?? 'missing', code: response?.body?.code ?? 'missing', requestId: response?.requestId ?? 'missing', traceId: response?.traceId ?? 'missing', mismatch: returned ? `version=${id(returned.priceBookVersionId) || 'missing'},channel=${id(returned.channelCode) || 'missing'},tax=${id(returned.pricingTaxMode) || 'missing'}` : null, }) } } return { priced: null, response: lastResponse, attempts, failures } } export async function findPricedOrderRouteAcrossOrigins({ receivings, destinations, loadChannels, estimate, concurrency = PRICING_PROBE_CONCURRENCY }) { const originFailures = [] let attempts = 0 let response = null for (const receiving of receivings) { if (!receiving?.id) continue const channelResponse = await loadChannels(receiving) response = channelResponse || response const channels = success(channelResponse) && Array.isArray(channelResponse.body?.data) ? channelResponse.body.data : [] const priceableChannels = channels.filter((item) => Array.isArray(item.pricingTaxModes) && item.pricingTaxModes.length > 0) if (priceableChannels.length === 0) { originFailures.push({ originWarehouseId: id(receiving.id), stage: 'channels', http: channelResponse?.http ?? 'missing', code: channelResponse?.body?.code ?? 'missing', requestId: channelResponse?.requestId ?? 'missing', traceId: channelResponse?.traceId ?? 'missing', count: channels.length }) continue } const result = await findPricedOrderRoute({ receiving, destinations, channels: priceableChannels, estimate, concurrency }) attempts += result.attempts response = result.response || response if (result.priced) return { receiving, ...result, originFailures } originFailures.push({ originWarehouseId: id(receiving.id), stage: 'estimate', attempts: result.attempts, evidence: pricingFailureEvidence(result) }) } return { receiving: null, priced: null, response, attempts, originFailures } } function pricingFailureEvidence(result) { const describe = (failure) => failure ? `${failure.tuple}[HTTP=${failure.http},code=${failure.code},requestId=${failure.requestId},traceId=${failure.traceId}${failure.mismatch ? `,response=${failure.mismatch}` : ''}]` : 'missing' return `没有可计价组合;attempts=${result.attempts}, first=${describe(result.failures[0])}, last=${describe(result.failures.at(-1))}` } function originPricingFailureEvidence(result) { const describe = (failure) => !failure ? 'missing' : failure.stage === 'channels' ? `origin=${failure.originWarehouseId},channels[HTTP=${failure.http},code=${failure.code},count=${failure.count},requestId=${failure.requestId},traceId=${failure.traceId}]` : `origin=${failure.originWarehouseId},estimate[attempts=${failure.attempts},${failure.evidence}]` return `没有 ACTIVE 国内仓可计价组合;origins=${result.originFailures?.length ?? 0}, attempts=${result.attempts}, first=${describe(result.originFailures?.[0])}, last=${describe(result.originFailures?.at(-1))}` } async function request(context, method, path, body, token, extraHeaders = {}) { const headers = { Accept: 'application/json', Authorization: `Bearer ${token}`, ...extraHeaders } let payload = body if (body !== undefined && !(body instanceof FormData)) { headers['Content-Type'] = 'application/json; charset=utf-8' payload = JSON.stringify(body) } try { const response = await fetch(`${context.options.gateway}${path}`, { method, headers, body: payload, signal: AbortSignal.timeout(30_000), }) const content = await response.text() let parsed = null try { parsed = content ? JSON.parse(content) : null } catch { /* Recorded as a bounded raw response. */ } return { method, path, http: response.status, body: parsed, raw: parsed === null ? content.slice(0, 500) : null, 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 uploadProductImage(context, merchantToken) { const form = new FormData() const png = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64') form.append('file', new Blob([png], { type: 'image/png' }), `product-${context.marker}.png`) const response = await request(context, 'POST', '/api/merchant/v1/files/product-images', form, merchantToken) const data = context.apiSuccess(MODULE, 'Client 上传产品图片', response, 'contentType=image/png', CONTRACT_OPTIONS) context.assert(MODULE, '产品图片上传返回字符串文件 ID', response, Boolean(data?.fileId) && typeof data.fileId === 'string' && data.contentType === 'image/png', `fileIdType=${typeof data?.fileId}, contentType=${data?.contentType ?? 'missing'}`, CONTRACT_OPTIONS) return id(data?.fileId) } async function assertProductImageDownload(context, merchantToken, fileId) { const path = `/api/merchant/v1/files/product-images/${encodeURIComponent(fileId)}` try { const response = await fetch(`${context.options.gateway}${path}`, { headers: { Accept: 'image/*', Authorization: `Bearer ${merchantToken}` }, signal: AbortSignal.timeout(30_000), }) const bytes = await response.arrayBuffer() const evidence = { method: 'GET', path, http: response.status, body: null, raw: null, requestId: response.headers.get('x-request-id'), traceId: response.headers.get('x-trace-id') } context.assert(MODULE, 'Client 可下载本轮产品图片', evidence, response.status === 200 && bytes.byteLength > 0 && (response.headers.get('content-type') || '').startsWith('image/'), `bytes=${bytes.byteLength}, contentType=${response.headers.get('content-type') || 'missing'}`, CONTRACT_OPTIONS) } catch (error) { context.assert(MODULE, 'Client 可下载本轮产品图片', null, false, String(error), CONTRACT_OPTIONS) } } async function loadMasterData(context, merchantToken) { const domesticResponse = await request(context, 'GET', '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', undefined, merchantToken) const domesticPage = context.apiSuccess(MODULE, 'Client 查询国内收货仓', domesticResponse) context.assert(MODULE, '存在可用国内收货仓', domesticResponse, records(domesticPage).length > 0, `count=${records(domesticPage).length}`) const fbaResponse = await request(context, 'GET', '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA', undefined, merchantToken) const fbaPage = context.apiSuccess(MODULE, 'Client 查询 FBA 目的仓', fbaResponse) context.assert(MODULE, '存在可用 FBA 目的仓', fbaResponse, records(fbaPage).length > 0, `count=${records(fbaPage).length}`) const receivings = records(domesticPage).filter((warehouse) => warehouse?.status === 'ACTIVE') const destinations = records(fbaPage) if (receivings.length === 0 || destinations.length === 0) throw new Error('缺少创建测试订单所需的收货仓或目的仓') const pricingResult = await findPricedOrderRouteAcrossOrigins({ receivings, destinations, loadChannels: (receiving) => request(context, 'GET', `/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`, undefined, merchantToken), estimate: (body) => request(context, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', body, merchantToken), }) const priced = pricingResult.priced const receiving = pricingResult.receiving context.addStep(MODULE, 'Client 从 ACTIVE 国内仓选择具有真实价格覆盖的渠道和目的仓', pricingResult.response, Boolean(priced), priced ? `origin=${receiving.id}, priceBookVersionId=${priced.channel.priceBookVersionId}, channel=${priced.channel.channelCode}, pricingTaxMode=${priced.pricingTaxMode}, destination=${priced.destination.warehouseCode}, attempts=${pricingResult.attempts}` : originPricingFailureEvidence(pricingResult), CONTRACT_OPTIONS) if (!priced) throw new Error('当前 ACTIVE 国内收货仓、渠道、报价税制和FBA目的仓没有可计价组合') const partnerResponse = await request(context, 'GET', '/api/merchant/v1/partners/candidates?type=SHIPPER', undefined, merchantToken) const partners = context.apiSuccess(MODULE, 'Client 查询发货客户候选', partnerResponse) const partner = Array.isArray(partners) ? partners.find((item) => item.status === 'ACTIVE' && item.types?.includes?.('SHIPPER')) : null return { receiving, destination: priced.destination, channel: priced.channel, pricingTaxMode: priced.pricingTaxMode, partner } } function buildPayload(context, profile, master, productImageFileId) { const shipmentId = `SHIP-${context.marker}`.slice(0, 64) const referenceId = `REF-${context.marker}`.slice(0, 64) const companyName = profile.tenantName || `订单验收客户-${context.marker}` return { customerId: master.partner?.id ?? null, customerName: master.partner?.partnerName ?? companyName, transportMode: 'SEA', tradeTerm: 'FOB', destinationCountryCode: master.destination.countryCode || 'US', destinationWarehouseId: master.destination.id, destinationWarehouseName: master.destination.warehouseName, expectedShipDate: tomorrow(), customerRemark: `${context.marker} Client 创建隔离订单`, pickupMethod: 'PICKUP', receivingWarehouseId: master.receiving.id, receivingWarehouseName: master.receiving.warehouseName, channelCode: master.channel.channelCode, channelName: master.channel.channelName, carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION', pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL', customsInspection: false, destinationType: 'FBA', amazonWarehouseCode: master.destination.warehouseCode, shipmentId, referenceId, sender: { companyName, contactName: '订单验收联系人', phone: '13800138000', email: 'order-acceptance@example.com', countryCode: 'CN', province: '广东省', city: '深圳市', district: '南山区', addressLine: `${context.marker} 测试地址`, postalCode: '518000', }, receiver: null, cargoes: BOXES.map((box, index) => { const volumeCbm = box.lengthCm * box.widthCm * box.heightCm / 1_000_000 const unitsPerPackage = 10 + index const packageCount = Number(context.context?.phase3LoadingIncrementalPackageCount) === 2 ? 2 : 1 return { skuNo: `SKU-${box.boxType}-${context.marker}`.slice(0, 64), cargoName: `${box.boxType} 测试产品`, cargoNameEn: `${box.boxType} test product`, hsCode: '8518210000', customsHsCode: '8518210000', packageType: 'CARTON', lengthCm: box.lengthCm, widthCm: box.widthCm, heightCm: box.heightCm, unitsPerPackage, packageCount, grossWeightKg: (10 + index) * packageCount, volumeCbm: volumeCbm * packageCount, totalQuantity: unitsPerPackage * packageCount, netWeightKg: (9 + index) * packageCount, unitDeclaredValue: 8.5, declaredValue: unitsPerPackage * packageCount * 8.5, currency: 'USD', material: '塑料 / Plastic', usage: '接口验收 / API acceptance', model: `MODEL-${box.boxType}`, brand: 'TEST-NO-LOGO', productLengthCm: 12 + index, productWidthCm: 8 + index, productHeightCm: 6 + index, shipmentId, referenceId, upcNo: `${Date.now()}${index}`.slice(-12), productImageFileId, dangerousGoods: false, cargoRemark: `${context.marker} boxType=${box.boxType}`, } }), } } function assertCargoContract(context, response, cargoes, payload) { const complete = Array.isArray(cargoes) && cargoes.length === BOXES.length && BOXES.every((box, index) => { const actual = cargoes[index]; const expected = payload.cargoes[index] return Number(actual?.lengthCm) === box.lengthCm && Number(actual?.widthCm) === box.widthCm && Number(actual?.heightCm) === box.heightCm && id(actual?.productImageFileId) === id(expected.productImageFileId) && actual?.skuNo === expected.skuNo && actual?.cargoName === expected.cargoName && actual?.cargoNameEn === expected.cargoNameEn && actual?.customsHsCode === expected.customsHsCode && Number(actual?.unitsPerPackage) === expected.unitsPerPackage && Number(actual?.packageCount) === expected.packageCount && Number(actual?.totalQuantity) === expected.totalQuantity && Number(actual?.netWeightKg) === expected.netWeightKg && Number(actual?.unitDeclaredValue) === expected.unitDeclaredValue && actual?.material === expected.material && actual?.productUsage === expected.usage && actual?.model === expected.model && actual?.brand === expected.brand && Number(actual?.productLengthCm) === expected.productLengthCm && Number(actual?.productWidthCm) === expected.productWidthCm && Number(actual?.productHeightCm) === expected.productHeightCm && actual?.shipmentId === expected.shipmentId && actual?.referenceId === expected.referenceId && actual?.upcNo === expected.upcNo && actual?.cargoRemark.includes(context.marker) }) context.assert(MODULE, '四箱型产品字段及图片契约完整回显', response, complete, `cargoes=${Array.isArray(cargoes) ? cargoes.length : 'n/a'}, imageFileId=${payload.cargoes[0].productImageFileId}`, CONTRACT_OPTIONS) } async function assertDimensionConfig(context, tenantId) { const path = `/api/platform/v1/first-leg/customer-dimension-config?tenantId=${encodeURIComponent(tenantId)}` const response = await context.request('GET', path) const data = context.apiSuccess(MODULE, 'Admin 查询客户固定箱型', response, `tenantId=${tenantId}`, CONTRACT_OPTIONS) const rules = Array.isArray(data?.rules) ? data.rules : [] const exact = BOXES.every((box, index) => { const rule = rules[index] return rule?.boxType === box.boxType && Number(rule.innerLengthCm) === box.lengthCm && Number(rule.innerWidthCm) === box.widthCm && Number(rule.innerHeightCm) === box.heightCm && Number(rule.edgeAdditionCm) === 2 }) context.assert(MODULE, '固定四箱型顺序、内径及每边加2cm正确', response, rules.length === BOXES.length && exact, `types=${rules.map((rule) => rule.boxType).join(',') || 'missing'}`, CONTRACT_OPTIONS) } async function waitForInbound(context, tenantId, orderId) { const path = `/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}` const taskPath = `/api/platform/v1/first-leg/tasks/by-order/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}` let response = null; let detail = null; let taskResponse = null; let task = null for (let attempt = 1; attempt <= 30; attempt += 1) { response = await context.request('GET', path) const candidate = success(response) ? response.body?.data : null if (candidate?.summary?.inboundId) { taskResponse = await context.request('GET', taskPath) } if (approvedInboundReady(candidate, taskResponse, tenantId, orderId)) { detail = candidate task = taskResponse.body.data break } if (attempt < 30) await sleep(1_000) } context.addStep(MODULE, '审核后入库单与履约任务异步收敛', response, Boolean(detail && task), detail ? `inboundId=${detail.summary.inboundId}, status=${detail.summary.mainStatus}, taskId=${id(task?.id)}` : `30秒内未收敛;status=${response?.body?.data?.summary?.mainStatus ?? 'missing'}, taskCode=${taskResponse?.body?.code ?? 'not-requested'}`, CONTRACT_OPTIONS) return { response, detail, taskResponse, task } } export async function run(context) { const loginResponse = await context.request('POST', '/api/auth/login', { username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD, }, '') const login = context.apiSuccess(MODULE, 'Client 测试账号登录', loginResponse, `username=${MERCHANT_USERNAME}`) const merchantToken = login?.accessToken if (!merchantToken) throw new Error('Client 登录未返回 accessToken') const profileResponse = await request(context, 'GET', '/api/merchant/v1/iam/tenant/profile', undefined, merchantToken) const profile = context.apiSuccess(MODULE, 'Client 查询当前企业资料', profileResponse) const tenantId = id(profile?.id) context.assert(MODULE, 'Client 测试企业已审核且客户 ID 有效', profileResponse, Boolean(tenantId) && profile?.status === 'ACTIVE', `tenantId=${tenantId || 'missing'}, status=${profile?.status ?? 'missing'}`) if (!tenantId) throw new Error('Client 企业资料未返回客户 ID') await assertDimensionConfig(context, tenantId) const master = await loadMasterData(context, merchantToken) const productImageFileId = await uploadProductImage(context, merchantToken) if (!productImageFileId) throw new Error('产品图片新契约未部署,无法创建带图片的隔离订单') await assertProductImageDownload(context, merchantToken, productImageFileId) const payload = buildPayload(context, profile, master, productImageFileId) const createPath = '/api/merchant/v1/orders/first-leg' const createResponse = await request(context, 'POST', createPath, payload, merchantToken, { 'Idempotency-Key': key(context, 'client-create') }) const orderId = id(context.apiSuccess(MODULE, 'Client 创建四箱型隔离订单', createResponse, `marker=${context.marker}`, CONTRACT_OPTIONS)) if (!orderId) throw new Error('Client 订单创建失败,后续 Admin 链路停止') context.createdResources.orders = { marker: context.marker, tenantId, orderId, productImageFileId, boxTypes: BOXES.map((box) => box.boxType), inboundId: null, receivingWarehouseId: id(master.receiving.id), } const replayResponse = await request(context, 'POST', createPath, payload, merchantToken, { 'Idempotency-Key': key(context, 'client-create') }) const replayedId = id(context.apiSuccess(MODULE, 'Client 创建订单幂等重放', replayResponse)) context.assert(MODULE, 'Client 创建幂等结果一致', replayResponse, replayedId === orderId, `orderId=${orderId}, replayedId=${replayedId}`) const clientDetailPath = `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}` const draftResponse = await request(context, 'GET', clientDetailPath, undefined, merchantToken) const draft = context.apiSuccess(MODULE, 'Client 查询隔离订单草稿', draftResponse) const orderNo = id(draft?.order?.orderNo) context.assert(MODULE, 'Client 草稿身份、marker 和状态正确', draftResponse, id(draft?.order?.id) === orderId && draft?.order?.status === 'DRAFT' && String(draft?.order?.customerRemark || '').includes(context.marker), `orderNo=${orderNo || 'missing'}, status=${draft?.order?.status ?? 'missing'}`) assertCargoContract(context, draftResponse, draft?.cargoes, payload) context.createdResources.orders.orderNo = orderNo const submitResponse = await request(context, 'POST', `${clientDetailPath}/submit`, undefined, merchantToken, { 'Idempotency-Key': key(context, 'client-submit') }) context.apiSuccess(MODULE, 'Client 提交隔离订单', submitResponse, `orderId=${orderId}`) const submittedResponse = await request(context, 'GET', clientDetailPath, undefined, merchantToken) const submitted = context.apiSuccess(MODULE, 'Client 回查已提交订单', submittedResponse) context.assert(MODULE, 'Client 订单已进入待审核状态', submittedResponse, submitted?.order?.status === 'SUBMITTED', `status=${submitted?.order?.status ?? 'missing'}`) const unifiedListResponse = await context.request('GET', `/api/platform/v1/orders?page=1&size=100&tenantId=${encodeURIComponent(tenantId)}&orderNo=${encodeURIComponent(orderNo)}`) const unifiedPage = context.apiSuccess(MODULE, 'Admin 查询统一订单列表', unifiedListResponse) const unifiedRow = records(unifiedPage).find((row) => id(row.orderId) === orderId) context.assert(MODULE, 'Admin 统一订单列表只核对本轮订单', unifiedListResponse, pageOk(unifiedPage) && Boolean(unifiedRow), `orderId=${orderId}, found=${Boolean(unifiedRow)}`) const unifiedDetailPath = `/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}` const unifiedDetailResponse = await context.request('GET', unifiedDetailPath) const unifiedDetail = context.apiSuccess(MODULE, 'Admin 查询统一订单详情', unifiedDetailResponse) context.assert(MODULE, 'Admin 统一订单详情身份与待审核状态正确', unifiedDetailResponse, id(unifiedDetail?.summary?.orderId) === orderId && unifiedDetail?.summary?.mainStatus === 'PENDING_REVIEW' && Array.isArray(unifiedDetail?.timeline), `mainStatus=${unifiedDetail?.summary?.mainStatus ?? 'missing'}, timeline=${unifiedDetail?.timeline?.length ?? 'n/a'}`) const firstLegListResponse = await context.request('GET', `/api/platform/v1/orders/first-leg?tenantId=${encodeURIComponent(tenantId)}&keyword=${encodeURIComponent(orderNo)}&page=1&size=100`) const firstLegPage = context.apiSuccess(MODULE, 'Admin 查询头程订单列表', firstLegListResponse) const firstLegRow = records(firstLegPage).find((row) => id(row.id) === orderId) context.assert(MODULE, 'Admin 头程订单列表只核对本轮订单', firstLegListResponse, pageOk(firstLegPage) && Boolean(firstLegRow), `orderId=${orderId}, found=${Boolean(firstLegRow)}`) const firstLegDetailPath = `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}` const firstLegDetailResponse = await context.request('GET', firstLegDetailPath) const firstLegDetail = context.apiSuccess(MODULE, 'Admin 查询头程订单详情', firstLegDetailResponse) context.assert(MODULE, 'Admin 头程订单详情聚合结构完整', firstLegDetailResponse, id(firstLegDetail?.order?.id) === orderId && Array.isArray(firstLegDetail?.cargoes) && Array.isArray(firstLegDetail?.parties) && Array.isArray(firstLegDetail?.internalNotes) && Array.isArray(firstLegDetail?.statusHistory), `cargoes=${firstLegDetail?.cargoes?.length ?? 'n/a'}, history=${firstLegDetail?.statusHistory?.length ?? 'n/a'}`) assertCargoContract(context, firstLegDetailResponse, firstLegDetail?.cargoes, payload) const noteResponse = await context.request('POST', `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/internal-notes?tenantId=${encodeURIComponent(tenantId)}`, { content: `${context.marker} Admin 订单链路验收` }) context.apiSuccess(MODULE, 'Admin 为本轮订单添加内部备注', noteResponse) const approveResponse = await context.request('POST', `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/approve?tenantId=${encodeURIComponent(tenantId)}`) context.apiSuccess(MODULE, 'Admin 审核通过本轮订单', approveResponse, `orderId=${orderId}`) const inboundResult = await waitForInbound(context, tenantId, orderId) const inboundId = id(inboundResult.detail?.summary?.inboundId) if (!inboundId || !inboundResult.task) { throw new Error('订单审核后的入库单与履约任务未在30秒内收敛,无法向 OCR/墨家模块交接') } const inboundPath = `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}` const inboundResponse = await context.request('GET', inboundPath) const inbound = context.apiSuccess(MODULE, 'Admin 查询本轮入库单详情', inboundResponse, `inboundId=${inboundId}`, CONTRACT_OPTIONS) const markerValues = [ inbound?.publicRemark, inbound?.summary?.orderNo, inbound?.summary?.inboundCode, ...(Array.isArray(inbound?.internalNotes) ? inbound.internalNotes.map((note) => note.content) : []), ] const markerFound = markerValues.some((value) => String(value || '').includes(context.marker)) const measurable = MEASURABLE_STATUSES.has(inbound?.summary?.processStatus) context.assert(MODULE, '隔离入库单 marker 来源与可测量状态正确', inboundResponse, id(inbound?.summary?.id) === inboundId && id(inbound?.summary?.tenantId) === tenantId && markerFound && measurable, `markerFound=${markerFound}, status=${inbound?.summary?.processStatus ?? 'missing'}`, CONTRACT_OPTIONS) context.assert(MODULE, '入库单详情包含托盘库位、文件、备注和状态历史', inboundResponse, Array.isArray(inbound?.palletLocations) && Array.isArray(inbound?.files) && Array.isArray(inbound?.internalNotes) && Array.isArray(inbound?.statusHistory), `inboundId=${inboundId}, history=${inbound?.statusHistory?.length ?? 'n/a'}`) const inboundListResponse = await context.request('GET', `/api/platform/inbounds?tenantId=${encodeURIComponent(tenantId)}&inboundCode=${encodeURIComponent(inbound.summary.inboundCode)}&page=1&size=100`) const inboundPage = context.apiSuccess(MODULE, 'Admin 查询入库单列表', inboundListResponse) context.assert(MODULE, 'Admin 入库单列表只核对本轮入库单', inboundListResponse, pageOk(inboundPage) && records(inboundPage).some((row) => id(row.id) === inboundId), `inboundId=${inboundId}, total=${inboundPage?.total ?? 'n/a'}`) context.createdResources.orders.inboundId = inboundId context.createdResources.orders.inboundCode = inbound.summary.inboundCode context.createdResources.orders.customerRemark = payload.customerRemark context.createdResources.orders.internalRemark = `${context.marker} Admin 订单链路验收` context.createdResources.orders.cargoRemark = payload.cargoes?.[0]?.cargoRemark || null context.createdResources.orders.processStatus = inbound.summary.processStatus context.createdResources.ocrInbound = { tenantId, inboundId, marker: context.marker } context.createdResources.testInbound = { tenantId, inboundId, marker: context.marker } context.context.inboundWeightOcr = { tenantId, inboundId, marker: context.marker } context.context.ocrInbound = context.context.inboundWeightOcr context.context.testInbound = context.context.inboundWeightOcr context.context.createdInbound = context.context.inboundWeightOcr const taskResponse = inboundResult.taskResponse const task = inboundResult.task context.apiSuccess(MODULE, 'Admin 查询本轮订单履约任务', taskResponse, '审批 Outbox 收敛后应生成可查询的履约任务', CONTRACT_OPTIONS) context.assert(MODULE, '履约任务与本轮订单和客户一致', taskResponse, id(task?.orderId) === orderId && id(task?.tenantId) === tenantId, `taskId=${id(task?.id) || 'missing'}, status=${task?.status ?? 'missing'}`, CONTRACT_OPTIONS) if (task?.id) { context.createdResources.orders.taskId = id(task.id) const historyResponse = await context.request('GET', `/api/platform/v1/first-leg/tasks/${encodeURIComponent(task.id)}/assignment-history?tenantId=${encodeURIComponent(tenantId)}&page=1&size=20`) const history = context.apiSuccess(MODULE, 'Admin 查询本轮履约任务分配历史', historyResponse) context.assert(MODULE, '履约分配历史分页结构正确', historyResponse, pageOk(history), `taskId=${task.id}, total=${history?.total ?? 'n/a'}`) } const acceptResponse = await context.request('POST', `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/accept?tenantId=${encodeURIComponent(tenantId)}`) context.apiSuccess(MODULE, 'Admin 受理本轮订单', acceptResponse) const startResponse = await context.request('POST', `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}/start?tenantId=${encodeURIComponent(tenantId)}`) context.apiSuccess(MODULE, 'Admin 启动本轮订单履约', startResponse) const finalDetailResponse = await context.request('GET', firstLegDetailPath) const finalDetail = context.apiSuccess(MODULE, 'Admin 回查本轮订单履约状态', finalDetailResponse) context.assert(MODULE, '本轮订单已进入处理中且保留状态历史', finalDetailResponse, finalDetail?.order?.status === 'PROCESSING' && finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'APPROVED') && finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'ACCEPTED') && finalDetail?.statusHistory?.some?.((item) => item.toStatus === 'PROCESSING'), `status=${finalDetail?.order?.status ?? 'missing'}, history=${finalDetail?.statusHistory?.length ?? 'n/a'}`) }