fix(phase4): enforce customer and pallet versions
This commit is contained in:
@@ -9,7 +9,7 @@ import { run } from './phase4-overseas-warehouse.mjs'
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'phase4-overseas-warehouse.mjs'), 'utf8')
|
||||
|
||||
function response(data) { return { http: 200, body: { code: '200', message: 'success', data }, requestId: 'request-test', traceId: 'trace-test' } }
|
||||
function response(data, code = '200') { return { http: 200, body: { code, message: code === '200' ? 'success' : 'conflict', data }, requestId: 'request-test', traceId: 'trace-test' } }
|
||||
function page(records = []) { return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 } }
|
||||
function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehouseRecords = [{ id: '20001', status: 'ACTIVE', warehouseCode: 'LAX', warehouseName: '洛杉矶仓' }] } = {}) {
|
||||
const calls = []; const steps = []; const gaps = []; let createdPallet = null
|
||||
@@ -20,8 +20,11 @@ function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehou
|
||||
calls.push({ channel: 'overseas', method, path: requestPath, body })
|
||||
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
|
||||
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) return response(null)
|
||||
if (method === 'POST' && requestPath.endsWith('/warehouse-admin/20001/pallets')) { createdPallet = { id: '71001', customerId: '0', warehouseId: '20001', locationId: '72001', ...body }; return response('71001') }
|
||||
if (method === 'PUT' && requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) { createdPallet = { ...createdPallet, ...body }; return response(null) }
|
||||
if (method === 'POST' && requestPath.endsWith('/warehouse-admin/20001/pallets')) { createdPallet = { id: '71001', customerId: '0', warehouseId: '20001', locationId: '72001', version: 0, ...body }; return response('71001') }
|
||||
if (method === 'PUT' && requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) {
|
||||
if (Number(body.version) !== Number(createdPallet.version)) return response(null, 'A0409')
|
||||
createdPallet = { ...createdPallet, ...body, version: createdPallet.version + 1 }; return response(null)
|
||||
}
|
||||
if (requestPath.startsWith('/api/platform/v1/last-leg/orders?')) return response(page([{ id: '30001', orderNo: 'LL-1', currentStatus: 'PENDING_ASSIGNMENT' }]))
|
||||
if (requestPath.includes('/last-leg/orders/30001?')) return response({ order: { id: '30001' }, cargo: [], statusHistory: [] })
|
||||
if (requestPath.startsWith('/api/platform/v1/last-leg/relabel-applications?')) return response(page([{ id: '40001', relabelNo: 'RL-1', status: 'PENDING_PROCESSING' }]))
|
||||
@@ -64,7 +67,7 @@ test('既有仓库场景只写本轮托盘生命周期', () => {
|
||||
assert.equal(context.calls.filter((call) => call.channel === 'domestic').length, 1)
|
||||
assert.ok(context.calls.filter((call) => call.channel === 'overseas').every((call) => call.path.startsWith('/api/platform/v1/')))
|
||||
const writes = context.calls.filter((call) => call.method !== 'GET')
|
||||
assert.deepEqual(writes.map((call) => call.method), ['POST', 'PUT', 'PUT'])
|
||||
assert.deepEqual(writes.map((call) => call.method), ['POST', 'PUT', 'PUT', 'PUT'])
|
||||
assert.ok(writes.every((call) => call.path.includes('/warehouse-admin/20001/pallets')))
|
||||
assert.ok(context.steps.every((step) => step.passed), context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
|
||||
})
|
||||
@@ -110,5 +113,5 @@ test('合同覆盖库存CSV并明确排除暂缓、离仓、PDA与内部路径',
|
||||
assert.match(source, /longId\(plan\.sourceOrderId\)/)
|
||||
assert.match(source, /id\(pallet\.customerId\) === '0'/)
|
||||
assert.match(source, /换标创建与状态生命周期/)
|
||||
assert.match(source, /旧版本并发冲突/)
|
||||
assert.match(source, /公共托盘旧版本编辑返回冲突/)
|
||||
})
|
||||
|
||||
@@ -37,8 +37,9 @@ async function overseasGet(context, name, path) {
|
||||
}
|
||||
async function overseasMutation(context, method, name, path, body) {
|
||||
const response = await context.requestOverseas(method, path, body)
|
||||
assertEnvelope(context, name, response)
|
||||
return { response, data: context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) }
|
||||
const passed = response?.http === 200 && String(response?.body?.code) === '200'
|
||||
context.addStep(MODULE, name, response, passed, trace(response), OPTIONS)
|
||||
return { response, data: passed ? response.body?.data : null }
|
||||
}
|
||||
async function overseasCsv(context, name, path) {
|
||||
const response = await context.requestOverseasBinary('GET', path)
|
||||
@@ -230,7 +231,7 @@ async function verifyPalletLifecycle(context, warehouseId) {
|
||||
|
||||
const updated = await overseasMutation(context, 'PUT', '编辑本轮公共托盘', detailPath, {
|
||||
palletCode, palletType: '木托盘', capacityCbm: 2.5, unitPrice: 1, status: 'IDLE',
|
||||
locationId: id(detail.data?.locationId) || undefined, remark: `${context.marker}-edited`,
|
||||
locationId: id(detail.data?.locationId) || undefined, remark: `${context.marker}-edited`, version: Number(detail.data?.version),
|
||||
})
|
||||
context.assert(MODULE, '公共托盘编辑成功', updated.response, updated.data === null,
|
||||
`palletId=${palletId}; ${trace(updated.response)}`, OPTIONS)
|
||||
@@ -239,14 +240,20 @@ async function verifyPalletLifecycle(context, warehouseId) {
|
||||
id(after.data?.id) === palletId && Number(after.data?.capacityCbm) === 2.5
|
||||
&& Number(after.data?.unitPrice) === 1 && after.data?.remark === `${context.marker}-edited`,
|
||||
`capacity=${after.data?.capacityCbm ?? 'missing'}, unitPrice=${after.data?.unitPrice ?? 'missing'}, remark=${after.data?.remark || 'missing'}; ${trace(after.response)}`, OPTIONS)
|
||||
coverageGap(context, '公共托盘旧版本并发冲突',
|
||||
'当前 Pallet SaveRequest/PUT 未提供 version 或 expectedVersion,无法构造稳定旧版本冲突且禁止伪造冲突请求', 'missing-version-contract')
|
||||
const stale = await context.requestOverseas('PUT', detailPath, {
|
||||
palletCode, palletType: '木托盘', capacityCbm: 9, unitPrice: 9, status: 'IDLE',
|
||||
remark: 'STALE-DO-NOT-APPLY', version: Number(detail.data?.version),
|
||||
})
|
||||
context.addStep(MODULE, '公共托盘旧版本编辑返回冲突', stale,
|
||||
stale?.http === 409 || (stale?.http === 200 && String(stale?.body?.code).endsWith('409')), trace(stale), OPTIONS)
|
||||
} finally {
|
||||
if (palletId) {
|
||||
const retired = await overseasMutation(context, 'PUT', '报废本轮公共托盘',
|
||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, {
|
||||
palletCode, palletType: '木托盘', capacityCbm: 2.5, unitPrice: 1, status: 'SCRAPPED',
|
||||
remark: `${context.marker}-acceptance-retired`,
|
||||
remark: `${context.marker}-acceptance-retired`, version: Number((await overseasGet(context,
|
||||
'读取公共托盘报废前最新版本',
|
||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`)).data?.version),
|
||||
})
|
||||
context.createdResources.phase4OverseasWarehouse.terminalStatus = retired.data === null ? 'SCRAPPED' : null
|
||||
context.assert(MODULE, '本轮公共托盘已报废收口', retired.response, retired.data === null,
|
||||
|
||||
Reference in New Issue
Block a user