84 lines
5.4 KiB
JavaScript
84 lines
5.4 KiB
JavaScript
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' })
|
|
})
|