fix(acceptance): follow reserved pallet workflows
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user