import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const MODULE = 'manual-pda-batch-fixture' const OPTIONS = { scenario: 'manual-pda-batch-fixture', 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 loadSource(context) { const report = JSON.parse(await readFile(resolve(context.options.manualPdaBatchSourceReport), 'utf8')) const source = report?.createdResources?.manualPdaReadyVerification const sourceGateway = String(report?.gateway || '').replace(/\/$/, '') const valid = sourceGateway === context.options.gateway && source?.stage === 'PDA_UNASSIGNED' && source?.pdaTaskId && source?.loadingPlanId && source?.containerBookingId context.assert(MODULE, '读取同环境 PDA 待领取核验报告', null, valid, `sourceMarker=${report?.marker || 'missing'}, sameGateway=${sourceGateway === context.options.gateway}, pdaTaskId=${id(source?.pdaTaskId) || 'missing'}`, OPTIONS) return valid ? source : null } function taskItemSnapshot(items) { return rows(items).map((item) => ({ id: id(item.id), loadingPlanItemId: id(item.loadingPlanItemId), tenantId: id(item.tenantId), inboundId: id(item.inboundId), inboundDetailId: id(item.inboundDetailId), orderNo: item.orderNo || null, inboundCode: item.inboundCode || null, cargoName: item.cargoName || null, boxCodes: rows(item.boxCodes).map(String), scanBoxCodes: rows(item.boxCodes).map(String).slice(0, Number(item.plannedPackageCount)), plannedPackageCount: Number(item.plannedPackageCount), })).sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id)) } export async function run(context) { const source = await loadSource(context) if (!source) return const taskId = id(source.pdaTaskId) const taskResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(source.warehouseId)}`) const task = context.apiSuccess(MODULE, 'PDA 人工作业前读取稳定任务详情', taskResponse, '', OPTIONS) const taskItems = taskItemSnapshot(task?.items) const scanBoxCodes = taskItems.flatMap((item) => item.scanBoxCodes) const cartonCodesReady = taskItems.length > 0 && taskItems.every((item) => item.id && item.inboundId && item.plannedPackageCount > 0 && item.boxCodes.length >= item.plannedPackageCount && item.scanBoxCodes.length === item.plannedPackageCount && new Set(item.boxCodes).size === item.boxCodes.length) && new Set(scanBoxCodes).size === scanBoxCodes.length context.assert(MODULE, '待领取任务下发稳定箱码且计划箱数可逐箱扫描', taskResponse, id(task?.id) === taskId && id(task?.loadingPlanId) === id(source.loadingPlanId) && id(task?.containerBookingId) === id(source.containerBookingId) && task?.taskStatus === 'LOADING' && task?.workflowStatus === 'UNASSIGNED' && task?.assignment == null && cartonCodesReady, `workflowStatus=${task?.workflowStatus || 'missing'}, items=${taskItems.length}, plannedPackages=${taskItems.reduce((sum, item) => sum + item.plannedPackageCount, 0)}, boxCodes=${taskItems.reduce((sum, item) => sum + item.boxCodes.length, 0)}`, OPTIONS) if (!cartonCodesReady || task?.workflowStatus !== 'UNASSIGNED') return const pendingResponse = await context.request('GET', `${PDA}/tasks/pending?keyword=${encodeURIComponent(source.containerNo)}`) const pending = context.apiSuccess(MODULE, '作业前读取当前操作员 PDA 待办', pendingResponse, '', OPTIONS) const pendingTask = rows(pending).find((item) => id(item.taskId) === taskId) context.assert(MODULE, '未领取任务在 PDA 待办中唯一可见', pendingResponse, Boolean(pendingTask) && pendingTask.workflowStatus === 'UNASSIGNED' && Number(pendingTask.pendingItemCount) === taskItems.length, `matches=${rows(pending).filter((item) => id(item.taskId) === taskId).length}, pendingItemCount=${pendingTask?.pendingItemCount ?? 'missing'}`, OPTIONS) const inventoryResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}/inventory-snapshot?warehouseId=${encodeURIComponent(source.warehouseId)}`) const inventory = context.apiSuccess(MODULE, '作业前读取实时库存快照基线', inventoryResponse, '', OPTIONS) const inventoryReady = id(inventory?.taskId) === taskId && id(inventory?.loadingPlanId) === id(source.loadingPlanId) && id(inventory?.containerBookingId) === id(source.containerBookingId) && Number(inventory?.taskVersion) === Number(task?.version) && typeof inventory?.snapshotToken === 'string' && inventory.snapshotToken.length === 64 && rows(inventory?.inbounds).length > 0 context.assert(MODULE, '库存快照与待领取任务版本和四元身份一致', inventoryResponse, inventoryReady, `taskVersion=${task?.version ?? 'missing'}, snapshotTaskVersion=${inventory?.taskVersion ?? 'missing'}, inboundCount=${rows(inventory?.inbounds).length}`, OPTIONS) const operationsResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}/operations`) const operations = context.apiSuccess(MODULE, '作业前读取 PDA 操作历史', operationsResponse, '', OPTIONS) context.assert(MODULE, '作业前不存在 CLAIM、BATCH、REVIEW 或 COMPLETE_LOADING', operationsResponse, rows(operations).every((item) => !['CLAIM', 'BATCH', 'REVIEW', 'COMPLETE_LOADING'].includes(item.actionType)), `actions=${rows(operations).map((item) => item.actionType).join(',') || 'none'}`, OPTIONS) context.createdResources.manualPdaBatchFixture = { schemaVersion: 1, sourceMarker: source.sourceMarker, preparedAt: new Date().toISOString(), stage: 'MANUAL_PDA_CLAIM_AND_BATCH_READY', pdaTaskId: taskId, loadingPlanId: id(source.loadingPlanId), containerBookingId: id(source.containerBookingId), cabinetNo: source.cabinetNo, containerNo: source.containerNo, tenantId: id(source.tenantId), warehouseId: id(source.warehouseId), beforeTaskVersion: Number(task.version), taskItems, inventoryBaseline: rows(inventory?.inbounds).map((item) => ({ inboundId: id(item.inboundId), availablePackageCount: Number(item.availablePackageCount), resourceCount: rows(item.resources).length })), instructions: [ '使用目标 PDA 作业员进入待办并领取本任务,确认任务进入 IN_PROGRESS。', '按 taskItems.scanBoxCodes 在真机逐箱扫描;每个箱码只扫描一次,处理完全部计划箱数。', '点击提交时由 PDA 重新获取库存快照,并只发送一次原子 batch;不要使用逐明细兼容接口。', '提交成功停在 AWAITING_REVIEW,不上传装柜证据、不复核、不执行最终完成。', 'HTTP 只能核验箱码下发和服务端数量,逐次扫码动作需保留真机人工证据。', ], } }