import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const MODULE = 'manual-pda-ready-verify' const OPTIONS = { scenario: 'manual-pda-ready-readonly-verification', deploymentBlocker: true } const SMART = '/api/platform/v1/first-leg/smart-loading' const FIRST_LEG = '/api/platform/v1/first-leg' const CONTAINERS = '/api/platform/v1/containers' const PDA = `${FIRST_LEG}/pda-loading` function id(value) { return value == null ? '' : String(value) } function rows(value) { return Array.isArray(value) ? value : [] } function positiveId(value) { return /^[1-9][0-9]*$/.test(id(value)) } function itemSnapshot(value) { return rows(value).map((item) => ({ id: id(item.id), inboundId: id(item.inboundId), packageCount: Number(item.packageCount) })) .sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id)) } async function loadFixture(context) { const report = JSON.parse(await readFile(resolve(context.options.manualPdaSourceReport), 'utf8')) const fixture = report?.createdResources?.manualPdaReadyFixture const sourceGateway = String(report?.gateway || '').replace(/\/$/, '') const valid = sourceGateway === context.options.gateway && fixture?.schemaVersion === 1 && fixture?.stage === 'MANUAL_BOOKING_LINK_CONFIRM_READY' && fixture?.loadingPlanId && fixture?.containerBookingId && Array.isArray(fixture?.beforeItems) && Array.isArray(fixture?.expectedAssignments) && fixture.expectedAssignments.length > 0 context.assert(MODULE, '读取同环境 PDA 就绪准备报告', null, valid, `sourceMarker=${report?.marker || 'missing'}, sameGateway=${sourceGateway === context.options.gateway}, loadingPlanId=${id(fixture?.loadingPlanId) || 'missing'}`, OPTIONS) return valid ? fixture : null } export async function run(context) { const fixture = await loadFixture(context) if (!fixture) return const planId = id(fixture.loadingPlanId) const containerId = id(fixture.containerBookingId) const planResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`) const plan = context.apiSuccess(MODULE, '读取预装确认后的原智能装柜计划', planResponse, '', OPTIONS) const afterItems = itemSnapshot(plan?.items) context.assert(MODULE, '订舱关联和预装确认后计划身份、状态及明细稳定', planResponse, id(plan?.id) === planId && id(plan?.bookingId || plan?.containerBookingId) === containerId && plan?.status === 'EDITING' && Number(plan?.version) > Number(fixture.beforePlanVersion) && JSON.stringify(afterItems) === JSON.stringify(fixture.beforeItems), `status=${plan?.status || 'missing'}, beforeVersion=${fixture.beforePlanVersion}, afterVersion=${plan?.version ?? 'missing'}, items=${afterItems.length}`, OPTIONS) const containerResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}`) const container = context.apiSuccess(MODULE, '读取关联真实柜号后的稳定柜次', containerResponse, '', OPTIONS) const oceanBookingId = id(container?.oceanBookingId) const realContainerNo = String(container?.containerNo || '') context.assert(MODULE, '真实柜号和订舱原地补全且内部柜次身份不变', containerResponse, id(container?.id) === containerId && container?.cabinetNo === fixture.cabinetNo && realContainerNo.length > 0 && positiveId(oceanBookingId) && Number(container?.version) > Number(fixture.beforeContainerVersion) && id(container?.warehouseId) === id(fixture.warehouseId) && container?.channelCode === fixture.channelCode, `containerId=${id(container?.id) || 'missing'}, cabinetNo=${container?.cabinetNo || 'missing'}, containerNo=${realContainerNo || 'missing'}, oceanBookingId=${oceanBookingId || 'missing'}`, OPTIONS) if (!positiveId(oceanBookingId) || !realContainerNo) return const bookingResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}`) const booking = context.apiSuccess(MODULE, '读取关联后的 RELEASED 订舱与 allocation', bookingResponse, '', OPTIONS) const allocation = rows(booking?.containers).find((item) => id(item.containerBookingId) === containerId && item.containerNo === realContainerNo) context.assert(MODULE, '订舱已放舱且唯一 allocation 指向原柜次', bookingResponse, booking?.oceanBooking?.status === 'RELEASED' && positiveId(allocation?.requirementId) && positiveId(allocation?.allocationId) && rows(booking?.containers).filter((item) => id(item.containerBookingId) === containerId).length === 1, `status=${booking?.oceanBooking?.status || 'missing'}, bookingNo=${booking?.oceanBooking?.bookingNo || 'missing'}, requirementId=${id(allocation?.requirementId) || 'missing'}, allocationId=${id(allocation?.allocationId) || 'missing'}`, OPTIONS) const detailResponse = await context.request('GET', `${FIRST_LEG}/loading-plans/${encodeURIComponent(planId)}?warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const detail = context.apiSuccess(MODULE, '读取整份预装确认结果', detailResponse, '', OPTIONS) const taskId = id(detail?.summary?.readiness?.pdaTaskId) context.assert(MODULE, '预装确认状态为 CONFIRMED 并返回唯一 PDA taskId', detailResponse, id(detail?.summary?.planId) === planId && detail?.summary?.status === 'EDITING' && detail?.summary?.readiness?.confirmationStatus === 'CONFIRMED' && detail?.summary?.readiness?.canConfirm === false && detail?.summary?.readiness?.readyForPda === true && positiveId(taskId), `confirmationStatus=${detail?.summary?.readiness?.confirmationStatus || 'missing'}, readyForPda=${detail?.summary?.readiness?.readyForPda}, taskId=${taskId || 'missing'}`, OPTIONS) if (!positiveId(taskId)) return const taskResponse = await context.request('GET', `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const task = context.apiSuccess(MODULE, '读取唯一待领取 PDA 任务', taskResponse, '', OPTIONS) const plannedPackages = rows(task?.items).reduce((sum, item) => sum + Number(item.plannedPackageCount || 0), 0) const expectedPackages = rows(fixture.expectedAssignments) .reduce((sum, item) => sum + Number(item.expectedAllocatedPackageCount || 0), 0) context.assert(MODULE, 'PDA 任务四元身份、未领取状态和计划箱数正确', taskResponse, id(task?.id) === taskId && id(task?.loadingPlanId) === planId && id(task?.containerBookingId) === containerId && task?.cabinetNo === fixture.cabinetNo && task?.currentContainerNo === realContainerNo && task?.taskStatus === 'LOADING' && task?.workflowStatus === 'UNASSIGNED' && task?.assignment == null && rows(task?.items).length > 0 && plannedPackages === expectedPackages, `taskId=${id(task?.id) || 'missing'}, taskStatus=${task?.taskStatus || 'missing'}, workflowStatus=${task?.workflowStatus || 'missing'}, plannedPackages=${plannedPackages}, expectedPackages=${expectedPackages}`, OPTIONS) for (const assignment of fixture.expectedAssignments || []) { const inboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(assignment.inboundId)}?tenantId=${encodeURIComponent(fixture.tenantId)}`) const inbound = context.apiSuccess(MODULE, `${assignment.role} claim 前回查入库状态`, inboundResponse, '', OPTIONS) context.assert(MODULE, `${assignment.role} 在 PDA 领取前仍为 WAIT_SPLIT`, inboundResponse, id(inbound?.summary?.id) === id(assignment.inboundId) && inbound?.summary?.processStatus === 'WAIT_SPLIT', `inboundId=${id(inbound?.summary?.id) || 'missing'}, status=${inbound?.summary?.processStatus || 'missing'}`, OPTIONS) } context.createdResources.manualPdaReadyVerification = { sourceMarker: fixture.sourceMarker, verifiedAt: new Date().toISOString(), stage: 'PDA_UNASSIGNED', oceanBookingId, bookingNo: booking?.oceanBooking?.bookingNo || null, requirementId: id(allocation?.requirementId), allocationId: id(allocation?.allocationId), loadingPlanId: planId, containerBookingId: containerId, cabinetNo: fixture.cabinetNo, containerNo: realContainerNo, pdaTaskId: taskId, tenantId: id(fixture.tenantId), warehouseId: id(fixture.warehouseId), plannedPackageCount: plannedPackages, nextStage: 'PDA 人工领取任务后进入 LOADING,再执行扫码、批量提交和待复核验收。', } }