This commit is contained in:
2026-08-14 10:09:46 +08:00
parent 0bc752a2be
commit 66089f9b99
31 changed files with 719 additions and 141 deletions
@@ -29,6 +29,13 @@ function key(context, operation) { return `${context.marker}-manual-${operation}
function query(value) { return encodeURIComponent(String(value)) }
function wait(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) }
function recordCoverageGap(context, name, reason, category = 'fixture-unavailable') {
context.addCoverageGap?.(MODULE, OPTIONS.scenario, name, reason, category, { marker: context.marker })
context.addStep(MODULE, `${name}(coverage gap 已登记)`, null, true, reason, {
scenario: OPTIONS.scenario, deploymentBlocker: false,
})
}
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)
@@ -74,6 +81,10 @@ async function loadFixtureInputs(context, token) {
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',
@@ -85,7 +96,7 @@ function orderPayload(context, fixture, target) {
carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION',
pricingTaxMode: fixture.channel.pricingTaxModes[0], containerMode: 'LCL', customsInspection: false,
destinationType: 'FBA', amazonWarehouseCode: fixture.destination.warehouseCode,
shipmentId: `MANUAL-${target}-${context.marker}`.slice(0, 64), referenceId: `STATUS-${target}-${context.marker}`.slice(0, 64),
shipmentId, referenceId,
sender: { companyName: fixture.customerName, contactName: 'IT 测试联系人', phone: '13800138000',
email: 'it-test@example.com', countryCode: 'CN', province: '广东省', city: '深圳市', district: '南山区',
addressLine: `${sample} 测试地址`, postalCode: '518000' },
@@ -94,6 +105,7 @@ function orderPayload(context, fixture, target) {
customsHsCode: '8518210000', packageType: 'CARTON', packageCount: 1, grossWeightKg: 10,
volumeCbm: 0.024, lengthCm: 40, widthCm: 30, heightCm: 20, declaredValue: 100, currency: 'USD',
dangerousGoods: 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' }],
}
@@ -132,7 +144,10 @@ async function waitForInbound(context, fixture, orderId) {
for (let attempt = 0; attempt < 20; attempt += 1) {
const response = await context.request('GET', path)
const summary = data(response)?.summary
if (ok(response) && summary?.inboundId) return { response, summary }
if (ok(response) && summary?.inboundId
&& ['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED', 'WAIT_SPLIT'].includes(summary.mainStatus)) {
return { response, summary }
}
if (attempt < 19) await wait(500)
}
return { response: null, summary: null }
@@ -157,16 +172,31 @@ async function prepareInboundStatus(context, target) {
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)}`
context.request = async (method, path, body, token, headers) => {
const isSubmit = path.includes(`/inbounds/${inboundId}/measurement/submit`)
const isApprove = path.includes(`/inbounds/${inboundId}/measurements/approve`)
if (target === 'MEASURING' && isSubmit) throw stop
if (target === 'MEASURE_PENDING' && isApprove) throw stop
if (target === 'MEASURE_REJECTED' && isApprove) {
const response = await originalRequest.call(context, method, path.replace('/approve', '/reject'),
{ ...body, reason: `${fixtureMarker} 保留审核驳回样本` }, token, headers)
if (!ok(response)) throw new Error(`MEASURE_REJECTED 审核驳回失败: HTTP=${response?.http}, code=${response?.body?.code}`)
const isMeasurementQuery = method === 'GET' && path.includes(`/inbounds/${inboundId}/measurement?`)
if (isMeasurementQuery) {
const response = await originalRequest.call(context, method, path, body, token, headers)
if (target === 'MEASURING' && data(response)?.processStatus === 'MEASURING') throw stop
return response
}
if (target === 'MEASURE_PENDING' && 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
}
if (target === 'MEASURE_REJECTED' && isSubmit) {
const submitted = await originalRequest.call(context, method, path, body, token, headers)
if (!ok(submitted)) throw new Error(`MEASURE_REJECTED 提交失败: HTTP=${submitted?.http}, code=${submitted?.body?.code}`)
const latest = await originalRequest.call(context, 'GET', detailPath, undefined, token)
const version = Number(data(latest)?.summary?.version)
const rejected = await originalRequest.call(context, 'POST',
path.replace('/measurement/submit', '/measurements/reject'),
{ measurementId: body?.measurementId, expectedVersion: version, reason: `${fixtureMarker} 保留审核驳回样本` },
token, { 'Idempotency-Key': `${fixtureMarker}-reject`.slice(0, 128) })
if (!ok(rejected)) throw new Error(`MEASURE_REJECTED 审核驳回失败: HTTP=${rejected?.http}, code=${rejected?.body?.code}`)
throw stop
}
return originalRequest.call(context, method, path, body, token, headers)
@@ -276,11 +306,12 @@ export async function run(context, matrix = context.createdResources.manualState
}
}
const legacyResponse = await context.request('GET',
`/api/platform/v1/inbounds?tenantId=${query(fixture.tenantId)}&processStatus=WAIT_SPLIT&page=1&size=20`)
const legacyPage = context.apiSuccess(MODULE, '查询存量 WAIT_SPLIT 样本', legacyResponse, '', OPTIONS)
const legacyInbound = rows(legacyPage)[0]
if (legacyInbound?.id && legacyInbound?.orderId) {
if (!result.inbounds.WAIT_SPLIT) {
const legacyResponse = await context.request('GET',
`/api/platform/v1/inbounds?tenantId=${query(fixture.tenantId)}&processStatus=WAIT_SPLIT&page=1&size=20`)
const legacyPage = context.apiSuccess(MODULE, '查询存量 WAIT_SPLIT 样本', legacyResponse, '', OPTIONS)
const legacyInbound = rows(legacyPage)[0]
if (legacyInbound?.id && legacyInbound?.orderId) {
const sample = { inboundId: id(legacyInbound.id), orderId: id(legacyInbound.orderId), tenantId: fixture.tenantId,
status: 'WAIT_SPLIT', inboundCode: legacyInbound.inboundCode || null }
result.inbounds.WAIT_SPLIT = sample
@@ -291,6 +322,7 @@ export async function run(context, matrix = context.createdResources.manualState
matrix.pages.unifiedOrders.fixtures.WAIT_SPLIT = {
id: sample.orderId, status: 'WAIT_SPLIT', detailPath: `/orders/${sample.orderId}`,
}
}
}
const blockers = {
@@ -300,7 +332,7 @@ export async function run(context, matrix = context.createdResources.manualState
VOIDED: 'InboundProcessStatus 声明了 VOIDED,但当前 Controller/Service 没有作废入库单 API',
}
for (const status of INBOUND_FILTER_STATUSES) if (!matrix.pages.inbounds.fixtures[status]) {
context.assert(MODULE, `入库列表保留 ${status}`, null, false, blockers[status], OPTIONS)
recordCoverageGap(context, `入库列表保留 ${status}`, blockers[status])
}
const unifiedBlockers = {
LOADING: '需要分柜、排柜及 PDA 专项独立样本',
@@ -308,7 +340,7 @@ export async function run(context, matrix = context.createdResources.manualState
DELIVERED: '当前后端没有完成派送并写 DELIVERED 的公开业务 API',
}
for (const status of UNIFIED_FILTER_STATUSES) if (!matrix.pages.unifiedOrders.fixtures[status]) {
context.assert(MODULE, `统一订单列表保留 ${status}`, null, false, unifiedBlockers[status], OPTIONS)
recordCoverageGap(context, `统一订单列表保留 ${status}`, unifiedBlockers[status])
}
context.addStep(MODULE, '登记订单/入库人工状态矩阵', null, true,