fix(firstleg): preserve sealed pallet identity in atomic PDA picking
This commit is contained in:
@@ -701,6 +701,21 @@ export async function run(context) {
|
||||
inventoryBlockIdentityVerified,
|
||||
`inbounds=${inventoryBlockSnapshots.length}, blocks=${inventoryBlockSnapshots.map((entry) => entry.resources[0]?.blockId).join(',')}`, OPTIONS)
|
||||
|
||||
if (dropshipAssignment) {
|
||||
const palletUnits = Array.isArray(inventory?.units) ? inventory.units : []
|
||||
const taskUnitsValid = canonicalPickItems.every((item) => item.unitType === 'SEALED_PALLET'
|
||||
&& id(item.palletId) === id(candidate.palletId) && item.palletNo === candidate.palletNo
|
||||
&& /^DP-[0-9]{6}-[0-9]{4}$/.test(item.palletNo) && item.indivisible === true)
|
||||
const inventoryUnitsValid = canonicalPickItems.every((item) => palletUnits.some((unit) =>
|
||||
id(unit.pickItemId) === id(item.id) && id(unit.loadingPlanItemId) === id(item.loadingPlanItemId)
|
||||
&& id(unit.inboundId) === id(item.inboundId) && unit.unitType === 'SEALED_PALLET'
|
||||
&& id(unit.palletId) === id(item.palletId) && unit.palletNo === item.palletNo && unit.indivisible === true))
|
||||
context.assert(MODULE, 'PDA task与inventory明确返回SEALED_PALLET及DP整托四字段', inventoryResponse,
|
||||
taskUnitsValid && inventoryUnitsValid,
|
||||
`taskUnits=${canonicalPickItems.length}, inventoryUnits=${palletUnits.length}`, OPTIONS)
|
||||
if (!taskUnitsValid || !inventoryUnitsValid) return
|
||||
}
|
||||
|
||||
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
|
||||
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
|
||||
const skipBatchKey = `${context.marker}-exception-skip-batch`.slice(0, 128)
|
||||
@@ -715,8 +730,10 @@ export async function run(context) {
|
||||
expectedVersion: Number(claimed?.version),
|
||||
inventorySnapshotToken: inventory.snapshotToken,
|
||||
items: canonicalPickItems.map((item) => ({
|
||||
itemId: id(item.id), action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount),
|
||||
version: Number(item.version),
|
||||
itemId: id(item.id), version: Number(item.version),
|
||||
...(dropshipWholePallet
|
||||
? { action: 'PICK_PALLET', palletNo: item.palletNo }
|
||||
: { action: 'PICK', pickedPackageCount: Number(item.plannedPackageCount) }),
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -754,6 +771,30 @@ export async function run(context) {
|
||||
&& operationsSignature(afterInvalidOperations) === operationsSignature(beforeInvalidOperations),
|
||||
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
if (dropshipWholePallet) {
|
||||
for (const action of ['PICK', 'PARTIAL', 'SKIP']) {
|
||||
const bypass = await request(context, 'POST', batchPath, {
|
||||
...batchPayload,
|
||||
items: canonicalPickItems.map((item) => ({ itemId: id(item.id), version: Number(item.version), action,
|
||||
...(action === 'PICK' ? { pickedPackageCount: Number(item.plannedPackageCount) }
|
||||
: action === 'PARTIAL' ? { finalPickedPackageCount: 1, reason: '整托不可部分拣货' }
|
||||
: { reason: '整托不可普通跳过' }),
|
||||
})),
|
||||
}, { 'Idempotency-Key': `${context.marker}-pallet-bypass-${action}`.slice(0, 128) })
|
||||
context.assert(MODULE, `整托不能通过普通${action}动作绕过`, bypass,
|
||||
bypass.http === 200 && codeOf(bypass) === 'A0409', `code=${codeOf(bypass)}`, OPTIONS)
|
||||
}
|
||||
const afterBypassResponse = await context.request('GET', taskDetailPath)
|
||||
const afterBypass = context.apiSuccess(MODULE, '整托绕过拒绝后任务回查', afterBypassResponse, '', OPTIONS)
|
||||
const afterBypassOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
||||
const afterBypassOperations = context.apiSuccess(MODULE, '整托绕过拒绝后审计回查', afterBypassOperationsResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '整托绕过失败后任务明细审计零写入', afterBypassResponse,
|
||||
Number(afterBypass?.version) === Number(beforeInvalidTask?.version)
|
||||
&& taskItemsSignature(afterBypass) === taskItemsSignature(beforeInvalidTask)
|
||||
&& operationsSignature(afterBypassOperations) === operationsSignature(beforeInvalidOperations),
|
||||
`taskVersion=${afterBypass?.version ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
|
||||
const merchantLogin = await context.request('POST', '/api/auth/login', {
|
||||
loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
|
||||
}, '')
|
||||
@@ -803,12 +844,8 @@ export async function run(context) {
|
||||
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
|
||||
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
|
||||
|
||||
const canonicalPickPath = dropshipWholePallet
|
||||
? `${pdaBase}/tasks/${canonicalTaskId}/pallets/pick`
|
||||
: batchPath
|
||||
const canonicalPickPayload = dropshipWholePallet
|
||||
? { warehouseId, palletNo: candidate.palletNo, expectedTaskVersion: Number(claimed?.version), operationId: batchKey }
|
||||
: batchPayload
|
||||
const canonicalPickPath = batchPath
|
||||
const canonicalPickPayload = batchPayload
|
||||
const canonicalPick = await request(context, 'POST', canonicalPickPath, canonicalPickPayload,
|
||||
{ 'Idempotency-Key': batchKey })
|
||||
const pickedCanonical = context.apiSuccess(MODULE,
|
||||
@@ -831,15 +868,25 @@ export async function run(context) {
|
||||
atomicBatchReplayVerified,
|
||||
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
if (dropshipWholePallet) {
|
||||
const palletHashConflict = await request(context, 'POST', batchPath, {
|
||||
...batchPayload, items: batchPayload.items.map((item) => ({ ...item,
|
||||
palletNo: item.palletNo === 'DP-991231-9999' ? 'DP-991231-9998' : 'DP-991231-9999' })),
|
||||
}, { 'Idempotency-Key': batchKey })
|
||||
context.assert(MODULE, '整托batch同键改托盘码必须冲突', palletHashConflict,
|
||||
palletHashConflict.http === 200 && codeOf(palletHashConflict) === 'A0409',
|
||||
`code=${codeOf(palletHashConflict)}`, OPTIONS)
|
||||
}
|
||||
|
||||
const preFinalOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
||||
const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前拣货审计', preFinalOperationsResponse, '', OPTIONS)
|
||||
const canonicalPickAction = dropshipWholePallet ? 'PICK_PALLET' : 'BATCH'
|
||||
const canonicalPickAction = '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 === canonicalPickAction
|
||||
&& id(item.operatorId) && Boolean(item.createdAt))
|
||||
&& preFinalOperations.filter((item) => item.actionType === canonicalPickAction
|
||||
&& item.operationId === batchKey && id(item.operatorId) && Boolean(item.createdAt)).length === 1
|
||||
&& !preFinalOperations.some((item) => item.actionType === 'COMPLETE_LOADING')
|
||||
&& !preFinalOperations.some((item) => item.actionType === 'REVIEW'),
|
||||
`actions=${Array.isArray(preFinalOperations) ? preFinalOperations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS)
|
||||
|
||||
Reference in New Issue
Block a user