import test from 'node:test' import assert from 'node:assert/strict' import { confirmCargoDiscard } from '../src/api/fulfillment.js' import { session } from '../src/stores/session.js' const rentPath = '/api/pda/v1/last-leg/storage-rent/preparations' const stockPath = '/api/pda/v1/last-leg/cargo-discards/CD%2F1/pallets/P%2F1/confirm' const body = { lines: [{ itemId: '9007199254740993', actualPackages: 2 }], photoFileIds: ['101'], scannedCartonMarks: ['BOX-1', 'BOX-2'], confirmAllCartons: false, idempotencyKey: 'discard-key' } const ready = { data: { preparationId: '9007199254740995', ready: true, status: 'READY' } } const confirmed = { data: { discardNo: 'CD/1', palletNo: 'P/1', status: 'COMPLETED' } } const pending = { data: { preparationId: '9007199254740995', ready: false, status: 'FREEZE_PENDING' } } const submit = () => confirmCargoDiscard('CD/1', 'P/1', body.lines, body.photoFileIds, body.scannedCartonMarks, false, { idempotencyKey: body.idempotencyKey }) 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 additional request') if (response.network) return options.fail({ errMsg: 'network down' }) options.success({ statusCode: 200, data: { code: '200', ...response }, header: {} }) } } return calls } test('出库先预校验仓租,再用相同明细、照片、箱号和幂等键确认一次', async () => { const calls = transport([ready, confirmed]); await submit() assert.deepEqual(calls, [ { method: 'POST', url: rentPath, data: { mode: 'DISCARD', businessNo: 'CD/1', palletNo: 'P/1', ...body } }, { method: 'POST', url: stockPath, data: body } ]) }) test('免费或不计仓租的服务端资格可以直接继续出库', async () => { const calls = transport([{ data: { status: 'NOT_REQUIRED', ready: true, amount: 0 } }, confirmed]) await submit(); assert.equal(calls.length, 2); assert.equal(calls[1].url, stockPath) }) test('等待财务回执期间只读状态,就绪后只提交一次出库', async t => { t.mock.method(globalThis, 'setTimeout', callback => { callback(); return 0 }) const calls = transport([pending, ready, confirmed]); await submit() assert.deepEqual(calls.map(call => call.method), ['POST', 'GET', 'POST']) assert.equal(calls[1].url, rentPath + '/9007199254740995') }) test('余额授信不足时显示财务原因,释放未使用预占且不发出库请求', async () => { const calls = transport([{ data: { ...pending.data, status: 'FAILED', message: '可用余额和授信额度不足' } }, { data: {} }]) await assert.rejects(submit(), error => error.code === 'A0409' && /仓租校验未通过.*余额和授信额度不足/.test(error.message)) assert.deepEqual(calls.map(call => call.method), ['POST', 'DELETE']) }) test('缺失、失败或断网的预校验不降级绕过,保留诊断信息', async () => { for (const response of [{ code: 'A0404', message: 'missing', requestId: 'req', traceId: 'trace' }, { code: 'A0409', message: 'conflict' }, { network: true }]) { const calls = transport([response]) await assert.rejects(submit(), error => response.network ? error.network : error.code === response.code && error.requestId === response.requestId && error.traceId === response.traceId) assert.equal(calls.length, 1) } }) test('回执超时释放预占,阻止出库并提示稍后重新提交', async t => { t.mock.method(globalThis, 'setTimeout', callback => { callback(); return 0 }) const calls = transport([pending, ...Array(20).fill(pending), { data: {} }]) await assert.rejects(submit(), /仓租资金预校验尚未通过/) assert.equal(calls.filter(call => call.method === 'GET').length, 20) assert.equal(calls.at(-1).method, 'DELETE') assert.ok(calls.every(call => call.url.includes('/storage-rent/'))) }) test('状态查询失败仍释放原预占;释放失败不覆盖原始诊断', async t => { t.mock.method(globalThis, 'setTimeout', callback => { callback(); return 0 }) const calls = transport([pending, { code: 'S0503', message: 'read failed', requestId: 'req', traceId: 'trace' }, { code: 'S0503', message: 'release failed' }]) await assert.rejects(submit(), error => error.message === 'read failed' && error.requestId === 'req' && error.traceId === 'trace') assert.deepEqual(calls.map(call => call.method), ['POST', 'GET', 'DELETE']) }) test('就绪标记必须为布尔真,异常响应不能放行出库', async () => { const calls = transport([{ data: { ...ready.data, ready: 'true' } }, { data: {} }]) await assert.rejects(submit(), /预校验/); assert.equal(calls.at(-1).method, 'DELETE') }) test('出库请求已发送后,业务错误或结果不明不重发、不取消原预占', async () => { for (const failure of [{ code: 'A0409', message: 'stock changed' }, { network: true }]) { const calls = transport([ready, failure]) await assert.rejects(submit(), error => failure.network ? error.network : error.code === failure.code) assert.equal(calls.length, 2); assert.ok(calls.every(call => call.method === 'POST')) } })