fix(pda): 暂停未配套上线的仓租预检查调用
This commit is contained in:
@@ -267,7 +267,7 @@ test('thirty scanned pallets confirm once without weight volume or pallet entry'
|
||||
assert.equal(state.error, null); assert.equal(state.current.stage, 'AFTER_LOADING'); assert.equal(sent.manifestHash, 'thirty'); assert.equal('measurements' in sent, false)
|
||||
})
|
||||
|
||||
for (const failure of [{ network: true, message: 'timeout' }, { code: 'A0409', message: 'conflict' }, { code: 'A0500', message: 'server error' }, { storageRentPreparation: true, code: 'A0409', message: '仓租费用尚未确认' }]) test('failed pick unlocks and submits current edited values: ' + failure.message, async () => {
|
||||
for (const failure of [{ network: true, message: 'timeout' }, { code: 'A0409', message: 'conflict' }, { code: 'A0500', message: 'server error' }]) test('failed pick unlocks and submits current edited values: ' + failure.message, async () => {
|
||||
const sent = []; let reads = 0
|
||||
storage()
|
||||
const state = page({ pickDispatchItem: async (no, id, body) => { sent.push(body); throw failure }, getDispatchTask: async () => { reads++; return task() } })
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import test from 'node:test'
|
||||
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: {} })
|
||||
} }
|
||||
return calls
|
||||
}
|
||||
|
||||
const dispatch = { recordVersion: '9007199254740993', positionId: '9007199254740994', packages: 21, cartonMark: null, productQuantity: 0, idempotencyKey: 'dispatch-key' }
|
||||
const outbound = { outboundPalletItemId: '9007199254740996', inventoryPositionId: null, packages: 1, cartonMarks: ['RETURN-BOX-1'], skuCode: 'SKU-A', productQuantity: 5, idempotencyKey: 'outbound-key' }
|
||||
const pallet = { idempotencyKey: 'pallet-key' }
|
||||
const discard = { lines: [{ itemId: '9007199254740993', actualPackages: 2 }], photoFileIds: ['101'], scannedCartonMarks: ['BOX-1'], confirmAllCartons: false, idempotencyKey: 'discard-key' }
|
||||
const modes = [
|
||||
{ name: 'dispatch', path: '/dispatches/DSP%2F1/items/9007199254740995/pick', body: dispatch, submit: () => pickDispatchItem('DSP/1', '9007199254740995', dispatch) },
|
||||
{ name: 'outbound', path: '/outbound-tasks/TASK%2F1/pick', body: outbound, submit: () => pickOutboundTask('TASK/1', outbound) },
|
||||
{ name: 'pallet', path: '/outbound-tasks/TASK%2F1/pallets/DP%2F1/pick', body: pallet, submit: () => pickOutboundPallet('TASK/1', 'DP/1', pallet) },
|
||||
{ name: 'discard', path: '/cargo-discards/CD%2F1/pallets/P%2F1/confirm', body: discard, submit: () => confirmCargoDiscard('CD/1', 'P/1', discard.lines, discard.photoFileIds, discard.scannedCartonMarks, false, { idempotencyKey: discard.idempotencyKey }) }
|
||||
]
|
||||
|
||||
for (const mode of modes) {
|
||||
test(`${mode.name} works without the unpublished rent endpoint and 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 }])
|
||||
})
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
test('invalid dispatch version and missing command keys fail before any request', async () => {
|
||||
const calls = transport()
|
||||
await assert.rejects(pickDispatchItem('DSP-1', '11', { idempotencyKey: 'key' }), /版本/)
|
||||
await assert.rejects(pickDispatchItem('DSP-1', '11', { recordVersion: 1 }), /标识/)
|
||||
await assert.rejects(pickOutboundTask('TASK-1', {}), /标识/)
|
||||
await assert.rejects(pickOutboundPallet('TASK-1', 'DP-1', {}), /标识/)
|
||||
await assert.rejects(confirmCargoDiscard('CD-1', 'P-1', [], [], [], false, {}), /标识/)
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
@@ -116,7 +116,6 @@ test('fulfillment API calls the frozen list detail pick complete and review rout
|
||||
request(options) {
|
||||
calls.push({ method: options.method, url: options.url, data: options.data, authorization: options.header.Authorization })
|
||||
let data = {}
|
||||
if (options.url.endsWith('/storage-rent/preparations')) data = { status: 'READY', ready: true }
|
||||
if (options.method === 'GET' && /\/dispatches(?:\?|$)/.test(options.url)) data = []
|
||||
else if (options.method === 'GET' && /\/outbound-tasks(?:\?|$)/.test(options.url)) data = []
|
||||
else if (options.method === 'GET' && options.url.includes('/dispatches/')) data = { dispatchNo: 'DSP-1', taskNo: 'TASK-1', items: [] }
|
||||
@@ -136,7 +135,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, 16)
|
||||
assert.equal(calls.length, 13)
|
||||
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')))
|
||||
|
||||
@@ -257,12 +257,12 @@ test('partial bound pallet confirmation sends only selected marks and preserves
|
||||
assert.deepEqual(payload, ['CD-P', 'P-1', [{ itemId: 'sku', actualPackages: 1 }], ['101'], ['BOX-2'], false, { idempotencyKey: 'key' }])
|
||||
})
|
||||
|
||||
test('discard confirmation preserves inputs after preflight failure and starts a new checked request', async () => {
|
||||
test('discard confirmation preserves inputs after stock command failure and starts a new explicit request', async () => {
|
||||
const submitted = []
|
||||
const retry = deferred()
|
||||
let attempt = 0
|
||||
const { vm } = page({
|
||||
confirmCargoDiscard: async (...args) => { submitted.push(args); if (++attempt === 1) throw new Error('费用确认中'); await retry.promise },
|
||||
confirmCargoDiscard: async (...args) => { submitted.push(args); if (++attempt === 1) throw new Error('库存状态已变化'); await retry.promise },
|
||||
fulfillmentIdempotencyKey: () => ({ value: `discard-${attempt}` }), listCargoDiscardTasks: async () => []
|
||||
})
|
||||
vm.discardCurrent = { discardNo: 'CD-RETRY' }
|
||||
@@ -271,7 +271,7 @@ test('discard confirmation preserves inputs after preflight failure and starts a
|
||||
await vm.confirmDiscard()
|
||||
assert.equal(vm.discardBusy, false)
|
||||
assert.ok(vm.discardTask)
|
||||
assert.equal(vm.error.message, '费用确认中')
|
||||
assert.equal(vm.error.message, '库存状态已变化')
|
||||
vm.successMessage = '上次作业已完成'
|
||||
const retried = vm.confirmDiscard()
|
||||
assert.equal(vm.error, null)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import test from 'node:test'
|
||||
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(reply) {
|
||||
const calls = []
|
||||
session.apiBase = '/api'; session.token = 'overseas-token'
|
||||
globalThis.uni = { request(options) {
|
||||
calls.push({ url: options.url, data: options.data })
|
||||
if (reply instanceof Error) return options.fail({ errMsg: reply.message })
|
||||
options.success({ statusCode: 200, data: { code: '200', data: options.url.endsWith('/preparations') ? reply : { success: true } }, header: {} })
|
||||
} }
|
||||
return calls
|
||||
}
|
||||
|
||||
test('all stock scan modes require ready preparation with the original command key and source identifiers', async () => {
|
||||
const calls = transport({ status: 'READY', ready: true })
|
||||
const request = { recordVersion: '9007199254740993', positionId: '9007199254740994', packages: 2, cartonMark: 'RC-1', productQuantity: 7, idempotencyKey: 'original-dispatch-key' }
|
||||
await pickDispatchItem('DSP-1', '9007199254740995', request)
|
||||
assert.deepEqual(calls[0].data, { mode: 'DISPATCH', businessNo: 'DSP-1', itemId: '9007199254740995', positionId: request.positionId, packages: 2, cartonMark: 'RC-1', productQuantity: 7, idempotencyKey: request.idempotencyKey })
|
||||
assert.deepEqual(calls[1].data, request)
|
||||
await pickOutboundTask('TASK-1', { outboundPalletItemId: '9007199254740996', inventoryPositionId: null, packages: 1, cartonMarks: [], skuCode: 'SKU-A', productQuantity: 5, idempotencyKey: 'original-sku-key' })
|
||||
assert.deepEqual(calls[2].data, { mode: 'OUTBOUND', businessNo: 'TASK-1', itemId: '9007199254740996', positionId: null, packages: 1, cartonMarks: [], skuCode: 'SKU-A', productQuantity: 5, idempotencyKey: 'original-sku-key' })
|
||||
await pickOutboundPallet('TASK-1', 'DP-1', { idempotencyKey: 'original-pallet-key' })
|
||||
assert.deepEqual(calls[4].data, { mode: 'OUTBOUND_PALLET', businessNo: 'TASK-1', palletNo: 'DP-1', idempotencyKey: 'original-pallet-key' })
|
||||
assert.equal(calls.length, 6)
|
||||
for (let i = 0; i < calls.length; i += 2) assert.ok(calls[i].url.endsWith('/preparations'))
|
||||
})
|
||||
|
||||
test('carton outbound preparation uses the same identity as the backend stock command', async () => {
|
||||
const calls = transport({ status: 'READY', ready: true })
|
||||
const request = { outboundPalletItemId: '21', inventoryPositionId: null, packages: 1, cartonMarks: ['RETURN-BOX-1'], idempotencyKey: 'carton-key' }
|
||||
await pickOutboundTask('TASK-2', request)
|
||||
assert.deepEqual(calls[0].data.cartonMarks, request.cartonMarks)
|
||||
// 出库后台消费只使用 cartonMarks,不能额外派生 cartonMark 导致幂等摘要不一致。
|
||||
assert.equal(Object.hasOwn(calls[0].data, 'cartonMark'), false)
|
||||
assert.deepEqual(calls[1].data, request)
|
||||
})
|
||||
|
||||
test('pending, failed, expired, malformed and network responses never send a stock mutation', async () => {
|
||||
for (const response of [{ status: 'FREEZE_PENDING', ready: false }, { status: 'FAILED', message: '余额及授信不足' }, { status: 'EXPIRED', ready: false }, { status: 'READY', ready: false }, { status: 'UNKNOWN', ready: true }, {}, new Error('network down')]) {
|
||||
const calls = transport(response)
|
||||
await assert.rejects(pickOutboundPallet('TASK-1', 'DP-1', { idempotencyKey: 'stable-key' }), error => error.storageRentPreparation === true)
|
||||
assert.equal(calls.length, 1)
|
||||
}
|
||||
})
|
||||
|
||||
test('retry reuses the preparation key; exempt and already consumed commands can recover the stock result', async () => {
|
||||
const request = { idempotencyKey: 'stable-key' }
|
||||
let calls = transport({ status: 'FREEZE_PENDING', ready: false })
|
||||
await assert.rejects(pickOutboundPallet('TASK-1', 'DP-1', request))
|
||||
const original = calls[0].data
|
||||
for (const status of ['NOT_REQUIRED', 'READY', 'CONSUMED']) {
|
||||
calls = transport({ status, ready: true })
|
||||
await pickOutboundPallet('TASK-1', 'DP-1', request)
|
||||
assert.deepEqual(calls[0].data, original)
|
||||
assert.equal(calls.length, 2)
|
||||
}
|
||||
})
|
||||
|
||||
test('invalid dispatch version or missing command identity cannot start a reservation', async () => {
|
||||
const calls = transport({ status: 'READY', ready: true })
|
||||
await assert.rejects(pickDispatchItem('DSP-1', '11', { idempotencyKey: 'key' }), /版本/)
|
||||
await assert.rejects(pickOutboundPallet('TASK-1', 'DP-1', {}), /标识/)
|
||||
assert.equal(calls.length, 0)
|
||||
})
|
||||
|
||||
test('discard requires funds before stock confirmation and preserves the complete source payload on retry', async () => {
|
||||
const lines = [{ itemId: '9007199254740993', actualPackages: 2 }]
|
||||
const submit = () => confirmCargoDiscard('CD-1', 'P-1', lines, ['101'], ['BOX-1'], false, { idempotencyKey: 'discard-stable-key' })
|
||||
let calls = transport({ status: 'FREEZE_PENDING', ready: false })
|
||||
await assert.rejects(submit(), error => error.storageRentPreparation === true)
|
||||
assert.equal(calls.length, 1)
|
||||
const original = calls[0].data
|
||||
assert.deepEqual(original, { mode: 'DISCARD', businessNo: 'CD-1', palletNo: 'P-1', lines, photoFileIds: ['101'], scannedCartonMarks: ['BOX-1'], confirmAllCartons: false, idempotencyKey: 'discard-stable-key' })
|
||||
calls = transport({ status: 'READY', ready: true })
|
||||
await submit()
|
||||
assert.deepEqual(calls[0].data, original)
|
||||
assert.equal(calls.length, 2)
|
||||
assert.ok(calls[1].url.endsWith('/cargo-discards/CD-1/pallets/P-1/confirm'))
|
||||
assert.deepEqual(calls[1].data, { lines, photoFileIds: ['101'], scannedCartonMarks: ['BOX-1'], confirmAllCartons: false, idempotencyKey: 'discard-stable-key' })
|
||||
})
|
||||
Reference in New Issue
Block a user