2026-08-12 20:36:11 +08:00
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import fs from 'node:fs'
|
|
|
|
|
import path from 'node:path'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
2026-10-08 17:07:57 +08:00
|
|
|
import { createCompletedOrder, transition, retainFeeConfirmingFixture, retainWaitPricingFixture } from './order-inbound-status-fixtures.mjs'
|
|
|
|
|
|
|
|
|
|
test('待计费样本通过同维度真实渠道变更生成且退出独立会话', async () => {
|
|
|
|
|
const execute = async (override = {}) => {
|
|
|
|
|
const calls = []
|
|
|
|
|
let detailReads = 0
|
|
|
|
|
const order = { id: '9', version: 2, status: 'APPROVED', receivingWarehouseId: '10',
|
|
|
|
|
destinationWarehouseId: '11', amazonWarehouseCode: 'ABE4', destinationWarehouseName: 'ABE4',
|
|
|
|
|
destinationType: 'FBA', pricingTaxMode: 'TAX_INCLUDED', channelCode: 'OLD', customerRemark: 'current-run' }
|
|
|
|
|
const context = { marker: 'current-run', context: { merchantQualificationFixture: { username: 'fixture', password: 'fixture' } },
|
|
|
|
|
async request(method, url, body, token) {
|
|
|
|
|
calls.push({ method, url, body, token })
|
|
|
|
|
let data
|
|
|
|
|
if (url === '/api/platform/inbounds/8?tenantId=7') {
|
|
|
|
|
detailReads += 1
|
|
|
|
|
data = { summary: { id: '8', tenantId: '7', orderId: '9', inboundCode: 'IN-8',
|
|
|
|
|
processStatus: detailReads === 1 ? 'FEE_CONFIRMING' : 'WAIT_PRICING',
|
|
|
|
|
channelCode: 'NEW', feeStatus: 'UNPRICED', ...(detailReads === 1 ? override.before : override.after) } }
|
|
|
|
|
} else if (url.includes('/orders/first-leg/9')) data = { order: { ...order, ...override.order }, cargoes: [{ itemNo: 1 }] }
|
|
|
|
|
else if (url === '/api/auth/login') data = { accessToken: 'test-session', refreshToken: 'test-refresh' }
|
|
|
|
|
else if (url.includes('/tenant/profile')) data = { id: override.customerId || '7' }
|
|
|
|
|
else if (url.includes('/pricing/channels')) data = override.channels ?? [
|
|
|
|
|
{ channelCode: 'OLD', pricingTaxModes: ['TAX_INCLUDED'], priceBookVersionId: '12' },
|
|
|
|
|
{ channelCode: 'NEW', channelName: '真实新渠道', pricingTaxModes: ['TAX_INCLUDED'], priceBookVersionId: '13' } ]
|
|
|
|
|
else if (url.includes('/pricing/estimate')) data = { channelCode: 'NEW', pricingTaxMode: 'TAX_INCLUDED', priceBookVersionId: '13' }
|
|
|
|
|
else if (url.includes('/declaration')) data = { version: 3, channelChanged: true, ...override.changed }
|
|
|
|
|
else if (url.includes('processStatus=WAIT_PRICING')) data = { records: override.rows ?? [{ id: '8', processStatus: 'WAIT_PRICING' }] }
|
|
|
|
|
return { http: 200, body: { code: '200', data } }
|
|
|
|
|
}, apiSuccess: (_m, _n, r) => r.body.data, assert() {} }
|
|
|
|
|
try { await retainWaitPricingFixture(context, { inboundId: '8', tenantId: '7', orderId: '9' }) }
|
|
|
|
|
finally {
|
|
|
|
|
if (calls.some(row => row.url === '/api/auth/login')) assert.equal(calls.at(-1).url, '/api/auth/logout')
|
|
|
|
|
}
|
|
|
|
|
return calls
|
|
|
|
|
}
|
|
|
|
|
const calls = await execute()
|
|
|
|
|
const mutation = calls.find(row => row.method === 'PUT')
|
|
|
|
|
assert.equal(mutation.body.expectedVersion, 2)
|
|
|
|
|
assert.equal(mutation.body.channelCode, 'NEW')
|
|
|
|
|
assert.equal(calls.filter(row => row.method === 'PUT').length, 1)
|
|
|
|
|
assert.equal(calls.find(row => row.url.includes('/pricing/estimate')).body.originWarehouseId, '10')
|
|
|
|
|
for (const override of [{ before: { orderId: 'other' } }, { order: { status: 'PROCESSING' } },
|
|
|
|
|
{ customerId: 'other' }, { channels: [] }, { changed: { version: 9 } },
|
|
|
|
|
{ after: { tenantId: 'other' } }, { rows: [] }]) {
|
|
|
|
|
await assert.rejects(execute(override), /WAIT_PRICING/)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('待费用确认样本必须经真实试算并独立回查费用、详情和筛选', async () => {
|
|
|
|
|
const execute = async (override = {}) => {
|
|
|
|
|
const calls = []
|
|
|
|
|
let reads = 0
|
|
|
|
|
const fee = { feeId: '90', feeVersion: 1, feeStatus: 'CALCULATED', nextProcessStatus: 'FEE_CONFIRMING' }
|
|
|
|
|
const context = { marker: 'current-run',
|
|
|
|
|
async request(method, url, body, token, headers) {
|
|
|
|
|
calls.push({ method, url, body, headers })
|
|
|
|
|
let data
|
|
|
|
|
if (url === '/api/platform/inbounds/8?tenantId=7') {
|
|
|
|
|
reads += 1
|
|
|
|
|
data = { summary: { id: '8', tenantId: '7', inboundCode: 'IN-TEST-8',
|
|
|
|
|
processStatus: reads === 1 ? 'MEASURE_PENDING' : 'FEE_CONFIRMING',
|
|
|
|
|
measureReviewStatus: 'PENDING', ...(reads === 1 ? override.before : override.after) } }
|
|
|
|
|
} else if (url.includes('/fees/calculate')) data = { ...fee, ...override.calculated }
|
|
|
|
|
else if (url.includes('/fees/latest')) data = { ...fee, ...override.latest }
|
|
|
|
|
else data = { records: override.rows ?? [{ id: '8', processStatus: 'FEE_CONFIRMING' }] }
|
|
|
|
|
return { http: 200, body: { code: '200', data } }
|
|
|
|
|
}, apiSuccess: (_m, _n, response) => response.body.data,
|
|
|
|
|
assert() {} }
|
|
|
|
|
const result = await retainFeeConfirmingFixture(context, { inboundId: '8', tenantId: '7' })
|
|
|
|
|
return { calls, result }
|
|
|
|
|
}
|
|
|
|
|
const { calls, result } = await execute()
|
|
|
|
|
assert.deepEqual(result, { feeId: '90', feeVersion: 1 })
|
|
|
|
|
assert.equal(calls.length, 5)
|
|
|
|
|
const writes = calls.filter(row => row.method !== 'GET')
|
|
|
|
|
assert.equal(writes.length, 1)
|
|
|
|
|
assert.deepEqual(writes[0].body, { weightSource: 'ACTUAL', volumeSource: 'ACTUAL' })
|
|
|
|
|
assert.ok(writes[0].headers['Idempotency-Key'].startsWith('current-run-'))
|
|
|
|
|
assert.match(calls.at(-1).url, /inboundCode=IN-TEST-8&processStatus=FEE_CONFIRMING/)
|
|
|
|
|
for (const override of [
|
|
|
|
|
{ before: { tenantId: 'other' } }, { before: { processStatus: 'WAIT_SPLIT' } },
|
|
|
|
|
{ calculated: { nextProcessStatus: 'WAIT_SPLIT' } },
|
|
|
|
|
{ after: { processStatus: 'MEASURE_PENDING' } }, { after: { measureReviewStatus: 'APPROVED' } },
|
|
|
|
|
{ latest: { feeId: '91' } }, { latest: { feeStatus: 'SENT' } }, { rows: [] },
|
|
|
|
|
]) await assert.rejects(execute(override), /FEE_CONFIRMING/)
|
|
|
|
|
})
|
2026-10-02 23:14:24 +08:00
|
|
|
|
|
|
|
|
test('dynamic Platform cancellation freezes version and key while reject body stays unchanged',async()=>{
|
|
|
|
|
const calls=[]
|
|
|
|
|
const context={marker:'fixture',token:'admin',apiSuccess(_m,_n,r){assert.equal(r.body.code,'200');return r.body.data},
|
|
|
|
|
async request(method,path,body,token,headers){calls.push({method,path,body,token,headers});return {http:200,body:{code:'200',data:{order:{version:9}}}}}}
|
|
|
|
|
await transition(context,'merchant',{tenantId:'2'},'1','CANCELLED','cancel')
|
|
|
|
|
const command=calls.find(row=>row.method==='POST')
|
|
|
|
|
assert.equal(command.token,'admin');assert.equal(command.body.expectedVersion,9);assert.ok(command.headers['Idempotency-Key'])
|
|
|
|
|
calls.length=0
|
|
|
|
|
await transition(context,'merchant',{tenantId:'2'},'1','REJECTED','reject')
|
|
|
|
|
assert.equal(calls.length,1);assert.deepEqual(calls[0].body,{reason:'fixture 保留 REJECTED 人工样本'})
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-12 20:36:11 +08:00
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
|
|
|
const source = fs.readFileSync(path.join(here, 'order-inbound-status-fixtures.mjs'), 'utf8')
|
|
|
|
|
|
2026-10-03 06:26:33 +08:00
|
|
|
test('完成样本来自真实履约编排且缺证据或客户漂移均阻断', async () => {
|
|
|
|
|
const context = { context: { phase4ReceivingEvidenceReader() {} }, createdResources: { orders: { orderId: '11' } } }
|
|
|
|
|
const prepare = async scope => { scope.context.merchantQualificationFixture = { tenantId: '7' } }
|
|
|
|
|
const run = proof => async scope => {
|
|
|
|
|
assert.equal(scope.context.f40SignatureOrder, 'ROOT_FIRST')
|
|
|
|
|
assert.equal(scope.context.phase4ReceivingEvidenceReader, context.context.phase4ReceivingEvidenceReader)
|
|
|
|
|
scope.createdResources.phase5RootFulfillment = proof
|
|
|
|
|
}
|
|
|
|
|
assert.deepEqual(await createCompletedOrder(context, run({ finalStatus: 'VERIFIED', customerId: '7', rootOrderId: '99' }), prepare),
|
|
|
|
|
{ orderId: '99', tenantId: '7' })
|
|
|
|
|
assert.equal(context.createdResources.orders.orderId, '11')
|
|
|
|
|
assert.equal(context.createdResources.phase5RootFulfillment, undefined)
|
|
|
|
|
for (const proof of [{ finalStatus: 'HTTP_VERIFIED_OUTBOX_PENDING', customerId: '7', rootOrderId: '99' },
|
|
|
|
|
{ finalStatus: 'VERIFIED', customerId: '8', rootOrderId: '99' }]) {
|
|
|
|
|
await assert.rejects(createCompletedOrder(context, run(proof), prepare), /真实签收/)
|
|
|
|
|
}
|
|
|
|
|
assert.doesNotMatch(source, /'start', 'complete'/)
|
|
|
|
|
assert.match(source, /phase5-root-fulfillment\.mjs/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-12 20:36:11 +08:00
|
|
|
test('为头程订单列表八个真实状态分别创建独立样本', () => {
|
|
|
|
|
for (const status of ['DRAFT', 'SUBMITTED', 'REJECTED', 'APPROVED', 'ACCEPTED', 'PROCESSING', 'COMPLETED', 'CANCELLED']) {
|
|
|
|
|
assert.match(source, new RegExp(`\\['${status}', \\[`))
|
|
|
|
|
assert.match(source, new RegExp(`result\\.orders\\[target\\]`))
|
|
|
|
|
}
|
|
|
|
|
assert.match(source, /status=\$\{status\}/)
|
|
|
|
|
assert.match(source, /筛选结果包含人工样本/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('所有写入均走订单生命周期 HTTP API 且不清理保留数据', () => {
|
|
|
|
|
assert.match(source, /\/api\/merchant\/v1\/orders\/first-leg/)
|
|
|
|
|
assert.match(source, /\/api\/platform\/v1\/orders\/first-leg/)
|
|
|
|
|
assert.match(source, /'Idempotency-Key': key\(context,/)
|
|
|
|
|
assert.doesNotMatch(source, /\b(?:INSERT|UPDATE|DELETE|TRUNCATE)\b/i)
|
|
|
|
|
assert.doesNotMatch(source, /cleanup|remove|unlink/i)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-15 11:51:15 +08:00
|
|
|
test('状态矩阵复用订单共享的逐仓真实报价选择,不假设首仓或首税制', () => {
|
|
|
|
|
assert.match(source, /findPricedOrderRouteAcrossOrigins/)
|
|
|
|
|
assert.match(source, /loadChannels: \(receiving\)/)
|
|
|
|
|
assert.match(source, /pricing\/estimate/)
|
|
|
|
|
assert.doesNotMatch(source, /rows\(.*domestic.*\)\[0\]/)
|
|
|
|
|
assert.doesNotMatch(source, /pricingTaxModes\[0\]/)
|
|
|
|
|
assert.match(source, /pricingTaxMode: priced\.priced\.pricingTaxMode/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-13 09:51:27 +08:00
|
|
|
test('入库通过真实订单测量链保留测量态并最终进入 WAIT_SPLIT', () => {
|
2026-08-12 20:36:11 +08:00
|
|
|
assert.match(source, /result\.inbounds\.WAIT_MEASURE/)
|
|
|
|
|
assert.match(source, /processStatus: 'WAIT_MEASURE'/)
|
2026-08-24 10:33:05 +08:00
|
|
|
for (const status of ['MEASURING', 'MEASURE_PENDING', 'WAIT_SPLIT']) {
|
2026-08-12 20:36:11 +08:00
|
|
|
assert.match(source, new RegExp(`'${status}'`))
|
|
|
|
|
}
|
|
|
|
|
assert.match(source, /\.\/inbound-measurement\.mjs/)
|
2026-08-25 00:24:43 +08:00
|
|
|
assert.doesNotMatch(source, /recordCoverageGap|addCoverageGap/)
|
2026-08-14 10:09:46 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('FBA 状态样本为订单头和每条货物行提供四个必填标识', () => {
|
|
|
|
|
assert.match(source, /const shipmentId = `MANUAL-/)
|
|
|
|
|
assert.match(source, /const referenceId = `STATUS-/)
|
|
|
|
|
assert.match(source, /const upcNo =/)
|
|
|
|
|
assert.match(source, /upcNo, shipmentId, referenceId/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('测量状态截停发生在正确的 HTTP 边界', () => {
|
|
|
|
|
assert.match(source, /includes\(summary\.mainStatus\)/)
|
|
|
|
|
assert.doesNotMatch(source, /includes\(summary\.processStatus\)/)
|
2026-08-14 12:50:37 +08:00
|
|
|
assert.match(source, /target === 'MEASURING'/)
|
|
|
|
|
assert.match(source, /\.\/inbound-weight-ocr\.mjs/)
|
|
|
|
|
assert.match(source, /await weightOcr\.run\(context\)/)
|
|
|
|
|
assert.match(source, /target !== 'MEASURING'/)
|
|
|
|
|
assert.doesNotMatch(source, /target === 'MEASURING' && ok\(response\)/)
|
2026-10-08 17:07:57 +08:00
|
|
|
assert.match(source, /\['MEASURE_PENDING', 'FEE_CONFIRMING', 'WAIT_PRICING'\]\.includes\(target\) && isSubmit/)
|
2026-08-24 10:33:05 +08:00
|
|
|
assert.doesNotMatch(source, /target === 'MEASURE_REJECTED' && isSubmit/)
|
2026-08-12 20:36:11 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-25 00:24:43 +08:00
|
|
|
test('入库模块只声明状态全集且不在后续专项生成前预写失败步骤', () => {
|
2026-08-12 20:36:11 +08:00
|
|
|
for (const status of ['WAIT_PRICING', 'FEE_CONFIRMING', 'INBOUNDING', 'VOIDED']) {
|
2026-08-25 00:24:43 +08:00
|
|
|
assert.match(source, new RegExp(`'${status}'`))
|
2026-08-12 20:36:11 +08:00
|
|
|
}
|
2026-10-08 17:07:57 +08:00
|
|
|
assert.match(source, /本轮 WAIT_SPLIT 独立样本缺失/)
|
|
|
|
|
assert.doesNotMatch(source, /查询存量 WAIT_SPLIT 样本|legacyInbound|legacyPage/)
|
2026-08-25 00:24:43 +08:00
|
|
|
assert.doesNotMatch(source, /覆盖阻断|fixture-unavailable|recordCoverageGap|addCoverageGap/)
|
2026-08-12 20:36:11 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-25 00:24:43 +08:00
|
|
|
test('统一订单声明完整状态集合并由总矩阵统一冻结不可达状态', () => {
|
2026-08-12 20:36:11 +08:00
|
|
|
assert.match(source, /result\.unifiedOrders\.PENDING_REVIEW/)
|
|
|
|
|
assert.match(source, /result\.unifiedOrders\.WAIT_MEASURE/)
|
2026-08-13 14:55:50 +08:00
|
|
|
for (const status of ['WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED']) {
|
2026-08-12 20:36:11 +08:00
|
|
|
assert.match(source, new RegExp(`'${status}'`))
|
|
|
|
|
}
|
2026-08-13 09:51:27 +08:00
|
|
|
assert.doesNotMatch(source, /WAIT_SPLIT 标记为历史废弃状态/)
|
2026-08-25 00:24:43 +08:00
|
|
|
assert.doesNotMatch(source, /没有完成派送并写 DELIVERED/)
|
2026-08-12 20:36:11 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('按总门禁格式写入 matrix.pages 的 requiredStatuses 和 fixtures', () => {
|
|
|
|
|
assert.match(source, /run\(context, matrix = context\.createdResources\.manualStateMatrix\)/)
|
|
|
|
|
for (const page of ['firstLegOrders', 'inbounds', 'unifiedOrders']) {
|
|
|
|
|
assert.match(source, new RegExp(`matrix\\.pages\\.${page} = \\{ requiredStatuses:`))
|
|
|
|
|
}
|
|
|
|
|
assert.match(source, /detailPath: `\/orders\/\$\{orderId\}`/)
|
|
|
|
|
})
|