This commit is contained in:
2026-10-08 17:07:57 +08:00
parent 16d2d9111a
commit c1ddb658bd
111 changed files with 2914 additions and 941 deletions
@@ -1,4 +1,4 @@
import { findPricedOrderRouteAcrossOrigins, freezeOrderCommand } from './orders.mjs'
import { findPricedOrderRouteAcrossOrigins, findPricedOrderRoute, freezeOrderCommand } from './orders.mjs'
const MODULE = 'order-inbound-status-fixtures'
const OPTIONS = { scenario: 'manual-order-inbound-status-matrix', deploymentBlocker: true }
@@ -178,19 +178,143 @@ async function waitForInbound(context, fixture, orderId, allowCompletedOrder = f
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']
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 }
@@ -206,7 +330,7 @@ async function prepareInboundStatus(context, target) {
context.request = async (method, path, body, token, headers) => {
const isSubmit = path.includes(`/inbounds/${inboundId}/measurement/submit`)
if (target === 'MEASURE_PENDING' && isSubmit) {
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
@@ -224,6 +348,13 @@ async function prepareInboundStatus(context, target) {
}
}
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) {
@@ -320,7 +451,7 @@ export async function run(context, matrix = context.createdResources.manualState
id: waitMeasure.orderId, status: 'WAIT_MEASURE', detailPath: `/orders/${waitMeasure.orderId}`,
}
for (const status of ['MEASURING', 'MEASURE_PENDING', 'WAIT_SPLIT']) {
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] = {
@@ -335,22 +466,7 @@ export async function run(context, matrix = context.createdResources.manualState
}
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
result.unifiedOrders.WAIT_SPLIT = sample
matrix.pages.inbounds.fixtures.WAIT_SPLIT = {
id: sample.inboundId, status: 'WAIT_SPLIT', detailPath: `/orders/${sample.orderId}`,
}
matrix.pages.unifiedOrders.fixtures.WAIT_SPLIT = {
id: sample.orderId, status: 'WAIT_SPLIT', detailPath: `/orders/${sample.orderId}`,
}
}
throw new Error('本轮 WAIT_SPLIT 独立样本缺失,不允许借用历史数据')
}
context.addStep(MODULE, '登记订单/入库人工状态矩阵', null, true,