优化
This commit is contained in:
@@ -1029,13 +1029,111 @@ export async function run(context) {
|
||||
const dynamicCandidatesResponse = await context.request('GET',
|
||||
`${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const dynamicCandidates = context.apiSuccess(MODULE, '查询同柜动态增量候选', dynamicCandidatesResponse, '', OPTIONS)
|
||||
const dynamicCandidate = Array.isArray(dynamicCandidates) ? dynamicCandidates.find((item) =>
|
||||
id(item?.inboundId) === incrementalInboundId && Number(item?.availablePackageCount) > 0) : null
|
||||
const markerOwnedDynamicCandidates = Array.isArray(dynamicCandidates) ? dynamicCandidates.filter((item) =>
|
||||
id(item?.inboundId) === incrementalInboundId && Number(item?.availablePackageCount) > 0) : []
|
||||
const dynamicCandidate = markerOwnedDynamicCandidates[0]
|
||||
const skippedExceptionCandidate = markerOwnedDynamicCandidates[1]
|
||||
const partialExceptionCandidate = markerOwnedDynamicCandidates.find((item, index) =>
|
||||
index > 1 && Number(item?.availablePackageCount) >= 2)
|
||||
context.assert(MODULE, '动态增量候选保持客户、仓库和渠道隔离', dynamicCandidatesResponse,
|
||||
Boolean(dynamicCandidate?.inboundDetailId) && id(dynamicCandidate?.accountId) === tenantId
|
||||
&& id(dynamicCandidate?.warehouseId) === warehouseId
|
||||
&& dynamicCandidate?.channelCode === initial?.summary?.channelCode,
|
||||
`inboundId=${incrementalInboundId}, detailId=${id(dynamicCandidate?.inboundDetailId) || 'missing'}, available=${dynamicCandidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细', dynamicCandidatesResponse,
|
||||
Boolean(skippedExceptionCandidate?.inboundDetailId) && Boolean(partialExceptionCandidate?.inboundDetailId)
|
||||
&& id(skippedExceptionCandidate?.inboundDetailId) !== id(partialExceptionCandidate?.inboundDetailId)
|
||||
&& id(dynamicCandidate?.inboundDetailId) !== id(skippedExceptionCandidate?.inboundDetailId)
|
||||
&& id(dynamicCandidate?.inboundDetailId) !== id(partialExceptionCandidate?.inboundDetailId),
|
||||
`candidateCount=${markerOwnedDynamicCandidates.length}, skipDetailId=${id(skippedExceptionCandidate?.inboundDetailId) || 'missing'}, partialDetailId=${id(partialExceptionCandidate?.inboundDetailId) || 'missing'}`, OPTIONS)
|
||||
if (skippedExceptionCandidate?.inboundDetailId && partialExceptionCandidate?.inboundDetailId) {
|
||||
const beforeSkipPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const beforeSkipPlan = context.apiSuccess(MODULE, 'SKIP 异常场景前读取计划即时版本', beforeSkipPlanResponse, '', OPTIONS)
|
||||
const skipAssignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
|
||||
inboundId: incrementalInboundId, inboundDetailId: id(skippedExceptionCandidate.inboundDetailId), packageCount: 1,
|
||||
expectedVersion: Number(beforeSkipPlan?.version), operationId: `${context.marker}-exception-skip-assign`.slice(0, 128),
|
||||
})
|
||||
const skipAssignedPlan = context.apiSuccess(MODULE, '加入本轮待跳拣异常明细', skipAssignmentResponse, '', OPTIONS)
|
||||
const skipPlanItem = skipAssignedPlan?.items?.find?.((item) =>
|
||||
id(item?.inboundDetailId) === id(skippedExceptionCandidate.inboundDetailId))
|
||||
const skipTaskResponse = await context.request('GET', taskDetailPath)
|
||||
const skipTask = context.apiSuccess(MODULE, '读取待跳拣异常 PDA 明细', skipTaskResponse, '', OPTIONS)
|
||||
const skipTaskItem = skipTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(skipPlanItem?.id))
|
||||
const skipInventoryResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const skipInventory = context.apiSuccess(MODULE, '跳拣前刷新实时库存快照', skipInventoryResponse, '', OPTIONS)
|
||||
const skipReason = `${context.marker}-箱唛破损待复核`
|
||||
const skipBatchResponse = await request(context, 'POST', batchPath, {
|
||||
warehouseId, loadingPlanId, containerBookingId,
|
||||
expectedVersion: Number(skipTask?.version), inventorySnapshotToken: skipInventory?.snapshotToken,
|
||||
items: [{ itemId: id(skipTaskItem?.id), action: 'SKIP', version: Number(skipTaskItem?.version),
|
||||
reason: skipReason }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-exception-skip-batch`.slice(0, 128) })
|
||||
const skippedTask = context.apiSuccess(MODULE, 'SKIP 携带异常原因并原子解决待拣明细', skipBatchResponse, '', OPTIONS)
|
||||
const skippedTaskItem = skippedTask?.items?.find?.((item) => id(item?.id) === id(skipTaskItem?.id))
|
||||
context.assert(MODULE, '合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW', skipBatchResponse,
|
||||
Boolean(skipReason.trim()) && skippedTaskItem?.itemStatus === 'SKIPPED'
|
||||
&& Number(skippedTaskItem?.pickedPackageCount) === 0
|
||||
&& skippedTask?.workflowStatus === 'AWAITING_REVIEW' && skippedTask?.taskStatus === 'LOADING',
|
||||
`itemStatus=${skippedTaskItem?.itemStatus || 'missing'}, picked=${skippedTaskItem?.pickedPackageCount ?? 'missing'}, workflow=${skippedTask?.workflowStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const beforePartialPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const beforePartialPlan = context.apiSuccess(MODULE, 'PARTIAL 异常场景前读取计划即时版本', beforePartialPlanResponse, '', OPTIONS)
|
||||
const partialAssignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
|
||||
inboundId: incrementalInboundId, inboundDetailId: id(partialExceptionCandidate.inboundDetailId), packageCount: 2,
|
||||
expectedVersion: Number(beforePartialPlan?.version), operationId: `${context.marker}-exception-partial-assign`.slice(0, 128),
|
||||
})
|
||||
const partialAssignedPlan = context.apiSuccess(MODULE, '加入本轮部分拣货异常明细', partialAssignmentResponse, '', OPTIONS)
|
||||
const partialPlanItem = partialAssignedPlan?.items?.find?.((item) =>
|
||||
id(item?.inboundDetailId) === id(partialExceptionCandidate.inboundDetailId))
|
||||
const unresolvedTaskResponse = await context.request('GET', taskDetailPath)
|
||||
const unresolvedTask = context.apiSuccess(MODULE, '读取存在未解决异常的 PDA 任务', unresolvedTaskResponse, '', OPTIONS)
|
||||
const unresolvedTaskItem = unresolvedTask?.items?.find?.((item) =>
|
||||
id(item?.loadingPlanItemId) === id(partialPlanItem?.id))
|
||||
const operationsBeforeUnresolvedCompleteResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
||||
const operationsBeforeUnresolvedComplete = context.apiSuccess(MODULE,
|
||||
'未解决异常完成前读取 PDA 操作审计基线', operationsBeforeUnresolvedCompleteResponse, '', OPTIONS)
|
||||
const unresolvedComplete = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`, {
|
||||
expectedTaskVersion: Number(unresolvedTask?.version),
|
||||
operationId: `${context.marker}-complete-with-unresolved`.slice(0, 128),
|
||||
remark: `${context.marker}-must-reject-unresolved`,
|
||||
}, { 'Idempotency-Key': `${context.marker}-complete-with-unresolved-key`.slice(0, 128) })
|
||||
context.addStep(MODULE, '存在未解决异常明细时禁止确认装柜完成', unresolvedComplete,
|
||||
unresolvedComplete.http === 200 && codeOf(unresolvedComplete) === 'A0409'
|
||||
&& String(unresolvedComplete.body?.message || '').includes('尚未处理'),
|
||||
responseEvidence(unresolvedComplete), OPTIONS)
|
||||
const afterUnresolvedCompleteTaskResponse = await context.request('GET', taskDetailPath)
|
||||
const afterUnresolvedCompleteTask = context.apiSuccess(MODULE,
|
||||
'未解决异常完成失败后回查 PDA 任务', afterUnresolvedCompleteTaskResponse, '', OPTIONS)
|
||||
const operationsAfterUnresolvedCompleteResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks/${canonicalTaskId}/operations`)
|
||||
const operationsAfterUnresolvedComplete = context.apiSuccess(MODULE,
|
||||
'未解决异常完成失败后回查 PDA 操作审计', operationsAfterUnresolvedCompleteResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '未解决异常完成失败后 task/items/operation 全部零写入', afterUnresolvedCompleteTaskResponse,
|
||||
Number(afterUnresolvedCompleteTask?.version) === Number(unresolvedTask?.version)
|
||||
&& taskItemsSignature(afterUnresolvedCompleteTask) === taskItemsSignature(unresolvedTask)
|
||||
&& operationsSignature(operationsAfterUnresolvedComplete) === operationsSignature(operationsBeforeUnresolvedComplete),
|
||||
`taskVersion=${afterUnresolvedCompleteTask?.version ?? 'missing'}, operations=${operationsAfterUnresolvedComplete?.length ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const partialInventoryResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
const partialInventory = context.apiSuccess(MODULE, '部分拣货前刷新实时库存快照', partialInventoryResponse, '', OPTIONS)
|
||||
const partialBatchResponse = await request(context, 'POST', batchPath, {
|
||||
warehouseId, loadingPlanId, containerBookingId,
|
||||
expectedVersion: Number(afterUnresolvedCompleteTask?.version),
|
||||
inventorySnapshotToken: partialInventory?.snapshotToken,
|
||||
items: [{ itemId: id(unresolvedTaskItem?.id), action: 'PARTIAL', finalPickedPackageCount: 1,
|
||||
releaseMode: 'MANUAL', locationIds: [], version: Number(unresolvedTaskItem?.version) }],
|
||||
}, { 'Idempotency-Key': `${context.marker}-exception-partial-batch`.slice(0, 128) })
|
||||
const partialTask = context.apiSuccess(MODULE, '合法 PARTIAL 解决未完成明细', partialBatchResponse, '', OPTIONS)
|
||||
const partialTaskItem = partialTask?.items?.find?.((item) => id(item?.id) === id(unresolvedTaskItem?.id))
|
||||
context.assert(MODULE, '部分拣货数量守恒并恢复最终可确认状态', partialBatchResponse,
|
||||
partialTaskItem?.itemStatus === 'PARTIAL' && Number(partialTaskItem?.plannedPackageCount) === 2
|
||||
&& Number(partialTaskItem?.pickedPackageCount) === 1
|
||||
&& partialTask?.workflowStatus === 'AWAITING_REVIEW' && partialTask?.taskStatus === 'LOADING',
|
||||
`itemStatus=${partialTaskItem?.itemStatus || 'missing'}, planned=${partialTaskItem?.plannedPackageCount ?? 'missing'}, picked=${partialTaskItem?.pickedPackageCount ?? 'missing'}, workflow=${partialTask?.workflowStatus || 'missing'}`, OPTIONS)
|
||||
}
|
||||
if (dynamicCandidate?.inboundDetailId) {
|
||||
const planBeforeIncrementResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const planBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前计划', planBeforeIncrementResponse, '', OPTIONS)
|
||||
|
||||
@@ -458,6 +458,32 @@ test('phase3 loading proves stale inventory zero-write and defers final completi
|
||||
assert.doesNotMatch(loading, /complete-before-downstream/)
|
||||
})
|
||||
|
||||
test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstream completion', () => {
|
||||
const loading = source('./phase3-loading.mjs')
|
||||
assertOrder(loading, [
|
||||
'本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细',
|
||||
'加入本轮待跳拣异常明细',
|
||||
'SKIP 携带异常原因并原子解决待拣明细',
|
||||
'合法 SKIP 保存零实拣事实并恢复 AWAITING_REVIEW',
|
||||
'加入本轮部分拣货异常明细',
|
||||
'未解决异常完成前读取 PDA 操作审计基线',
|
||||
'存在未解决异常明细时禁止确认装柜完成',
|
||||
'未解决异常完成失败后 task/items/operation 全部零写入',
|
||||
'合法 PARTIAL 解决未完成明细',
|
||||
'部分拣货数量守恒并恢复最终可确认状态',
|
||||
'报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING',
|
||||
])
|
||||
assert.match(loading, /action: 'SKIP', version: Number\(skipTaskItem\?\.version\),\s*reason: skipReason/)
|
||||
assert.match(loading, /skipReason = `\$\{context\.marker\}-箱唛破损待复核`/)
|
||||
assert.match(loading, /itemStatus === 'SKIPPED'[\s\S]*pickedPackageCount\) === 0/)
|
||||
assert.match(loading, /action: 'PARTIAL', finalPickedPackageCount: 1/)
|
||||
assert.match(loading, /plannedPackageCount\) === 2[\s\S]*pickedPackageCount\) === 1/)
|
||||
assert.match(loading, /String\(unresolvedComplete\.body\?\.message \|\| ''\)\.includes\('尚未处理'\)/)
|
||||
assert.match(loading, /taskItemsSignature\(afterUnresolvedCompleteTask\) === taskItemsSignature\(unresolvedTask\)/)
|
||||
assert.match(loading, /operationsSignature\(operationsAfterUnresolvedComplete\) === operationsSignature\(operationsBeforeUnresolvedComplete\)/)
|
||||
assert.doesNotMatch(loading, /addCoverageGap\([^)]*(SKIP|PARTIAL|未解决异常)/)
|
||||
})
|
||||
|
||||
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
|
||||
const entry = source('../admin-test-env-acceptance.mjs')
|
||||
const measurement = source('./inbound-measurement.mjs')
|
||||
|
||||
Reference in New Issue
Block a user