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),
|
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
|
||||||
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
|
`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 })
|
{ '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'
|
const awaitingReview = pickedCanonical?.taskStatus === 'LOADING'
|
||||||
&& pickedCanonical?.workflowStatus === 'AWAITING_REVIEW'
|
&& pickedCanonical?.workflowStatus === 'AWAITING_REVIEW'
|
||||||
context.assert(MODULE, 'PDA 原子批次完成拣货并等待人工确认', canonicalPick,
|
context.assert(MODULE, 'PDA 原子批次完成拣货并等待人工确认', canonicalPick,
|
||||||
awaitingReview && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
|
awaitingReview && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
|
||||||
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
|
`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 })
|
{ 'Idempotency-Key': batchKey })
|
||||||
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
|
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
|
||||||
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
|
&& 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)
|
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
const preFinalOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
const preFinalOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
||||||
const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前原子 Batch 审计', preFinalOperationsResponse, '', OPTIONS)
|
const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前拣货审计', preFinalOperationsResponse, '', OPTIONS)
|
||||||
context.assert(MODULE, '最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING', preFinalOperationsResponse,
|
const canonicalPickAction = dropshipWholePallet ? 'PICK_PALLET' : 'BATCH'
|
||||||
|
context.assert(MODULE, '最终确认前仅保留领取和拣货审计且无 COMPLETE_LOADING', preFinalOperationsResponse,
|
||||||
Array.isArray(preFinalOperations)
|
Array.isArray(preFinalOperations)
|
||||||
&& preFinalOperations.some((item) => item.actionType === 'CLAIM'
|
&& preFinalOperations.some((item) => item.actionType === 'CLAIM'
|
||||||
&& id(item.operatorId) && Boolean(item.createdAt))
|
&& id(item.operatorId) && Boolean(item.createdAt))
|
||||||
&& preFinalOperations.some((item) => item.actionType === 'BATCH'
|
&& preFinalOperations.some((item) => item.actionType === canonicalPickAction
|
||||||
&& id(item.operatorId) && Boolean(item.createdAt))
|
&& id(item.operatorId) && Boolean(item.createdAt))
|
||||||
&& !preFinalOperations.some((item) => item.actionType === 'COMPLETE_LOADING')
|
&& !preFinalOperations.some((item) => item.actionType === 'COMPLETE_LOADING')
|
||||||
&& !preFinalOperations.some((item) => item.actionType === 'REVIEW'),
|
&& !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, /canonicalPickItems\.map/)
|
||||||
assert.match(loading, /pickedPackageCount: Number\(item\.plannedPackageCount\)/)
|
assert.match(loading, /pickedPackageCount: Number\(item\.plannedPackageCount\)/)
|
||||||
assert.match(loading, /const dropshipWholePallet = source\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'/)
|
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, /一件代发整托生产者不执行可拆分动态增量场景/)
|
||||||
assert.match(loading, /!dropshipWholePallet && Array\.isArray\(incrementalFixture\?\.skuCodes\)/)
|
assert.match(loading, /!dropshipWholePallet && Array\.isArray\(incrementalFixture\?\.skuCodes\)/)
|
||||||
assert.match(pda, /find\(\(item\) => item\?\.fulfillmentType === 'ONE_CLICK_DROPSHIP'\)/)
|
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.equal((loading.match(/id\(item\?\.inboundDetailId\) === id\(dynamicCandidate\.inboundDetailId\)/g) || []).length, 2)
|
||||||
assert.doesNotMatch(loading, /items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
|
assert.doesNotMatch(loading, /items\?\.find\?\.\(\(item\) => id\(item\?\.inboundId\) === incrementalInboundId\)/)
|
||||||
assertOrder(loading, [
|
assertOrder(loading, [
|
||||||
'回查下游门禁前原子 Batch 审计',
|
'回查下游门禁前拣货审计',
|
||||||
'最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING',
|
'最终确认前仅保留领取和拣货审计且无 COMPLETE_LOADING',
|
||||||
'LOADING 阶段向同一 PDA task 增加入库单',
|
'LOADING 阶段向同一 PDA task 增加入库单',
|
||||||
'增量后旧 inventory token 明确 A0409 并要求刷新',
|
'增量后旧 inventory token 明确 A0409 并要求刷新',
|
||||||
'增量旧 token 冲突后 plan/task/items 零写入',
|
'增量旧 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, /inbound-plans\/container-preview/)
|
||||||
assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/)
|
assert.match(source, /snapshotVersion: Number\(preview\.snapshotVersion\)/)
|
||||||
assert.match(source, /snapshotHash: String\(preview\.snapshotHash\)/)
|
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, /同键异载荷冲突/)
|
||||||
assert.match(source, /跨柜切换锁返回409/)
|
assert.match(source, /跨柜切换锁返回409/)
|
||||||
assert.match(source, /item\.operable === true && item\.allPlansReady === true/)
|
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,
|
assert.match(receivingSource,
|
||||||
/destinationType: inbound\.destinationType,[\s\S]*fbaWarehouseCode: inbound\.destinationType === 'FBA' \? inbound\.fbaWarehouseCode : null/)
|
/destinationType: inbound\.destinationType,[\s\S]*fbaWarehouseCode: inbound\.destinationType === 'FBA' \? inbound\.fbaWarehouseCode : null/)
|
||||||
assert.match(source, /fbaWarehouseCode,\s*\n\s*remark:/)
|
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/)
|
assert.doesNotMatch(source, /createEmptyPallet\([^\n]*main\.destinationWarehouseCode/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -109,11 +109,13 @@ async function createEmptyPallet(context, token, fixture, suffix) {
|
|||||||
`当前FBA入仓码 ${fixture.inboundCode || 'missing'} 缺少权威fbaWarehouseCode`)
|
`当前FBA入仓码 ${fixture.inboundCode || 'missing'} 缺少权威fbaWarehouseCode`)
|
||||||
return null
|
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 palletCode = `PDAOPS-${context.marker}-${suffix}`.replace(/[^A-Za-z0-9_-]/g, '').slice(-120)
|
||||||
const response = await context.requestOverseas('POST',
|
const response = await context.requestOverseas('POST',
|
||||||
`/api/platform/v1/warehouse-admin/${encodeURIComponent(fixture.overseasWarehouseId)}/pallets`, {
|
`/api/platform/v1/warehouse-admin/${encodeURIComponent(fixture.overseasWarehouseId)}/pallets`, {
|
||||||
palletCode, palletType: '木托盘', capacityCbm: 10, unitPrice: 0, status: 'IDLE',
|
palletCode, palletType: '木托盘', capacityCbm: 10, unitPrice: 0, status: 'IDLE',
|
||||||
locationId: fixture.location.locationId,
|
locationId: location.locationId,
|
||||||
fbaWarehouseCode,
|
fbaWarehouseCode,
|
||||||
remark: `${context.marker}-pda-operations-${suffix}`,
|
remark: `${context.marker}-pda-operations-${suffix}`,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -332,12 +332,28 @@ async function convertReceivingContainers(context, producers) {
|
|||||||
const convertedInboundCodes = new Set(plans
|
const convertedInboundCodes = new Set(plans
|
||||||
.flatMap((item) => Array.isArray(item?.lines) ? item.lines : [])
|
.flatMap((item) => Array.isArray(item?.lines) ? item.lines : [])
|
||||||
.map((line) => id(line?.inboundCode)).filter(nonBlank))
|
.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,
|
context.addStep(MODULE, `整柜转换为逐入仓码独立计划 ${containerNo}`, convertedResponse,
|
||||||
plans.length === inboundCodes
|
plans.length === inboundCodes
|
||||||
&& plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0)
|
&& plans.every((item) => Array.isArray(item?.lines) && item.lines.length > 0)
|
||||||
&& convertedInboundCodes.size === inboundCodes,
|
&& convertedInboundCodes.size === inboundCodes && reservedCoverage,
|
||||||
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
|
`plans=${plans.length}, inboundCodes=${convertedInboundCodes.size}, reserved=${Object.keys(reservedPalletsByInbound).length}, expected=${inboundCodes}; ${trace(convertedResponse)}`, OPTIONS)
|
||||||
if (plans.length !== inboundCodes) return false
|
if (plans.length !== inboundCodes || !reservedCoverage) return false
|
||||||
|
producer.receivingPalletsByInbound = reservedPalletsByInbound
|
||||||
const replayResponse = await context.requestOverseas('POST',
|
const replayResponse = await context.requestOverseas('POST',
|
||||||
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
|
'/api/platform/v1/last-leg/inbound-plans/convert-container', body,
|
||||||
{ 'Idempotency-Key': key }, context.overseasToken)
|
{ '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
|
if (!success(afterResolve.response) || receivingSnapshot(afterResolve.data) !== beforeResolve) return
|
||||||
const pallets = new Map()
|
const pallets = new Map()
|
||||||
for (const [index, inbound] of current.data.inbounds.entries()) {
|
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) {
|
if (!pallet) {
|
||||||
blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}实物托盘`,
|
blockerGap(context, `${inbound.destinationType}入仓码${inbound.inboundCode}实物托盘`,
|
||||||
`无法在实际海外收货仓 ${inbound.overseasWarehouseId || 'missing'} 创建可解析真实托盘;目的仓 ${inbound.destinationWarehouseId || 'missing'} 不得代替物理仓`, 'warehouse-resource')
|
`无法在实际海外收货仓 ${inbound.overseasWarehouseId || 'missing'} 创建可解析真实托盘;目的仓 ${inbound.destinationWarehouseId || 'missing'} 不得代替物理仓`, 'warehouse-resource')
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pallets.set(inbound.inboundCode, pallet)
|
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}实物托盘`,
|
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 })
|
'/api/pda/v1/last-leg/receiving-containers/current/pallets/resolve', { palletCode: pallet.palletCode, inboundCode: inbound.inboundCode })
|
||||||
context.assert(MODULE, `${inbound.destinationType}托盘目的模式匹配`, resolved.response,
|
context.assert(MODULE, `${inbound.destinationType}托盘目的模式匹配`, resolved.response,
|
||||||
|
|||||||
Reference in New Issue
Block a user