test(overseas): cover split relabel inventory

This commit is contained in:
2026-09-09 02:48:10 +08:00
parent 41225248d0
commit 045d0d815b
4 changed files with 57 additions and 25 deletions
@@ -183,7 +183,7 @@ test('PDA receiving lifecycle is ordered from candidate claim through photos, mo
'/current/receipts/bulk', '/current/receipts/carton',
"'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'",
'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push',
'completedContainerFacts.push', 'expectedPalletCost', 'physicalPallets', 'resolvedPallet.data?.unitPrice',
'completedContainerFacts.push', 'expectedPalletCost', 'physicalPallets', '完成入库后读取托盘计费单价',
]) assert.ok(source.includes(token), token)
})
@@ -49,7 +49,7 @@ test('P2 to P5 cover all frozen endpoints, replays and conflict boundaries witho
'SUBMITTED', '移回原库位', 'WAIT_REVIEW', 'REWORK', 'APPROVE', 'COMPLETED',
'chargeAmount: 0.01', 'cartonMark:', 'fbaCartonMark:',
'inventory-candidates', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:',
'sourceInboundOrderId:', 'operationWarehouseId:',
'sourceInboundOrderId:', 'operationWarehouseId:', 'relabelItemForUnit', 'sourceLines.map',
'P5换标费用冻结后开放PDA作业', 'P5换标完成后钱包结算收敛']) {
assert.ok(source.includes(marker), marker)
}
@@ -29,6 +29,15 @@ function operationPallet(result, palletCode) {
return (result?.pallets || []).find((item) => item?.palletCode === palletCode)
}
function relabelItemForUnit(detail, unitIndex) {
let remaining = unitIndex
for (const item of detail?.items || []) {
if (remaining < Number(item?.expectedPackages || 0)) return item
remaining -= Number(item?.expectedPackages || 0)
}
return null
}
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)
@@ -344,35 +353,51 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
}), undefined, {}, merchantToken)
const inventoryCandidates = success(inventoryCandidatesResponse)
&& Array.isArray(inventoryCandidatesResponse.body?.data) ? inventoryCandidatesResponse.body.data : []
const source = inventoryCandidates.find((item) => id(item?.inventoryBalanceId) === id(inventory?.balanceId))
|| inventoryCandidates.find((item) => item?.skuCode === inventory.skuCode
&& item?.inboundCode === fixture.inboundCode)
const sourcePosition = source?.positions?.find((item) => id(item?.positionId) === id(inventory?.positionId)
&& Number(item?.availablePackages) >= 3)
|| source?.positions?.find((item) => Number(item?.availablePackages) >= 3)
const orderedCandidates = [...inventoryCandidates].sort((left, right) => {
const leftPreferred = id(left?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
const rightPreferred = id(right?.inventoryBalanceId) === id(inventory?.balanceId) ? 1 : 0
return rightPreferred - leftPreferred
})
const sourceLines = []
let requiredPackages = 3
for (const source of orderedCandidates) {
if (!id(source?.inventoryBalanceId) || !id(source?.inboundOrderId)) continue
for (const position of source?.positions || []) {
if (!id(position?.positionId) || Number(position?.availablePackages) <= 0) continue
const packageCount = Math.min(requiredPackages, Number(position.availablePackages))
sourceLines.push({ source, position, packageCount })
requiredPackages -= packageCount
if (requiredPackages === 0) break
}
if (requiredPackages === 0) break
}
context.addStep(MODULE, 'P5客户库存候选返回换标权威来源', inventoryCandidatesResponse,
Boolean(source) && Boolean(sourcePosition) && id(source?.inboundOrderId) !== '',
`balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(sourcePosition?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
if (!source || !sourcePosition || !id(source.inboundOrderId)) {
blocker(context, 'P5本轮库存换标来源', '客户库存候选未返回本轮库存的余额、至少三箱位置和来源入库单', inventoryCandidatesResponse)
requiredPackages === 0,
`sourceLines=${sourceLines.length}, selectedPackages=${3 - requiredPackages}; ${trace(inventoryCandidatesResponse)}`, OPTIONS)
if (requiredPackages !== 0) {
blocker(context, 'P5本轮库存换标来源', '客户库存候选未返回可组合三箱的权威余额、位置和来源入库单', inventoryCandidatesResponse)
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 markerSuffix = String(context.marker).slice(-8)
const createBody = {
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,
idempotencyKey: key(context, 'RELABEL-CREATE'),
items: [{ sourceInventoryBalanceId: id(source.inventoryBalanceId),
sourceInventoryPositionId: id(sourcePosition.positionId), sourceInboundOrderId: id(source.inboundOrderId),
items: sourceLines.map(({ source, position, packageCount }, index) => ({
sourceInventoryBalanceId: id(source.inventoryBalanceId),
sourceInventoryPositionId: id(position.positionId), sourceInboundOrderId: id(source.inboundOrderId),
skuCode: source.skuCode, productNameZh: source.productNameZh || inventory.productName || '本轮库存商品',
totalPackageCount: 3, notifiedPackageCount: 3, shipmentNo: `SHIP-${context.marker}`.slice(0, 128),
newSkuCode: newSku, labelRequirement: context.marker,
cartonMark: `CM-${String(context.marker).slice(-32)}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode }],
totalPackageCount: packageCount, notifiedPackageCount: packageCount,
shipmentNo: `SHIP-${context.marker}-${index + 1}`.slice(0, 128),
newSkuCode: `${source.skuCode}-NEW-${markerSuffix}-${index + 1}`.slice(0, 128),
labelRequirement: context.marker,
cartonMark: `CM-${String(context.marker).slice(-28)}-${index + 1}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-27)}-${index + 1}`,
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
})),
}
const createdResponse = await context.requestOverseas('POST',
query(relabelBase, { customerId }), createBody)
@@ -407,13 +432,13 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
claimReplay.data?.taskStatus === 'PROCESSING' && claimed.data?.taskStatus === 'PROCESSING',
trace(claimReplay.response), OPTIONS)
let detail = claimed.data || available.data
const item = detail?.items?.[0]
const item = relabelItemForUnit(detail, 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,
const scanBody = { itemId: id(item.itemId), oldLabel: `${context.marker}-OLD-1`, newLabel: item.newSkuCode,
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)
@@ -424,7 +449,8 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
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`,
const skipItem = relabelItemForUnit(detail, 1)
const skipBody = { itemId: id(skipItem?.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)
@@ -432,7 +458,8 @@ async function verifyRelabel(context, token, merchantToken, fixture) {
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`,
const exceptionItem = relabelItemForUnit(detail, 2)
const exceptionBody = { itemId: id(exceptionItem?.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登记换标异常一箱',
@@ -212,7 +212,12 @@ async function verifyWarehouseQueries(context, token, inboundCode, pallet, desti
&& Number(resolvedPallet.data?.occupiedPackages) > 0,
`palletId=${id(resolvedPallet.data?.palletId) || 'missing'}; ${trace(resolvedPallet.response)}`, OPTIONS)
if (Number(resolvedPallet.data?.unitPrice) > 0) pallet.unitPrice = Number(resolvedPallet.data.unitPrice)
const palletDetail = await context.requestOverseas('GET',
`/api/platform/v1/warehouse-admin/${encodeURIComponent(pallet.warehouseId)}/pallets/${encodeURIComponent(pallet.palletId)}`)
context.addStep(MODULE, '完成入库后读取托盘计费单价', palletDetail,
success(palletDetail) && Number(palletDetail.body?.data?.unitPrice) > 0,
`palletId=${pallet.palletId}, unitPrice=${palletDetail.body?.data?.unitPrice ?? 'missing'}; ${trace(palletDetail)}`, OPTIONS)
if (Number(palletDetail.body?.data?.unitPrice) > 0) pallet.unitPrice = Number(palletDetail.body.data.unitPrice)
const locationCode = resolvedPallet.data?.locationCode || pallet.locationCode
const location = await pda(context, token, 'POST', '完成入库后扫码查询库位',