This commit is contained in:
2026-09-09 20:53:24 +08:00
parent 4cc3e5372b
commit 6737c4c327
23 changed files with 525 additions and 120 deletions
+107 -1
View File
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { normalizeRows, channelName, matchRoute, measurementShape, buildSourceDraft, buildPayload,
requireSuccess, executeCandidates, totalPreservingShapes } from './real-order-xlsx-test-env.mjs'
requireSuccess, executeCandidates, totalPreservingShapes, saveMeasurementDraft } from './real-order-xlsx-test-env.mjs'
const header = ['进仓编号', '客户', '渠道', '亚马逊仓', '实际件数', '实际毛重', '实际立方', '预报件数', '买单/退税', '品名', 'SKU', 'FBA货件编号', '跟踪编号']
const row = ['ABCD2606010001', 'Example', '合德海卡包税', 'ABC1', 31, 100, 2, 31, '买单', 'Goods', 'SKU1', 'SHIP1', 'REF1']
@@ -39,6 +39,15 @@ test('split rows aggregate actual counts once and reconcile forecast', () => {
b[4] = 15
assert.ok(normalizeRows([header, a, b]).orders[0].problems.includes('SPLIT_COUNT_MISMATCH'))
})
test('split weight and volume totals use the persisted order precision', () => {
const a = [...row], b = [...row]
a[4] = 90; b[4] = 300; a[7] = 390; b[7] = 390
a[5] = 1137.461538; b[5] = 3791.538
a[6] = 7.256538462; b[6] = 24.188
const result = normalizeRows([header, a, b]).orders[0]
assert.equal(result.weightKg, 4929)
assert.equal(result.volumeCbm, 31.4445)
})
test('conflicting channels on same inbound code block import', () => {
const b = [...row]; b[2] = '其他包税'
assert.ok(normalizeRows([header, row, b]).orders[0].problems.includes('CONFLICT_渠道'))
@@ -105,9 +114,106 @@ test('HTTP business errors preserve diagnostic evidence', () => {
assert.throws(() => requireSuccess({ http: 200, path: '/example', body: { code: 'A0409' }, requestId: 'req' }),
(error) => error.evidence.code === 'A0409' && error.evidence.requestId === 'req')
})
test('measurement draft retries a transient server error after refreshing the version', async () => {
let reads = 0, writes = 0
const retries = []
const entry = { dimensionPlan: [{ itemId: 'item-1', count: 31, length: 40, width: 30, height: 30 }] }
const api = async (method) => {
if (method === 'GET') return { measurementId: 'measurement-1', version: ++reads,
items: [{ id: 'item-1', inboundDetailId: 'detail-1', itemNo: 1, boxSpecification: 'TEST' }] }
writes++
if (writes === 1) { const error = Error('temporary'); error.evidence = { code: 'S0500', requestId: 'req-1' }; throw error }
return { measurementId: 'measurement-1', version: 3 }
}
const result = await saveMeasurementDraft({ api, measurementPath: '/measurement', base: '/inbound/1', q: 'tenantId=1',
order: order(), entry, marker: 'MARKER', wait: async () => {}, onRetry: async (retry) => retries.push(retry) })
assert.equal(result.version, 3)
assert.equal(reads, 2)
assert.equal(writes, 2)
assert.deepEqual(retries.map((retry) => retry.code), ['S0500'])
})
test('same batch never recreates an attempted order, even when interrupted', async () => {
const value = order(), state = { identity: { batchId: 'test' }, orders: { [value.key]: { status: 'STARTED' } } }
await executeCandidates({ candidates: [{ order: value, route: catalog }], limit: 1, state, save: async () => {},
api: async () => { throw Error('must not call HTTP') } })
assert.equal(state.lastRun.attempted, 0); assert.equal(state.lastRun.skipped, 1)
})
test('pending-review stage submits but never calls approval or measurement APIs', async () => {
const value = order(), route = matchRoute(value, catalog)
const state = { identity: { batchId: 'pending-review' }, orders: {} }
const calls = []
let detailReads = 0
const api = async (method, path) => {
calls.push(`${method} ${path}`)
if (path.endsWith('/pricing/estimate')) return { channelCode: route.channel.channelCode, pricingTaxMode: value.pricingTaxMode }
if (method === 'POST' && path === '/api/merchant/v1/orders/first-leg') return 'order-1'
if (path === '/api/merchant/v1/orders/first-leg/order-1' && method === 'GET') {
detailReads++
return { order: { id: 'order-1', orderNo: 'FL-1', customerRemark: Object.values(state.orders)[0].marker,
status: detailReads > 1 ? 'SUBMITTED' : 'DRAFT' },
cargoes: [{ packageCount: value.packageCount, grossWeightKg: value.weightKg, volumeCbm: value.volumeCbm }] }
}
if (path === '/api/merchant/v1/orders/order-1') return { summary: { mainStatus: 'PENDING_REVIEW' } }
if (method === 'POST' && path.endsWith('/submit')) return true
throw Error(`unexpected call ${method} ${path}`)
}
await executeCandidates({ candidates: [{ order: value, route }], limit: 1, concurrency: 1, state,
save: async () => {}, api, merchant: 'merchant', profile: { tenantName: 'Example' },
sharedResources: { productImageFileId: 'image-1' }, stage: 'pending-review' })
assert.equal(Object.values(state.orders)[0].stage, 'PENDING_REVIEW')
assert.equal(Object.values(state.orders)[0].verification.mainStatus, 'PENDING_REVIEW')
assert.equal(calls.some((call) => /approve|measurement|inbounds/.test(call)), false)
})
test('pending-review retries an estimate-only pricing failure and records it as a warning', async () => {
const value = order(), route = matchRoute(value, catalog)
const state = { identity: { batchId: 'pricing-warning' }, orders: { [value.key]: { status: 'FAILED', stage: 'estimate',
error: 'HTTP_200_CODE_A0404', evidence: { code: 'A0404', requestId: 'old-request' }, finishedAt: '2026-01-01T00:00:00Z' } } }
let detailReads = 0
const api = async (method, path) => {
if (path.endsWith('/pricing/estimate')) { const error = Error('no price'); error.evidence = { code: 'A0404', message: 'no price' }; throw error }
if (method === 'POST' && path === '/api/merchant/v1/orders/first-leg') return 'order-2'
if (path === '/api/merchant/v1/orders/first-leg/order-2' && method === 'GET') {
detailReads++
return { order: { id: 'order-2', orderNo: 'FL-2', customerRemark: Object.values(state.orders)[0].marker,
status: detailReads > 1 ? 'SUBMITTED' : 'DRAFT' },
cargoes: [{ packageCount: value.packageCount, grossWeightKg: value.weightKg, volumeCbm: value.volumeCbm }] }
}
if (path === '/api/merchant/v1/orders/order-2') return { summary: { mainStatus: 'PENDING_REVIEW' } }
if (method === 'POST' && path.endsWith('/submit')) return true
throw Error(`unexpected call ${method} ${path}`)
}
await executeCandidates({ candidates: [{ order: value, route }], limit: 1, concurrency: 1, state,
save: async () => {}, api, merchant: 'merchant', profile: { tenantName: 'Example' },
sharedResources: { productImageFileId: 'image-1' }, stage: 'pending-review' })
const entry = state.orders[value.key]
assert.equal(entry.status, 'SUCCEEDED')
assert.equal(entry.pricingWarning.code, 'A0404')
assert.equal(entry.priorAttempts.length, 1)
})
test('single process respects bounded concurrency while keeping per-order checkpoints', async () => {
const base = order()
const route = matchRoute(base, catalog)
const candidates = Array.from({ length: 7 }, (_, index) => ({
order: { ...base, key: `key-${index}`, sourceCode: `SRC-${index}`, rows: [index + 2] }, route,
}))
const state = { identity: { batchId: 'concurrency' }, orders: {} }
let active = 0, maximum = 0, id = 0
const api = async (method, path, body) => {
if (path.endsWith('/pricing/estimate')) {
active++; maximum = Math.max(maximum, active)
await new Promise((resolve) => setTimeout(resolve, 5))
active--
return { channelCode: route.channel.channelCode, pricingTaxMode: base.pricingTaxMode }
}
if (method === 'POST') return String(++id)
const marker = path.split('/').at(-1)
return { order: { id: marker, orderNo: `ORDER-${marker}`, customerRemark: Object.values(state.orders)
.find((entry) => entry.orderId === marker)?.marker }, cargoes: [{ packageCount: 31, grossWeightKg: 100, volumeCbm: 2 }] }
}
await executeCandidates({ candidates, limit: 7, concurrency: 3, state, save: async () => {}, api,
profile: { tenantName: 'Example' }, stage: 'create' })
assert.equal(maximum, 3)
assert.equal(state.lastRun.attempted, 7)
assert.equal(state.lastRun.succeeded, 7)
assert.equal(Object.values(state.orders).filter((entry) => entry.status === 'SUCCEEDED').length, 7)
})