This commit is contained in:
2026-11-02 17:16:15 +08:00
parent 63407d176f
commit 436393d4ea
5 changed files with 76 additions and 14 deletions
+48
View File
@@ -0,0 +1,48 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { pickDispatchItem } from '../src/api/fulfillment.js'
import { session } from '../src/stores/session.js'
const path = '/api/pda/v1/last-leg/storage-rent/preparations'
const pick = '/api/pda/v1/last-leg/dispatches/PC1/items/9007199254740995/pick'
const body = { recordVersion: '9007199254740993', positionId: '9007199254740994', packages: 2, productQuantity: 0, cartonMark: null, idempotencyKey: 'dispatch-rent-1' }
const ready = { data: { preparationId: 'rent1', ready: true, status: 'READY' } }
const pending = { data: { preparationId: 'rent1', ready: false, status: 'FREEZE_PENDING' } }
const submit = () => pickDispatchItem('PC1', '9007199254740995', body)
function transport(responses) {
const calls = []; session.apiBase = '/api'; session.token = 'overseas-token'
globalThis.uni = { request(options) {
calls.push({ method: options.method, url: options.url, data: options.data })
const response = responses.shift(); assert.ok(response, 'unexpected request')
if (response.network) return options.fail({ errMsg: 'network down' })
options.success({ statusCode: 200, data: { code: '200', ...response }, header: {} })
} }
return calls
}
test('排车仓租与实际拣货绑定同一来源、数量和幂等键,保持长ID精度', async () => {
const calls = transport([ready, { data: { success: true } }]); await submit()
assert.deepEqual(calls, [
{ method: 'POST', url: path, data: { mode: 'DISPATCH', businessNo: 'PC1', itemId: '9007199254740995', positionId: body.positionId, packages: 2, cartonMark: null, idempotencyKey: body.idempotencyKey, productQuantity: 0 } },
{ method: 'POST', url: pick, data: body }
])
})
test('免租或不计租仍按后端就绪结果继续,非布尔就绪禁止拣货', async () => {
const calls = transport([{ data: { ready: true, status: 'NOT_REQUIRED' } }, { data: {} }]); await submit(); assert.equal(calls.at(-1).url, pick)
const blocked = transport([{ data: { ready: 'true' } }]); await assert.rejects(submit(), /预校验/); assert.equal(blocked.length, 1)
})
test('预占待回执只读查询,就绪后仅发一次拣货', async t => {
t.mock.method(globalThis, 'setTimeout', callback => { callback(); return 0 })
const calls = transport([pending, ready, { data: {} }]); await submit()
assert.deepEqual(calls.map(c => [c.method, c.url]), [['POST', path], ['GET', path + '/rent1'], ['POST', pick]])
})
test('资金失败释放预占;预校验404或断网不绕过,不发拣货', async () => {
const calls = transport([{ data: { preparationId: 'rent1', ready: false, status: 'FAILED', message: '余额不足' } }, { data: {} }])
await assert.rejects(submit(), /余额不足/); assert.deepEqual(calls.map(c => c.method), ['POST', 'DELETE'])
for (const failure of [{ code: 'A0404', message: 'missing', requestId: 'req1' }, { network: true }]) {
const failed = transport([failure]); await assert.rejects(submit(), e => failure.network ? e.network : e.code === failure.code && e.requestId === 'req1'); assert.equal(failed.length, 1)
}
})
test('拣货请求发出后结果不明,不重发也不释放可能已消费的预占', async () => {
const calls = transport([ready, { network: true }]); await assert.rejects(submit(), e => e.network)
assert.deepEqual(calls.map(c => c.method), ['POST', 'POST'])
})
+10 -8
View File
@@ -3,15 +3,15 @@ import assert from 'node:assert/strict'
import { pickDispatchItem, pickOutboundTask, pickOutboundPallet, confirmCargoDiscard } from '../src/api/fulfillment.js'
import { session } from '../src/stores/session.js'
// 模拟当前生产:仓租预检查接口不存在,既有库存命令仍正常受理。
// 排车拣货先完成仓租预校验,再提交同一份库存命令。
function transport(failure) {
const calls = []
session.apiBase = '/api'; session.token = 'overseas-token'
globalThis.uni = { request(options) {
calls.push({ method: options.method, url: options.url, data: options.data })
if (failure?.network) return options.fail({ errMsg: 'network down' })
const error = options.url.includes('/storage-rent/') ? { code: 'A0404', message: 'Resource not found' } : failure
options.success({ statusCode: 200, data: error || { code: '200', data: { success: true } }, header: {} })
const rent = options.url.includes('/storage-rent/')
options.success({ statusCode: 200, data: rent ? { code: '200', data: { preparationId: 'rent-1', ready: true, status: 'READY' } } : failure || { code: '200', data: { success: true } }, header: {} })
} }
return calls
}
@@ -27,18 +27,20 @@ const modes = [
]
for (const mode of modes) {
test(`${mode.name} works without the unpublished rent endpoint and preserves its stock payload`, async () => {
test(`${mode.name} preserves its stock payload`, async () => {
const calls = transport()
await mode.submit()
assert.deepEqual(calls, [{ method: 'POST', url: '/api/pda/v1/last-leg' + mode.path, data: mode.body }])
const stock = calls.find(call => call.url.includes(mode.path))
assert.deepEqual(stock, { method: 'POST', url: '/api/pda/v1/last-leg' + mode.path, data: mode.body })
if (mode.name === 'dispatch') assert.equal(calls.filter(call => call.url.includes('/storage-rent/')).length, 1)
})
test(`${mode.name} propagates stock failures without fallback or automatic resubmission`, async () => {
for (const failure of [{ code: 'A0404', message: 'missing' }, { code: 'A0409', message: 'conflict' }, { code: 'S0503', message: 'unavailable' }, { network: true }]) {
const calls = transport(failure)
await assert.rejects(mode.submit(), error => failure.network ? error.network === true : error.code === failure.code)
assert.equal(calls.length, 1)
assert.equal(calls[0].url, '/api/pda/v1/last-leg' + mode.path)
assert.deepEqual(calls[0].data, mode.body)
const stock = calls.find(call => call.url.includes(mode.path))
if (failure.network && mode.name === 'dispatch') assert.equal(stock, undefined)
else assert.deepEqual(stock, { method: 'POST', url: '/api/pda/v1/last-leg' + mode.path, data: mode.body })
}
})
}
+2 -1
View File
@@ -121,6 +121,7 @@ test('fulfillment API calls the frozen list detail pick complete and review rout
else if (options.method === 'GET' && options.url.includes('/dispatches/')) data = { dispatchNo: 'DSP-1', taskNo: 'TASK-1', items: [] }
else if (options.method === 'GET' && options.url.includes('/outbound-tasks/resolve/')) data = { workflow: 'DISPATCH', dispatchNo: 'DSP-1', outboundNo: 'OUT-1' }
else if (options.method === 'GET' && options.url.includes('/outbound-tasks/')) data = { task: { taskNo: 'OUT-TASK-1', outboundNo: 'OUT-1' }, items: [] }
else if (options.method === 'POST' && options.url.includes('/storage-rent/preparations')) data = { preparationId: 'rent-1', ready: true, status: 'READY' }
options.success({ statusCode: 200, data: { code: '200', data }, header: {} })
}
}
@@ -135,7 +136,7 @@ test('fulfillment API calls the frozen list detail pick complete and review rout
await pickOutboundPallet('OUT/TASK/1', 'DP/1', { idempotencyKey: 'out-pallet-key' })
await completeOutboundTask('OUT/TASK/1', { idempotencyKey: 'out-complete-key' })
assert.equal(calls.length, 13)
assert.equal(calls.length, 14)
assert.deepEqual(calls.find(call => call.url.endsWith('/dispatches/DSP%2F1/loading-review')).data, { recordVersion: 0, idempotencyKey: 'review-key', decision: 'REJECTED', reason: '车牌不符' })
assert.ok(calls.some((call) => call.url.endsWith('/dispatches')))
assert.ok(calls.some((call) => call.url.includes('/dispatches/DSP%2F1/items/11/pick')))