Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs
T

443 lines
29 KiB
JavaScript

const MODULE = 'phase4-overseas-pda-operations'
const SCENARIO = 'phase4-overseas-pda-p2-p5'
const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true }
const PNG = Uint8Array.from(Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64'))
function id(value) { return value == null ? '' : String(value) }
function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' }
function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' }
function conflict(response) {
return (response?.http === 409 || (response?.http === 200
&& ['409', 'A0409'].includes(String(response?.body?.code))))
&& nonBlank(response?.requestId) && nonBlank(response?.traceId)
}
function trace(response) {
return `HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}`
}
function records(value) { return Array.isArray(value?.records) ? value.records : Array.isArray(value) ? value : [] }
function key(context, action) {
return `${context.marker}-PDAOPS-${action}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 120)
}
function query(path, values) {
const params = new URLSearchParams()
for (const [name, value] of Object.entries(values || {})) {
if (value !== undefined && value !== null && String(value) !== '') params.set(name, String(value))
}
return params.size ? `${path}?${params}` : path
}
function operationPallet(result, palletCode) {
return (result?.pallets || []).find((item) => item?.palletCode === palletCode)
}
function blocker(context, name, reason, response = null) {
context.addCoverageGap(MODULE, SCENARIO, name, reason, 'fixture-unavailable', { deploymentBlocker: true })
context.assert(MODULE, `${name}覆盖前置`, response, false, `${reason}; ${trace(response)}`, OPTIONS)
}
async function request(context, token, method, name, path, body = undefined) {
const response = await context.requestOverseas(method, path, body, {}, token)
context.addStep(MODULE, name, response, success(response), trace(response), OPTIONS)
return { response, data: success(response) ? response.body?.data : null }
}
async function expectConflict(context, token, method, name, path, body) {
const response = await context.requestOverseas(method, path, body, {}, token)
context.addStep(MODULE, name, response, conflict(response), trace(response), OPTIONS)
return response
}
async function resolvePallet(context, token, palletCode, name = '扫码解析托盘') {
return request(context, token, 'POST', name,
'/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode })
}
async function createLocation(context, warehouseId, suffix) {
const locationCode = `PDAOPS-${context.marker}-${suffix}`.replace(/[^A-Za-z0-9_-]/g, '').slice(-64)
const response = await context.requestOverseas('POST',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations`, {
zoneCode: 'PDAOPS', locationCode, locationType: 'STORAGE', maxLoadKg: 5000,
status: 'IDLE', remark: `${context.marker}-pda-operations-${suffix}`,
})
const locationId = id(response?.body?.data)
context.addStep(MODULE, `创建${suffix}目标库位`, response,
success(response) && /^\d+$/.test(locationId) && locationId !== '0',
`locationId=${locationId || 'missing'}, locationCode=${locationCode}; ${trace(response)}`, OPTIONS)
return success(response) ? { locationId, locationCode } : null
}
async function createEmptyPallet(context, token, fixture, suffix, destinationWarehouseCode) {
const palletCode = `PDAOPS-${context.marker}-${suffix}`.replace(/[^A-Za-z0-9_-]/g, '').slice(-120)
const response = await context.requestOverseas('POST',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(fixture.overseasWarehouseId)}/pallets`, {
palletCode, palletType: '木托盘', capacityCbm: 10, unitPrice: 0, status: 'IDLE',
locationId: fixture.location.locationId,
fbaWarehouseCode: destinationWarehouseCode || null,
remark: `${context.marker}-pda-operations-${suffix}`,
})
context.addStep(MODULE, `创建${suffix}辅助空托盘`, response, success(response), trace(response), OPTIONS)
if (!success(response)) return null
return (await resolvePallet(context, token, palletCode, `解析${suffix}辅助空托盘`)).data
}
async function verifyStocktake(context, token, fixture) {
const path = '/api/pda/v1/last-leg/stocktakes'
const body = { scopeType: 'PALLET', scopeCode: fixture.pallet.palletCode,
idempotencyKey: key(context, 'STOCKTAKE-CREATE') }
const created = await request(context, token, 'POST', 'P2按PALLET创建盘点任务', path, body)
const taskNo = id(created.data?.task?.taskNo)
if (!taskNo || !Array.isArray(created.data?.items) || created.data.items.length === 0) {
blocker(context, 'P2真实库存盘点', '本轮完成托盘没有可盘点库存位置', created.response)
return false
}
const replay = await request(context, token, 'POST', 'P2创建盘点同键重放', path, body)
context.assert(MODULE, 'P2创建重放复用同一任务', replay.response,
id(replay.data?.task?.taskNo) === taskNo, trace(replay.response), OPTIONS)
await expectConflict(context, token, 'POST', 'P2创建盘点同键异载荷A0409', path,
{ ...body, scopeCode: `${body.scopeCode}-DIFFERENT` })
for (const [index, item] of created.data.items.entries()) {
const countPath = `${path}/${encodeURIComponent(taskNo)}/counts`
const countBody = { positionId: id(item.positionId), actualPackages: Number(item.bookPackages),
expectedVersion: Number(item.version), idempotencyKey: key(context, `STOCKTAKE-COUNT-${index}`) }
const counted = await request(context, token, 'POST', `P2库存项${index + 1}按原数计数`, countPath, countBody)
context.assert(MODULE, `P2库存项${index + 1}零差异`, counted.response,
Number(counted.data?.actualPackages) === Number(item.bookPackages)
&& Number(counted.data?.differencePackages) === 0,
`positionId=${id(item.positionId)}, book=${item.bookPackages}; ${trace(counted.response)}`, OPTIONS)
const countReplay = await request(context, token, 'POST', `P2库存项${index + 1}计数同键重放`, countPath, countBody)
context.assert(MODULE, `P2库存项${index + 1}重放结果稳定`, countReplay.response,
id(countReplay.data?.positionId) === id(item.positionId)
&& Number(countReplay.data?.actualPackages) === Number(item.bookPackages), trace(countReplay.response), OPTIONS)
await expectConflict(context, token, 'POST', `P2库存项${index + 1}计数同键异载荷A0409`, countPath,
{ ...countBody, actualPackages: Number(item.bookPackages) + 1 })
await expectConflict(context, token, 'POST', `P2库存项${index + 1}旧版本A0409`, countPath,
{ ...countBody, idempotencyKey: key(context, `STOCKTAKE-COUNT-STALE-${index}`) })
}
const detail = await request(context, token, 'GET', 'P2回查全部盘点项已计数',
`${path}/${encodeURIComponent(taskNo)}`)
const taskVersion = Number(detail.data?.task?.version)
context.assert(MODULE, 'P2盘点任务计数守恒', detail.response,
detail.data?.task?.status === 'IN_PROGRESS'
&& Number(detail.data?.task?.countedCount) === detail.data?.items?.length
&& detail.data.items.every((item) => Number(item.actualPackages) === Number(item.bookPackages)),
`counted=${detail.data?.task?.countedCount ?? 'missing'}, items=${detail.data?.items?.length ?? 'missing'}; ${trace(detail.response)}`, OPTIONS)
await expectConflict(context, token, 'POST', 'P2提交盘点旧任务版本A0409',
`${path}/${encodeURIComponent(taskNo)}/submit`, {
expectedVersion: Math.max(0, taskVersion - 1), reason: `${context.marker}-stale`,
idempotencyKey: key(context, 'STOCKTAKE-SUBMIT-STALE'),
})
const submitBody = { expectedVersion: taskVersion, reason: `${context.marker}-matched`,
idempotencyKey: key(context, 'STOCKTAKE-SUBMIT') }
const submitted = await request(context, token, 'POST', 'P2提交盘点到SUBMITTED',
`${path}/${encodeURIComponent(taskNo)}/submit`, submitBody)
context.assert(MODULE, 'P2盘点提交状态正确且无差异', submitted.response,
submitted.data?.task?.status === 'SUBMITTED' && Number(submitted.data?.task?.differenceCount) === 0,
trace(submitted.response), OPTIONS)
const submitReplay = await request(context, token, 'POST', 'P2提交盘点同键重放',
`${path}/${encodeURIComponent(taskNo)}/submit`, submitBody)
context.assert(MODULE, 'P2提交重放仍为SUBMITTED', submitReplay.response,
submitReplay.data?.task?.status === 'SUBMITTED', trace(submitReplay.response), OPTIONS)
const page = await request(context, token, 'GET', 'P2分页查询已提交盘点',
`${path}?page=1&size=20&status=SUBMITTED`)
context.assert(MODULE, 'P2列表和详情均包含本轮任务', page.response,
records(page.data).some((item) => id(item?.taskNo) === taskNo),
`taskNo=${taskNo}, records=${records(page.data).length}; ${trace(page.response)}`, OPTIONS)
return success(submitted.response)
}
async function verifyInventoryMove(context, token, fixture) {
const target = await createLocation(context, fixture.overseasWarehouseId, 'MOVE')
if (!target) return false
let pallet = (await resolvePallet(context, token, fixture.pallet.palletCode, 'P3移库前解析整托')).data
if (!pallet || !nonBlank(pallet.locationCode)) {
blocker(context, 'P3整托移库', '本轮完成托盘无法解析当前位置')
return false
}
const originalLocationCode = pallet.locationCode
const path = '/api/pda/v1/last-leg/inventory-moves'
const body = { sourcePalletCode: pallet.palletCode, targetLocationCode: target.locationCode,
expectedPalletVersion: Number(pallet.palletVersion), idempotencyKey: key(context, 'MOVE-FORWARD'),
reason: `${context.marker}-move-forward` }
const moved = await request(context, token, 'POST', 'P3整托移到目标库位', path, body)
context.assert(MODULE, 'P3整托移库数量和目标位置正确', moved.response,
moved.data?.status === 'COMPLETED' && moved.data?.targetLocationCode === target.locationCode
&& Number(moved.data?.packageCount) > 0, trace(moved.response), OPTIONS)
const replay = await request(context, token, 'POST', 'P3整托移库同键重放', path, body)
context.assert(MODULE, 'P3移库重放标记且结果稳定', replay.response,
replay.data?.replayed === true && replay.data?.moveNo === moved.data?.moveNo, trace(replay.response), OPTIONS)
await expectConflict(context, token, 'POST', 'P3移库同键异载荷A0409', path,
{ ...body, reason: `${body.reason}-DIFFERENT` })
await expectConflict(context, token, 'POST', 'P3移库旧托盘版本A0409', path, {
...body, targetLocationCode: originalLocationCode, idempotencyKey: key(context, 'MOVE-STALE'),
})
pallet = (await resolvePallet(context, token, pallet.palletCode, 'P3回查托盘已位于目标库位')).data
context.assert(MODULE, 'P3托盘与库存位置同步到目标库位', moved.response,
pallet?.locationCode === target.locationCode && Number(pallet?.totalPackages) > 0,
`location=${pallet?.locationCode || 'missing'}; ${trace(moved.response)}`, OPTIONS)
const restoreBody = { sourcePalletCode: pallet.palletCode, targetLocationCode: originalLocationCode,
expectedPalletVersion: Number(pallet.palletVersion), idempotencyKey: key(context, 'MOVE-RESTORE'),
reason: `${context.marker}-move-restore` }
const restored = await request(context, token, 'POST', 'P3整托移回原库位供后续验证', path, restoreBody)
return success(restored.response) && restored.data?.targetLocationCode === originalLocationCode
}
async function replayOperation(context, token, name, path, body, expectedType) {
const first = await request(context, token, 'POST', name, path, body)
const replay = await request(context, token, 'POST', `${name}同键重放`, path, body)
context.assert(MODULE, `${name}重放结果稳定`, replay.response,
replay.data?.replayed === true && replay.data?.operationId === first.data?.operationId
&& replay.data?.operationType === expectedType,
trace(replay.response), OPTIONS)
return first
}
async function verifyPalletOperations(context, token, fixture) {
const base = '/api/pda/v1/last-leg/pallet-operations'
let main = (await resolvePallet(context, token, fixture.pallet.palletCode, 'P4 resolve主托盘')).data
if (!main || !Array.isArray(main.inventory) || main.inventory.length === 0) {
blocker(context, 'P4托盘操作', '主托盘没有可移动库存行')
return false
}
if (main.inventory.reduce((total, line) => total + Number(line.availablePackages || 0), 0) <= 1) {
blocker(context, 'P4解绑后主托盘可继续验证',
'主托盘只有一箱可用库存,解绑会清空唯一业务托盘;必须由本轮生产矩阵提供至少两箱')
return false
}
const swapTarget = await createEmptyPallet(context, token, fixture, 'SWAP', main.destinationWarehouseCode)
const splitTarget = await createEmptyPallet(context, token, fixture, 'SPLIT', main.destinationWarehouseCode)
const unbindTarget = await createEmptyPallet(context, token, fixture, 'UNBIND', main.destinationWarehouseCode)
if (!swapTarget || !splitTarget || !unbindTarget) {
blocker(context, 'P4辅助实物托盘', '无法创建换托、拆托和解绑所需辅助托盘')
return false
}
const swapBody = { idempotencyKey: key(context, 'PALLET-SWAP'),
source: { palletCode: main.palletCode, expectedVersion: Number(main.palletVersion) },
target: { palletCode: swapTarget.palletCode, expectedVersion: Number(swapTarget.palletVersion) },
reason: `${context.marker}-swap` }
const swapped = await replayOperation(context, token, 'P4整托换托', `${base}/swap`, swapBody, 'PALLET_SWAP')
await expectConflict(context, token, 'POST', 'P4换托同键异载荷A0409', `${base}/swap`,
{ ...swapBody, reason: `${swapBody.reason}-DIFFERENT` })
let occupiedSwap = operationPallet(swapped.data, swapTarget.palletCode)
|| (await resolvePallet(context, token, swapTarget.palletCode, 'P4换托后解析目标托盘')).data
let emptyMain = operationPallet(swapped.data, main.palletCode)
|| (await resolvePallet(context, token, main.palletCode, 'P4换托后解析原托盘')).data
await expectConflict(context, token, 'POST', 'P4换托旧托盘版本A0409', `${base}/swap`, {
...swapBody, idempotencyKey: key(context, 'PALLET-SWAP-STALE'),
source: { ...swapBody.source }, target: { ...swapBody.target },
})
const swapBackBody = { idempotencyKey: key(context, 'PALLET-SWAP-BACK'),
source: { palletCode: occupiedSwap.palletCode, expectedVersion: Number(occupiedSwap.palletVersion) },
target: { palletCode: emptyMain.palletCode, expectedVersion: Number(emptyMain.palletVersion) },
reason: `${context.marker}-swap-back` }
await replayOperation(context, token, 'P4换托恢复主托盘', `${base}/swap`, swapBackBody, 'PALLET_SWAP')
main = (await resolvePallet(context, token, main.palletCode, 'P4拆托前刷新主托盘')).data
const sourceLine = main.inventory.find((line) => Number(line.availablePackages) > 1)
|| main.inventory.find((line) => Number(line.availablePackages) > 0)
const onePackage = 1
const disassembleBody = { idempotencyKey: key(context, 'PALLET-DISASSEMBLE'),
source: { palletCode: main.palletCode, expectedVersion: Number(main.palletVersion) },
targets: [{ target: { palletCode: splitTarget.palletCode, expectedVersion: Number(splitTarget.palletVersion) },
lines: [{ positionId: id(sourceLine.positionId), packages: onePackage,
expectedPositionVersion: Number(sourceLine.positionVersion) }] }],
reason: `${context.marker}-disassemble` }
const disassembled = await replayOperation(context, token, 'P4拆托一箱到辅助托盘',
`${base}/disassemble`, disassembleBody, 'PALLET_DISASSEMBLE')
const splitOccupied = operationPallet(disassembled.data, splitTarget.palletCode)
|| (await resolvePallet(context, token, splitTarget.palletCode, 'P4拆托后解析辅助托盘')).data
const mainAfterSplit = operationPallet(disassembled.data, main.palletCode)
|| (await resolvePallet(context, token, main.palletCode, 'P4拆托后解析主托盘')).data
const splitLine = splitOccupied.inventory.find((line) => Number(line.availablePackages) > 0)
const assembleBody = { idempotencyKey: key(context, 'PALLET-ASSEMBLE'),
target: { palletCode: mainAfterSplit.palletCode, expectedVersion: Number(mainAfterSplit.palletVersion) },
sources: [{ palletCode: splitOccupied.palletCode, expectedVersion: Number(splitOccupied.palletVersion) }],
lines: [{ positionId: id(splitLine.positionId), packages: Number(splitLine.availablePackages),
expectedPositionVersion: Number(splitLine.positionVersion) }],
reason: `${context.marker}-assemble` }
await replayOperation(context, token, 'P4合托恢复主托盘', `${base}/assemble`, assembleBody, 'PALLET_ASSEMBLE')
main = (await resolvePallet(context, token, main.palletCode, 'P4解绑前刷新主托盘')).data
const unbindSource = main.inventory.find((line) => Number(line.availablePackages) > 1)
|| main.inventory.find((line) => Number(line.availablePackages) > 0)
const unbindPreparedBody = { idempotencyKey: key(context, 'PALLET-UNBIND-PREPARE'),
source: { palletCode: main.palletCode, expectedVersion: Number(main.palletVersion) },
targets: [{ target: { palletCode: unbindTarget.palletCode, expectedVersion: Number(unbindTarget.palletVersion) },
lines: [{ positionId: id(unbindSource.positionId), packages: 1,
expectedPositionVersion: Number(unbindSource.positionVersion) }] }],
reason: `${context.marker}-unbind-prepare` }
const prepared = await replayOperation(context, token, 'P4拆出一箱作为解绑样本',
`${base}/disassemble`, unbindPreparedBody, 'PALLET_DISASSEMBLE')
const boundAuxiliary = operationPallet(prepared.data, unbindTarget.palletCode)
|| (await resolvePallet(context, token, unbindTarget.palletCode, 'P4解析解绑样本托盘')).data
const unbindBody = { idempotencyKey: key(context, 'PALLET-UNBIND'),
source: { palletCode: boundAuxiliary.palletCode, expectedVersion: Number(boundAuxiliary.palletVersion) },
reason: `${context.marker}-unbind` }
const unbound = await replayOperation(context, token, 'P4解绑辅助托盘库存',
`${base}/unbind`, unbindBody, 'PALLET_UNBIND')
context.assert(MODULE, 'P4主托盘保留可继续验证状态且辅助托盘解绑完成', unbound.response,
unbound.data?.status === 'COMPLETED'
&& operationPallet(unbound.data, boundAuxiliary.palletCode)?.status === 'IDLE',
trace(unbound.response), OPTIONS)
return success(unbound.response)
}
async function verifyRelabel(context, token, fixture) {
const customerId = id(fixture.order?.order?.customerId || context.createdResources?.phase4OverseasPda?.customerId)
const customerOrderNo = fixture.order?.order?.orderNo || fixture.order?.order?.sourceOrderNo
|| `PDA-${context.marker}`
const inventory = fixture.inventory.find((item) => nonBlank(item?.skuCode))
if (!customerId || !inventory?.skuCode || !fixture.overseasWarehouseId) {
blocker(context, 'P5本轮库存换标申请', '缺少本轮完成库存的客户、SKU或海外仓事实')
return false
}
const relabelBase = '/api/platform/v1/last-leg/relabel-applications'
const pdaBase = '/api/pda/v1/last-leg/relabel-tasks'
const newSku = `${inventory.skuCode}-NEW-${String(context.marker).slice(-8)}`.slice(0, 128)
const createBody = {
customerOrderNo: String(customerOrderNo).slice(0, 128), overseasWarehouseId: id(fixture.overseasWarehouseId),
labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19),
contactName: 'PDA换标验收', contactPhone: '13800000006', remark: context.marker,
idempotencyKey: key(context, 'RELABEL-CREATE'),
items: [{ skuCode: inventory.skuCode, productNameZh: inventory.productName || '本轮库存商品',
totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128),
newSkuCode: newSku, labelRequirement: context.marker }],
}
const createdResponse = await context.requestOverseas('POST',
query(relabelBase, { customerId }), createBody)
const created = success(createdResponse) ? createdResponse.body?.data : null
const relabelNo = id(created?.application?.relabelNo)
context.addStep(MODULE, 'P5基于本轮完成库存创建真实换标申请', createdResponse,
success(createdResponse) && nonBlank(relabelNo)
&& created?.application?.status === 'PENDING_PROCESSING',
`relabelNo=${relabelNo || 'missing'}, sku=${inventory.skuCode}; ${trace(createdResponse)}`, OPTIONS)
if (!relabelNo) {
blocker(context, 'P5真实换标申请', '平台正式换标创建接口未返回申请单号', createdResponse)
return false
}
const candidates = await request(context, token, 'GET', 'P5读取当前仓换标候选', `${pdaBase}/candidates`)
context.assert(MODULE, 'P5候选包含本轮真实申请', candidates.response,
Array.isArray(candidates.data) && candidates.data.some((item) => id(item?.relabelNo) === relabelNo),
`relabelNo=${relabelNo}; ${trace(candidates.response)}`, OPTIONS)
const available = await request(context, token, 'GET', 'P5读取换标任务详情',
`${pdaBase}/${encodeURIComponent(relabelNo)}`)
const claimBody = { idempotencyKey: key(context, 'RELABEL-CLAIM') }
const claimed = await request(context, token, 'POST', 'P5认领换标任务',
`${pdaBase}/${encodeURIComponent(relabelNo)}/claim`, claimBody)
const claimReplay = await request(context, token, 'POST', 'P5认领换标同键重放',
`${pdaBase}/${encodeURIComponent(relabelNo)}/claim`, claimBody)
context.assert(MODULE, 'P5认领重放保持PROCESSING', claimReplay.response,
claimReplay.data?.taskStatus === 'PROCESSING' && claimed.data?.taskStatus === 'PROCESSING',
trace(claimReplay.response), OPTIONS)
let detail = claimed.data || available.data
const item = detail?.items?.[0]
if (!id(item?.itemId)) {
blocker(context, 'P5换标明细', '认领后的换标任务没有真实明细', claimed.response)
return false
}
const unitPath = `${pdaBase}/${encodeURIComponent(relabelNo)}/units`
const scanBody = { itemId: id(item.itemId), oldLabel: `${context.marker}-OLD-1`, newLabel: newSku,
expectedVersion: Number(detail.version), idempotencyKey: key(context, 'RELABEL-NORMAL') }
const scanned = await request(context, token, 'POST', 'P5正常换标一箱', unitPath, scanBody)
const scanReplay = await request(context, token, 'POST', 'P5正常换标同键重放', unitPath, scanBody)
context.assert(MODULE, 'P5正常换标重放不重复计数', scanReplay.response,
Number(scanReplay.data?.relabelledPackages) === 1, trace(scanReplay.response), OPTIONS)
await expectConflict(context, token, 'POST', 'P5正常换标同键异载荷A0409', unitPath,
{ ...scanBody, oldLabel: `${context.marker}-OLD-DIFFERENT` })
await expectConflict(context, token, 'POST', 'P5正常换标旧版本A0409', unitPath,
{ ...scanBody, oldLabel: `${context.marker}-OLD-STALE`, idempotencyKey: key(context, 'RELABEL-STALE') })
detail = scanned.data
const skipBody = { itemId: id(item.itemId), oldLabel: `${context.marker}-OLD-2`,
reason: `${context.marker}-skip`, expectedVersion: Number(detail.version),
idempotencyKey: key(context, 'RELABEL-SKIP') }
const skipped = await request(context, token, 'POST', 'P5跳过换标一箱', `${unitPath}/skip`, skipBody)
const skipReplay = await request(context, token, 'POST', 'P5跳过换标同键重放', `${unitPath}/skip`, skipBody)
context.assert(MODULE, 'P5跳过换标重放不重复计数', skipReplay.response,
Number(skipReplay.data?.skippedPackages) === 1, trace(skipReplay.response), OPTIONS)
detail = skipped.data
const exceptionBody = { itemId: id(item.itemId), oldLabel: `${context.marker}-OLD-3`,
exceptionCode: 'DAMAGED', reason: `${context.marker}-damaged`, expectedVersion: Number(detail.version),
idempotencyKey: key(context, 'RELABEL-EXCEPTION') }
const exception = await request(context, token, 'POST', 'P5登记换标异常一箱',
`${pdaBase}/${encodeURIComponent(relabelNo)}/exceptions`, exceptionBody)
const exceptionReplay = await request(context, token, 'POST', 'P5换标异常同键重放',
`${pdaBase}/${encodeURIComponent(relabelNo)}/exceptions`, exceptionBody)
context.assert(MODULE, 'P5换标异常重放不重复计数', exceptionReplay.response,
Number(exceptionReplay.data?.exceptionPackages) === 1, trace(exceptionReplay.response), OPTIONS)
detail = exception.data
const photo = await context.requestOverseasMultipart('POST',
`${pdaBase}/${encodeURIComponent(relabelNo)}/photos/files`, {
name: `${context.marker}-relabel.png`, type: 'image/png', content: PNG,
}, token)
const photoId = id(photo?.body?.data?.fileId)
context.addStep(MODULE, 'P5上传换标完成照片', photo,
success(photo) && /^\d+$/.test(photoId) && photoId !== '0',
`fileId=${photoId || 'missing'}; ${trace(photo)}`, OPTIONS)
if (!photoId) return false
const completePath = `${pdaBase}/${encodeURIComponent(relabelNo)}/complete`
const completeBody = { photoFileIds: [photoId], expectedVersion: Number(detail.version),
idempotencyKey: key(context, 'RELABEL-COMPLETE-1'), remark: `${context.marker}-complete-1` }
const completed = await request(context, token, 'POST', 'P5首次完成换标进入WAIT_REVIEW', completePath, completeBody)
const completeReplay = await request(context, token, 'POST', 'P5首次完成同键重放', completePath, completeBody)
context.assert(MODULE, 'P5首次完成重放保持WAIT_REVIEW', completeReplay.response,
completeReplay.data?.taskStatus === 'WAIT_REVIEW', trace(completeReplay.response), OPTIONS)
const reviewPath = `${pdaBase}/${encodeURIComponent(relabelNo)}/review`
const reworkBody = { decision: 'REWORK', expectedVersion: Number(completed.data?.version),
idempotencyKey: key(context, 'RELABEL-REWORK'), remark: `${context.marker}-rework` }
const reworked = await request(context, token, 'POST', 'P5复核退回REWORK', reviewPath, reworkBody)
const reworkReplay = await request(context, token, 'POST', 'P5 REWORK同键重放', reviewPath, reworkBody)
context.assert(MODULE, 'P5 REWORK重放保持PROCESSING', reworkReplay.response,
reworkReplay.data?.taskStatus === 'PROCESSING', trace(reworkReplay.response), OPTIONS)
const secondCompleteBody = { photoFileIds: [photoId], expectedVersion: Number(reworked.data?.version),
idempotencyKey: key(context, 'RELABEL-COMPLETE-2'), remark: `${context.marker}-complete-2` }
const secondCompleted = await request(context, token, 'POST', 'P5返工后再次完成WAIT_REVIEW',
completePath, secondCompleteBody)
const secondReplay = await request(context, token, 'POST', 'P5再次完成同键重放', completePath, secondCompleteBody)
context.assert(MODULE, 'P5再次完成重放保持WAIT_REVIEW', secondReplay.response,
secondReplay.data?.taskStatus === 'WAIT_REVIEW', trace(secondReplay.response), OPTIONS)
const approveBody = { decision: 'APPROVE', expectedVersion: Number(secondCompleted.data?.version),
idempotencyKey: key(context, 'RELABEL-APPROVE'), remark: `${context.marker}-approved` }
const approved = await request(context, token, 'POST', 'P5复核批准换标完成', reviewPath, approveBody)
const approveReplay = await request(context, token, 'POST', 'P5批准同键重放', reviewPath, approveBody)
context.assert(MODULE, 'P5最终任务和申请均COMPLETED', approveReplay.response,
approved.data?.taskStatus === 'COMPLETED' && approved.data?.applicationStatus === 'COMPLETED'
&& approveReplay.data?.taskStatus === 'COMPLETED', trace(approveReplay.response), OPTIONS)
return success(approved.response)
}
export async function run(context) {
const handoff = context.context?.phase4OverseasPdaOperations
const token = handoff?.primary?.token
try {
if (handoff?.marker !== context.marker || !nonBlank(token)
|| !Array.isArray(handoff.completedFixtures) || handoff.completedFixtures.length === 0) {
blocker(context, 'P2-P5本轮PDA收货交接',
'phase4-overseas-pda未交接本轮primary picker会话与已完成库存事实')
return
}
const fixture = handoff.completedFixtures.find((item) => Array.isArray(item?.inventory)
&& item.inventory.some((line) => Number(line?.availablePackages) > 1))
|| handoff.completedFixtures.find((item) => Array.isArray(item?.inventory)
&& item.inventory.some((line) => Number(line?.availablePackages) > 0))
if (!fixture) {
blocker(context, 'P2-P5可操作库存', '本轮已完成柜没有可移动的真实库存位置')
return
}
fixture.overseasWarehouseId = id(handoff.overseasWarehouseId)
await verifyStocktake(context, token, fixture)
await verifyInventoryMove(context, token, fixture)
await verifyPalletOperations(context, token, fixture)
await verifyRelabel(context, token, fixture)
} finally {
try {
if (typeof handoff?.cleanup === 'function') await handoff.cleanup()
} finally {
if (handoff?.primary) delete handoff.primary.token
if (handoff?.secondary) delete handoff.secondary.token
delete context.context.phase4OverseasPdaOperations
}
}
}