Files
baiyue-firstleg/scripts/admin-test-env-modules/manual-pda-batch-verify.mjs
2026-08-26 16:50:22 +08:00

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 核验。',
}
}