import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const MODULE = 'manual-loading-complete-fixture' const OPTIONS = { scenario: 'manual-loading-complete-fixture', deploymentBlocker: true } const PDA = '/api/platform/v1/first-leg/pda-loading' const SMART = '/api/platform/v1/first-leg/smart-loading' const CONTAINERS = '/api/platform/v1/containers' const REQUIRED_EVIDENCE_TYPES = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'] 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.manualLoadingCompleteSourceReport), 'utf8')) const source = report?.createdResources?.manualPdaBatchVerification const sourceGateway = String(report?.gateway || '').replace(/\/$/, '') const valid = sourceGateway === context.options.gateway && source?.stage === 'PDA_AWAITING_REVIEW' && 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 taskItems(value) { return rows(value).map((item) => ({ id: id(item.id), tenantId: id(item.tenantId), inboundId: id(item.inboundId), inboundDetailId: id(item.inboundDetailId), plannedPackageCount: Number(item.plannedPackageCount), pickedPackageCount: Number(item.pickedPackageCount), itemStatus: item.itemStatus, version: Number(item.version) })) .sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id)) } function sourceInboundSnapshots(items) { const values = new Map() for (const item of rows(items)) { const inboundId = id(item.inboundId) const current = values.get(inboundId) || { tenantId: id(item.tenantId), inboundId, pickedPackageCount: 0 } current.pickedPackageCount += Number(item.pickedPackageCount || 0) values.set(inboundId, current) } return [...values.values()] } 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 beforeItems = taskItems(task?.items) const evidenceOwner = rows(task?.items).find((item) => id(item.inboundId) === id(task?.inboundId)) || rows(task?.items)[0] context.assert(MODULE, '任务全部拣齐、待复核且证据归属入库单明确', taskResponse, id(task?.id) === taskId && task?.taskStatus === 'LOADING' && task?.workflowStatus === 'AWAITING_REVIEW' && task?.assignment?.assigneeId && beforeItems.length > 0 && beforeItems.every((item) => item.itemStatus === 'PICKED' && item.pickedPackageCount === item.plannedPackageCount) && evidenceOwner?.tenantId && evidenceOwner?.inboundId, `workflowStatus=${task?.workflowStatus || 'missing'}, items=${beforeItems.length}, evidenceTenantId=${id(evidenceOwner?.tenantId) || 'missing'}, evidenceInboundId=${id(evidenceOwner?.inboundId) || 'missing'}`, OPTIONS) if (!evidenceOwner?.tenantId || !evidenceOwner?.inboundId) return const evidencePath = `/api/platform/v1/inbounds/${encodeURIComponent(evidenceOwner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(evidenceOwner.tenantId)}` const evidenceResponse = await context.request('GET', evidencePath) const evidence = context.apiSuccess(MODULE, '上传五类证据前读取空基线', evidenceResponse, '', OPTIONS) const activeEvidence = rows(evidence?.evidences).filter((item) => item.status !== 'VOID') context.assert(MODULE, '本轮最终完成前尚未上传 ACTIVE 装柜证据', evidenceResponse, activeEvidence.length === 0, `activeEvidence=${activeEvidence.map((item) => item.evidenceType).join(',') || 'none'}`, OPTIONS) const planResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(source.loadingPlanId)}`) const plan = context.apiSuccess(MODULE, '最终完成前读取运行中装柜计划', planResponse, '', OPTIONS) context.assert(MODULE, '计划保持 LOADING 且尚未最终完成', planResponse, id(plan?.id) === id(source.loadingPlanId) && plan?.status === 'LOADING' && plan?.finalCompletionStatus === 'IN_PROGRESS', `status=${plan?.status || 'missing'}, finalCompletionStatus=${plan?.finalCompletionStatus || 'missing'}, version=${plan?.version ?? 'missing'}`, OPTIONS) const operationsResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}/operations`) const operations = context.apiSuccess(MODULE, '最终完成前读取任务审计', operationsResponse, '', OPTIONS) context.assert(MODULE, '完成前只有一次 CLAIM/BATCH 且无 COMPLETE_LOADING', operationsResponse, rows(operations).filter((item) => item.actionType === 'CLAIM').length === 1 && rows(operations).filter((item) => item.actionType === 'BATCH').length === 1 && !rows(operations).some((item) => ['REVIEW', 'COMPLETE_LOADING'].includes(item.actionType)), `actions=${rows(operations).map((item) => item.actionType).join(',') || 'none'}`, OPTIONS) const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(source.containerBookingId)}`) const container = context.apiSuccess(MODULE, '最终完成前回查 RELEASED 订舱柜次', containerResponse, '', OPTIONS) const bookingResponse = container?.oceanBookingId ? await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(container.oceanBookingId)}`) : containerResponse const booking = context.apiSuccess(MODULE, '最终完成前回查订舱门禁', bookingResponse, '', OPTIONS) context.assert(MODULE, '完成前真实柜号与 RELEASED 订舱仍有效', bookingResponse, id(container?.id) === id(source.containerBookingId) && container?.containerNo === source.containerNo && booking?.oceanBooking?.status === 'RELEASED', `containerNo=${container?.containerNo || 'missing'}, oceanBookingId=${id(container?.oceanBookingId) || 'missing'}, bookingStatus=${booking?.oceanBooking?.status || 'missing'}`, OPTIONS) context.createdResources.manualLoadingCompleteFixture = { schemaVersion: 1, sourceMarker: source.sourceMarker, preparedAt: new Date().toISOString(), stage: 'MANUAL_EVIDENCE_AND_COMPLETE_READY', pdaTaskId: taskId, loadingPlanId: id(source.loadingPlanId), containerBookingId: id(source.containerBookingId), cabinetNo: source.cabinetNo, containerNo: source.containerNo, warehouseId: id(source.warehouseId), evidenceTenantId: id(evidenceOwner.tenantId), evidenceInboundId: id(evidenceOwner.inboundId), requiredEvidenceTypes: REQUIRED_EVIDENCE_TYPES, beforeTaskVersion: Number(task.version), beforePlanVersion: Number(plan?.version), beforeItems, sourceInbounds: sourceInboundSnapshots(task.items), instructions: [ '在 PDA 当前任务中依次现场拍摄空仓、半仓、满仓、锁仓和带锁码照五类证据。', '五类分别对应 PHOTO_STEP_1、PHOTO_STEP_2、PHOTO_STEP_3、PHOTO_STEP_4、SEAL;SEAL 必须填写非空锁码。', '确认五类证据均为 ACTIVE 后,只点击一次最终完成;不要调用旧入库单兼容完成入口。', '完成成功后任务、计划和所有来源入库单应进入唯一终态,立即运行 manual-loading-complete-verify。', ], } }