112 lines
7.7 KiB
JavaScript
112 lines
7.7 KiB
JavaScript
import { readFile } from 'node:fs/promises'
|
|||
|
|
import { resolve } from 'node:path'
|
||
|
|
|
||
|
|
const MODULE = 'manual-pda-batch-verify'
|
||
|
|
const OPTIONS = { scenario: 'manual-pda-batch-readonly-verification', deploymentBlocker: true }
|
||
|
|
const PDA = '/api/platform/v1/first-leg/pda-loading'
|
||
|
|
|
||
|
|
function id(value) { return value == null ? '' : String(value) }
|
||
|
|
function rows(value) { return Array.isArray(value) ? value : [] }
|
||
|
|
|
||
|
|
async function loadFixture(context) {
|
||
|
|
const report = JSON.parse(await readFile(resolve(context.options.manualPdaBatchSourceReport), 'utf8'))
|
||
|
|
const fixture = report?.createdResources?.manualPdaBatchFixture
|
||
|
|
const sourceGateway = String(report?.gateway || '').replace(/\/$/, '')
|
||
|
|
const valid = sourceGateway === context.options.gateway && fixture?.schemaVersion === 1
|
||
|
|
&& fixture?.stage === 'MANUAL_PDA_CLAIM_AND_BATCH_READY' && fixture?.pdaTaskId
|
||
|
|
&& Array.isArray(fixture?.taskItems) && fixture.taskItems.length > 0
|
||
|
|
context.assert(MODULE, '读取同环境 PDA 人工作业准备报告', null, valid,
|
||
|
|
`sourceMarker=${report?.marker || 'missing'}, sameGateway=${sourceGateway === context.options.gateway}, pdaTaskId=${id(fixture?.pdaTaskId) || 'missing'}`,
|
||
|
|
OPTIONS)
|
||
|
|
return valid ? fixture : null
|
||
|
|
}
|
||
|
|
|
||
|
|
function stableItem(value) {
|
||
|
|
return { id: id(value.id), loadingPlanItemId: id(value.loadingPlanItemId), tenantId: id(value.tenantId),
|
||
|
|
inboundId: id(value.inboundId), inboundDetailId: id(value.inboundDetailId),
|
||
|
|
boxCodes: rows(value.boxCodes).map(String), plannedPackageCount: Number(value.plannedPackageCount) }
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function run(context) {
|
||
|
|
const fixture = await loadFixture(context)
|
||
|
|
if (!fixture) return
|
||
|
|
const taskId = id(fixture.pdaTaskId)
|
||
|
|
|
||
|
|
const taskResponse = await context.request('GET',
|
||
|
|
`${PDA}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(fixture.warehouseId)}`)
|
||
|
|
const task = context.apiSuccess(MODULE, '读取人工领取和批量提交后的 PDA 任务', taskResponse, '', OPTIONS)
|
||
|
|
const afterItems = rows(task?.items).map(stableItem)
|
||
|
|
.sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id))
|
||
|
|
const beforeStable = fixture.taskItems.map(stableItem)
|
||
|
|
const allPicked = rows(task?.items).length > 0 && rows(task?.items).every((item) =>
|
||
|
|
item.itemStatus === 'PICKED'
|
||
|
|
&& Number(item.pickedPackageCount) === Number(item.plannedPackageCount))
|
||
|
|
context.assert(MODULE, '领取后任务绑定作业员并以一次批量提交进入待复核', taskResponse,
|
||
|
|
id(task?.id) === taskId && id(task?.loadingPlanId) === id(fixture.loadingPlanId)
|
||
|
|
&& id(task?.containerBookingId) === id(fixture.containerBookingId)
|
||
|
|
&& task?.cabinetNo === fixture.cabinetNo && task?.currentContainerNo === fixture.containerNo
|
||
|
|
&& task?.taskStatus === 'LOADING' && task?.workflowStatus === 'AWAITING_REVIEW'
|
||
|
|
&& task?.assignment?.assigneeId && task?.assignment?.assigneeName
|
||
|
|
&& Number(task?.version) > Number(fixture.beforeTaskVersion)
|
||
|
|
&& JSON.stringify(afterItems) === JSON.stringify(beforeStable) && allPicked,
|
||
|
|
`taskStatus=${task?.taskStatus || 'missing'}, workflowStatus=${task?.workflowStatus || 'missing'}, assigneeId=${id(task?.assignment?.assigneeId) || 'missing'}, beforeVersion=${fixture.beforeTaskVersion}, afterVersion=${task?.version ?? 'missing'}`,
|
||
|
|
OPTIONS)
|
||
|
|
if (!task || task.workflowStatus !== 'AWAITING_REVIEW') return
|
||
|
|
|
||
|
|
const pendingResponse = await context.request('GET',
|
||
|
|
`${PDA}/tasks/pending?keyword=${encodeURIComponent(fixture.containerNo)}`)
|
||
|
|
const pending = context.apiSuccess(MODULE, '读取待复核 PDA pending 投影', pendingResponse, '', OPTIONS)
|
||
|
|
const pendingTask = rows(pending).find((item) => id(item.taskId) === taskId)
|
||
|
|
context.assert(MODULE, 'pending 与详情一致显示 AWAITING_REVIEW 且无未处理明细', pendingResponse,
|
||
|
|
Boolean(pendingTask) && pendingTask.workflowStatus === 'AWAITING_REVIEW'
|
||
|
|
&& Number(pendingTask.itemCount) === rows(task.items).length
|
||
|
|
&& Number(pendingTask.pendingItemCount) === 0,
|
||
|
|
`workflowStatus=${pendingTask?.workflowStatus || 'missing'}, itemCount=${pendingTask?.itemCount ?? 'missing'}, pendingItemCount=${pendingTask?.pendingItemCount ?? 'missing'}`,
|
||
|
|
OPTIONS)
|
||
|
|
|
||
|
|
const operationsResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}/operations`)
|
||
|
|
const operations = context.apiSuccess(MODULE, '读取领取与原子批量提交审计', operationsResponse, '', OPTIONS)
|
||
|
|
const claimOperations = rows(operations).filter((item) => item.actionType === 'CLAIM')
|
||
|
|
const batchOperations = rows(operations).filter((item) => item.actionType === 'BATCH')
|
||
|
|
context.assert(MODULE, '人工主链只保留一次 CLAIM 和一次 BATCH 且尚无复核完成记录', operationsResponse,
|
||
|
|
claimOperations.length === 1 && batchOperations.length === 1
|
||
|
|
&& [...claimOperations, ...batchOperations].every((item) => item.operationId
|
||
|
|
&& item.operatorId && item.operatorName && item.createdAt)
|
||
|
|
&& !rows(operations).some((item) => ['REVIEW', 'COMPLETE_LOADING'].includes(item.actionType)),
|
||
|
|
`actions=${rows(operations).map((item) => item.actionType).join(',') || 'none'}, claimCount=${claimOperations.length}, batchCount=${batchOperations.length}`,
|
||
|
|
OPTIONS)
|
||
|
|
|
||
|
|
const uniqueInbounds = [...new Map(rows(task.items).map((item) => [id(item.inboundId), item])).values()]
|
||
|
|
for (const item of uniqueInbounds) {
|
||
|
|
const projectionResponse = await context.request('GET',
|
||
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(item.inboundId)}/loading/picking?tenantId=${encodeURIComponent(item.tenantId)}`)
|
||
|
|
const projection = context.apiSuccess(MODULE, `回查 ${item.inboundCode || item.inboundId} 入库级拣货投影`, projectionResponse, '', OPTIONS)
|
||
|
|
context.assert(MODULE, `${item.inboundCode || item.inboundId} 服务端汇总数量已拣齐`, projectionResponse,
|
||
|
|
Number(projection?.expectedCount) > 0
|
||
|
|
&& Number(projection?.expectedCount) === Number(projection?.pickedCount),
|
||
|
|
`status=${projection?.status || 'missing'}, expected=${projection?.expectedCount ?? 'missing'}, picked=${projection?.pickedCount ?? 'missing'}`,
|
||
|
|
OPTIONS)
|
||
|
|
const evidenceResponse = await context.request('GET',
|
||
|
|
`/api/platform/v1/inbounds/${encodeURIComponent(item.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(item.tenantId)}`)
|
||
|
|
const evidence = context.apiSuccess(MODULE, `${item.inboundCode || item.inboundId} 待复核取证入口可读`, evidenceResponse, '', OPTIONS)
|
||
|
|
context.assert(MODULE, `${item.inboundCode || item.inboundId} AWAITING_REVIEW 可进入取证`, evidenceResponse,
|
||
|
|
Array.isArray(evidence?.evidences),
|
||
|
|
`evidenceCount=${evidence?.evidences?.length ?? 'missing'}`, OPTIONS)
|
||
|
|
}
|
||
|
|
|
||
|
|
context.createdResources.manualPdaBatchVerification = {
|
||
|
|
sourceMarker: fixture.sourceMarker, verifiedAt: new Date().toISOString(), stage: 'PDA_AWAITING_REVIEW',
|
||
|
|
pdaTaskId: taskId, loadingPlanId: id(fixture.loadingPlanId),
|
||
|
|
containerBookingId: id(fixture.containerBookingId), cabinetNo: fixture.cabinetNo,
|
||
|
|
containerNo: fixture.containerNo, warehouseId: id(fixture.warehouseId), taskVersion: Number(task.version),
|
||
|
|
assigneeId: id(task.assignment?.assigneeId), assigneeName: task.assignment?.assigneeName || null,
|
||
|
|
itemCount: rows(task.items).length,
|
||
|
|
plannedPackageCount: rows(task.items).reduce((sum, item) => sum + Number(item.plannedPackageCount || 0), 0),
|
||
|
|
pickedPackageCount: rows(task.items).reduce((sum, item) => sum + Number(item.pickedPackageCount || 0), 0),
|
||
|
|
claimOperationId: claimOperations[0]?.operationId || null,
|
||
|
|
batchOperationId: batchOperations[0]?.operationId || null,
|
||
|
|
deviceEvidenceRequired: 'HTTP 不记录逐次扫码轨迹;箱码逐一扫描需保留真机人工验收证据。',
|
||
|
|
nextStage: '上传五类 ACTIVE 装柜证据后,再执行 PDA 最终完成与 LOADED 核验。',
|
||
|
|
}
|
||
|
|
}
|