fix: reuse overseas receiving resources in acceptance

This commit is contained in:
2026-09-11 00:48:21 +08:00
parent 9abb0004dd
commit 7dff9b57ed
8 changed files with 107 additions and 85 deletions
@@ -90,47 +90,29 @@ async function resolvePallet(context, token, palletCode, 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 resolvePublicLocation(context, fixture) {
const response = await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(fixture.overseasWarehouseId)}/locations?page=1&size=200`)
const location = records(response?.body?.data).find((item) => item?.locationType === 'VIRTUAL'
&& item?.status === 'ACTIVE')
context.addStep(MODULE, '复用当前海外仓公共库位', response,
success(response) && longId(id(location?.id)) && nonBlank(location?.locationCode),
`locationId=${id(location?.id) || 'missing'}, locationCode=${location?.locationCode || 'missing'}; ${trace(response)}`, OPTIONS)
return success(response) && location
? { locationId: id(location.id), locationCode: location.locationCode } : null
}
export function auxiliaryPalletFbaWarehouseCode(destinationType, fbaWarehouseCode) {
if (destinationType === 'OVERSEAS') return null
if (destinationType === 'FBA' && nonBlank(fbaWarehouseCode)) return fbaWarehouseCode.trim()
return null
}
async function createEmptyPallet(context, token, fixture, suffix) {
const fbaWarehouseCode = auxiliaryPalletFbaWarehouseCode(
fixture.destinationType, fixture.fbaWarehouseCode)
if (fixture.destinationType === 'FBA' && !fbaWarehouseCode) {
blocker(context, `${suffix}辅助空托盘`,
`当前FBA入仓码 ${fixture.inboundCode || 'missing'} 缺少权威fbaWarehouseCode`)
return null
async function resolveReusableAuxiliaryPallet(context, token, fixture) {
for (const candidate of fixture.auxiliaryPallets || []) {
if (!nonBlank(candidate?.palletCode) || id(candidate?.palletId) === id(fixture.pallet?.palletId)) continue
const resolved = await resolvePallet(context, token, candidate.palletCode, '解析入仓计划已有空托盘')
if (resolved.data && Array.isArray(resolved.data.inventory) && resolved.data.inventory.length === 0) {
context.assert(MODULE, '辅助托盘只复用本轮入仓计划已有空托盘', resolved.response, true,
`palletCode=${resolved.data.palletCode}; ${trace(resolved.response)}`, OPTIONS)
return resolved.data
}
}
const location = await createLocation(context, fixture.overseasWarehouseId, `${suffix}-PALLET`)
if (!location) return null
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: location.locationId,
fbaWarehouseCode,
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
return null
}
async function verifyStocktake(context, token, fixture) {
@@ -201,7 +183,7 @@ async function verifyStocktake(context, token, fixture) {
}
async function verifyInventoryMove(context, token, fixture) {
const target = await createLocation(context, fixture.overseasWarehouseId, 'MOVE')
const target = await resolvePublicLocation(context, fixture)
if (!target) return false
let pallet = (await resolvePallet(context, token, fixture.pallet.palletCode, 'P3移库前解析整托')).data
if (!pallet || !nonBlank(pallet.locationCode)) {
@@ -209,6 +191,14 @@ async function verifyInventoryMove(context, token, fixture) {
return false
}
const originalLocationCode = pallet.locationCode
if (originalLocationCode === target.locationCode) {
const availablePackages = (pallet.inventory || []).reduce((sum, item) =>
sum + Number(item?.availablePackages || 0), 0)
context.assert(MODULE, 'P3托盘已在公共库位不重复移库', null,
availablePackages > 0,
`palletCode=${pallet.palletCode}, locationCode=${target.locationCode}, availablePackages=${availablePackages}`, OPTIONS)
return availablePackages > 0
}
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'),
@@ -258,23 +248,21 @@ async function verifyPalletOperations(context, token, fixture) {
'主托盘只有一箱可用库存,解绑会清空唯一业务托盘;必须由本轮生产矩阵提供至少两箱')
return false
}
const swapTarget = await createEmptyPallet(context, token, fixture, 'SWAP')
const splitTarget = await createEmptyPallet(context, token, fixture, 'SPLIT')
const unbindTarget = await createEmptyPallet(context, token, fixture, 'UNBIND')
if (!swapTarget || !splitTarget || !unbindTarget) {
blocker(context, 'P4辅助实物托盘', '无法创建换托、拆托和解绑所需辅助托盘')
let auxiliary = await resolveReusableAuxiliaryPallet(context, token, fixture)
if (!auxiliary) {
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) },
target: { palletCode: auxiliary.palletCode, expectedVersion: Number(auxiliary.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 occupiedSwap = operationPallet(swapped.data, auxiliary.palletCode)
|| (await resolvePallet(context, token, auxiliary.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`, {
@@ -288,19 +276,20 @@ async function verifyPalletOperations(context, token, fixture) {
await replayOperation(context, token, 'P4换托恢复主托盘', `${base}/swap`, swapBackBody, 'PALLET_SWAP')
main = (await resolvePallet(context, token, main.palletCode, 'P4拆托前刷新主托盘')).data
auxiliary = (await resolvePallet(context, token, auxiliary.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) },
targets: [{ target: { palletCode: auxiliary.palletCode, expectedVersion: Number(auxiliary.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 splitOccupied = operationPallet(disassembled.data, auxiliary.palletCode)
|| (await resolvePallet(context, token, auxiliary.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)
@@ -313,18 +302,19 @@ async function verifyPalletOperations(context, token, fixture) {
await replayOperation(context, token, 'P4合托恢复主托盘', `${base}/assemble`, assembleBody, 'PALLET_ASSEMBLE')
main = (await resolvePallet(context, token, main.palletCode, 'P4解绑前刷新主托盘')).data
auxiliary = (await resolvePallet(context, token, auxiliary.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) },
targets: [{ target: { palletCode: auxiliary.palletCode, expectedVersion: Number(auxiliary.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 boundAuxiliary = operationPallet(prepared.data, auxiliary.palletCode)
|| (await resolvePallet(context, token, auxiliary.palletCode, 'P4解析解绑样本托盘')).data
const unbindBody = { idempotencyKey: key(context, 'PALLET-UNBIND'),
source: { palletCode: boundAuxiliary.palletCode, expectedVersion: Number(boundAuxiliary.palletVersion) },
reason: `${context.marker}-unbind` }
@@ -393,6 +383,7 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
customerOrderNo: String(customerOrderNo).slice(0, 128), operationWarehouseId: id(fixture.overseasWarehouseId),
labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19),
contactName: 'PDA换标验收', contactPhone: '13800000006', chargeAmount: 0.01, remark: context.marker,
deliveryMethod: 'TRUCK', printShippingLabel: false,
idempotencyKey: key(context, 'RELABEL-CREATE'),
items: sourceLines.map(({ source, position, packageCount }, index) => {
const newSkuCode = `${source.skuCode}-NEW-${markerSuffix}-${index + 1}`.slice(0, 128)