From f7e3863898039296c5ab4064bcb100ccd88a56bf Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 01:30:22 +0800 Subject: [PATCH] fix(acceptance): follow reserved pallet workflows --- .../admin-test-env-modules/phase3-loading.mjs | 21 +++++++++---- ...-phase4-producer-fixture-contract.test.mjs | 3 ++ .../phase3-producer-chain-contract.test.mjs | 4 +-- .../phase4-overseas-pda-contract.test.mjs | 4 +++ ...-overseas-pda-operations-contract.test.mjs | 2 ++ .../phase4-overseas-pda-operations.mjs | 4 ++- .../phase4-overseas-pda.mjs | 31 ++++++++++++++++--- 7 files changed, 55 insertions(+), 14 deletions(-) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 68203663c..d385265f9 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -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'), diff --git a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs index 2582c0b23..a45c18b83 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs @@ -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'\)/) diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index ba48c4721..043dd6c8d 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -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 零写入', diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs index c3ef29024..2787fceaf 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs @@ -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/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs index 1a741c306..e0f31dc7d 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations-contract.test.mjs @@ -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/) }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs index 272642a7e..584304677 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-operations.mjs @@ -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}`, }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index abf335bbd..b870b6b9e 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -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,