fix(acceptance): follow reserved pallet workflows

This commit is contained in:
2026-09-09 01:30:22 +08:00
parent b11ad31d5f
commit f7e3863898
7 changed files with 55 additions and 14 deletions
@@ -803,16 +803,24 @@ export async function run(context) {
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
const canonicalPick = await request(context, 'POST', batchPath, batchPayload,
const canonicalPickPath = dropshipWholePallet
? `${pdaBase}/tasks/${canonicalTaskId}/pallets/pick`
: batchPath
const canonicalPickPayload = dropshipWholePallet
? { warehouseId, palletNo: candidate.palletNo, expectedTaskVersion: Number(claimed?.version), operationId: batchKey }
: batchPayload
const canonicalPick = await request(context, 'POST', canonicalPickPath, canonicalPickPayload,
{ 'Idempotency-Key': batchKey })
const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS)
const pickedCanonical = context.apiSuccess(MODULE,
dropshipWholePallet ? '扫描一件代发封托托盘并整体拣货' : '原子提交柜次 PDA 整批拣货',
canonicalPick, '', OPTIONS)
const awaitingReview = pickedCanonical?.taskStatus === 'LOADING'
&& pickedCanonical?.workflowStatus === 'AWAITING_REVIEW'
context.assert(MODULE, 'PDA 原子批次完成拣货并等待人工确认', canonicalPick,
awaitingReview && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
const canonicalBatchReplay = await request(context, 'POST', batchPath, batchPayload,
const canonicalBatchReplay = await request(context, 'POST', canonicalPickPath, canonicalPickPayload,
{ 'Idempotency-Key': batchKey })
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
@@ -824,12 +832,13 @@ export async function run(context) {
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
const preFinalOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前原子 Batch 审计', preFinalOperationsResponse, '', OPTIONS)
context.assert(MODULE, '最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING', preFinalOperationsResponse,
const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前拣货审计', preFinalOperationsResponse, '', OPTIONS)
const canonicalPickAction = dropshipWholePallet ? 'PICK_PALLET' : 'BATCH'
context.assert(MODULE, '最终确认前仅保留领取和拣货审计且无 COMPLETE_LOADING', preFinalOperationsResponse,
Array.isArray(preFinalOperations)
&& preFinalOperations.some((item) => item.actionType === 'CLAIM'
&& id(item.operatorId) && Boolean(item.createdAt))
&& preFinalOperations.some((item) => item.actionType === 'BATCH'
&& preFinalOperations.some((item) => item.actionType === canonicalPickAction
&& id(item.operatorId) && Boolean(item.createdAt))
&& !preFinalOperations.some((item) => item.actionType === 'COMPLETE_LOADING')
&& !preFinalOperations.some((item) => item.actionType === 'REVIEW'),
@@ -95,6 +95,9 @@ test('measurement and loading pass destination identity into the LOADED producer
assert.match(loading, /canonicalPickItems\.map/)
assert.match(loading, /pickedPackageCount: Number\(item\.plannedPackageCount\)/)
assert.match(loading, /const dropshipWholePallet = source\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
assert.match(loading, /tasks\/\$\{canonicalTaskId\}\/pallets\/pick/)
assert.match(loading, /palletNo: candidate\.palletNo/)
assert.match(loading, /canonicalPickAction = dropshipWholePallet \? 'PICK_PALLET' : 'BATCH'/)
assert.match(loading, /一件代发整托生产者不执行可拆分动态增量场景/)
assert.match(loading, /!dropshipWholePallet && Array\.isArray\(incrementalFixture\?\.skuCodes\)/)
assert.match(pda, /find\(\(item\) => item\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'\)/)
@@ -541,8 +541,8 @@ test('phase3 loading proves stale inventory zero-write and defers final completi
assert.equal((loading.match(/id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/g) || []).length, 2)
assert.doesNotMatch(loading, /items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
assertOrder(loading, [
'回查下游门禁前原子 Batch 审计',
'最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING',
'回查下游门禁前拣货审计',
'最终确认前仅保留领取和拣货审计且无 COMPLETE_LOADING',
'LOADING 阶段向同一 PDA task 增加入库单',
'增量后旧 inventory token 明确 A0409 并要求刷新',
'增量旧 token 冲突后 plan/task/items 零写入',
@@ -33,6 +33,10 @@ test('PDA module covers the container-number receiving contract and excludes out
assert.match(source, /inbound-plans\/container-preview/)
assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/)
assert.match(source, /snapshotHash: String\(preview\.snapshotHash\)/)
assert.match(source, /reservedPalletsByInbound/)
assert.match(source, /warehouseResourceLockId/)
assert.match(source, /preferredProducer\?\.receivingPalletsByInbound\?\.\[inbound\.inboundCode\]/)
assert.match(source, /复用整柜转换预留托盘/)
assert.match(source, /同键异载荷冲突/)
assert.match(source, /跨柜切换锁返回409/)
assert.match(source, /item\.operable === true && item\.allPlansReady === true/)
@@ -65,6 +65,8 @@ test('auxiliary pallets use the authoritative inbound FBA code and never the bus
assert.match(receivingSource,
/destinationType: inbound\.destinationType,[\s\S]*fbaWarehouseCode: inbound\.destinationType === 'FBA' \? inbound\.fbaWarehouseCode : null/)
assert.match(source, /fbaWarehouseCode,\s*\n\s*remark:/)
assert.match(source, /createLocation\(context, fixture\.overseasWarehouseId, `\$\{suffix\}-PALLET`\)/)
assert.match(source, /locationId: location\.locationId/)
assert.doesNotMatch(source, /createEmptyPallet\([^\n]*main\.destinationWarehouseCode/)
})
@@ -109,11 +109,13 @@ async function createEmptyPallet(context, token, fixture, suffix) {
`当前FBA入仓码 ${fixture.inboundCode || 'missing'} 缺少权威fbaWarehouseCode`)
return null
}
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: fixture.location.locationId,
locationId: location.locationId,
fbaWarehouseCode,
remark: `${context.marker}-pda-operations-${suffix}`,
})
@@ -332,12 +332,28 @@ async function convertReceivingContainers(context, producers) {
const convertedInboundCodes = new Set(plans
.flatMap((item) => Array.isArray(item?.lines) ? item.lines : [])
.map((line) => id(line?.inboundCode)).filter(nonBlank))
const reservedPalletsByInbound = {}
for (const plan of plans) {
const reserved = Array.isArray(plan?.pallets) ? plan.pallets.find((item) =>
longId(item?.warehousePalletId) && longId(item?.warehouseLocationId)
&& longId(item?.warehouseResourceLockId)
&& ['RESERVED', 'ASSIGNED', 'LOCKED'].includes(item?.assignmentStatus)) : null
for (const line of Array.isArray(plan?.lines) ? plan.lines : []) {
if (reserved && nonBlank(line?.inboundCode)) reservedPalletsByInbound[id(line.inboundCode)] = {
palletId: id(reserved.warehousePalletId), palletCode: reserved.palletNo,
locationId: id(reserved.warehouseLocationId), retained: true,
}
}
}
const reservedCoverage = [...convertedInboundCodes].every((inboundCode) =>
nonBlank(reservedPalletsByInbound[inboundCode]?.palletCode))
context.addStep(MODULE, `整柜转换为逐入仓码独立计划 ${containerNo}`, convertedResponse,
plans.length === inboundCodes
&& plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0)
&& convertedInboundCodes.size === inboundCodes,
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
if (plans.length !== inboundCodes) return false
&& convertedInboundCodes.size === inboundCodes && reservedCoverage,
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, reserved=${Object.keys(reservedPalletsByInbound).length}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
if (plans.length !== inboundCodes || !reservedCoverage) return false
producer.receivingPalletsByInbound = reservedPalletsByInbound
const replayResponse = await context.requestOverseas('POST',
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
{ 'Idempotency-Key': key }, context.overseasToken)
@@ -614,14 +630,19 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe
if (!success(afterResolve.response) || receivingSnapshot(afterResolve.data) !== beforeResolve) return
const pallets = new Map()
for (const [index, inbound] of current.data.inbounds.entries()) {
const pallet = await createPallet(context, inbound, index)
const reservedPallet = preferredProducer?.receivingPalletsByInbound?.[inbound.inboundCode]
const pallet = reservedPallet || await createPallet(context, inbound, index)
if (reservedPallet) context.addStep(MODULE, `${inbound.destinationType}复用整柜转换预留托盘`, null,
longId(reservedPallet.palletId) && nonBlank(reservedPallet.palletCode)
&& longId(reservedPallet.locationId),
`inboundCode=${inbound.inboundCode}, palletCode=${reservedPallet.palletCode}`, OPTIONS)
if (!pallet) {
blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}实物托盘`,
`无法在实际海外收货仓 ${inbound.overseasWarehouseId || 'missing'} 创建可解析真实托盘;目的仓 ${inbound.destinationWarehouseId || 'missing'} 不得代替物理仓`, 'warehouse-resource')
continue
}
pallets.set(inbound.inboundCode, pallet)
context.createdResources.phase4OverseasPda.palletIds.push(pallet.palletId)
if (!reservedPallet) context.createdResources.phase4OverseasPda.palletIds.push(pallet.palletId)
const resolved = await pda(context, token, 'POST', `解析${inbound.destinationType}实物托盘`,
'/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve', { palletCode: pallet.palletCode, inboundCode: inbound.inboundCode })
context.assert(MODULE, `${inbound.destinationType}托盘目的模式匹配`, resolved.response,