import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const MODULE = 'manual-preload-push-verify' const OPTIONS = { scenario: 'manual-preload-push-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 codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } function itemSnapshot(items) { return rows(items).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.manualPreloadSourceReport), 'utf8')) const fixture = report?.createdResources?.manualPreloadPushFixture const sourceGateway = String(report?.gateway || '').replace(/\/$/, '') const valid = sourceGateway === context.options.gateway && fixture?.schemaVersion === 1 && fixture?.stage === 'MANUAL_PUSH_PRELOAD_READY' && fixture?.loadingPlanId && fixture?.containerBookingId && Array.isArray(fixture?.beforeItems) context.assert(MODULE, '读取同环境推送预装准备报告', 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, '无号推送后计划保持 EDITING、版本递增且明细零漂移', planResponse, id(plan?.id) === planId && id(plan?.bookingId || plan?.containerBookingId) === containerId && plan?.status === 'EDITING' && plan?.containerNo == null && 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) if (!plan || plan.status !== 'EDITING') return const linksResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}/preloads`) const links = context.apiSuccess(MODULE, '回查推送原子建立的预装 bridge', linksResponse, '', OPTIONS) const linkRows = rows(links) for (const expected of fixture.expectedAssignments || []) { const matched = linkRows.filter((link) => id(link.inboundId) === id(expected.inboundId)) const allocated = matched.reduce((sum, link) => sum + Number(link.allocatedPackageCount || 0), 0) context.assert(MODULE, `${expected.role} 预装 bridge 数量与人工分柜一致`, linksResponse, matched.length >= 1 && allocated === Number(expected.expectedAllocatedPackageCount) && matched.every((link) => id(link.loadingPlanId) === planId && id(link.containerBookingId) === containerId && link.preloadStatus === 'PENDING' && link.loadingPlanStatus === 'EDITING' && Number(link.loadingPlanVersion) === Number(plan.version)), `inboundId=${id(expected.inboundId)}, expected=${expected.expectedAllocatedPackageCount}, actual=${allocated}, links=${matched.length}`, OPTIONS) } const expectedInboundIds = new Set((fixture.expectedAssignments || []).map((item) => id(item.inboundId))) const unexpectedLinks = linkRows.filter((link) => !expectedInboundIds.has(id(link.inboundId))) context.assert(MODULE, '预装 bridge 未混入人工分柜清单外入库单', linksResponse, unexpectedLinks.length === 0 && linkRows.length >= expectedInboundIds.size, `linkCount=${linkRows.length}, unexpectedInboundIds=${unexpectedLinks.map((link) => id(link.inboundId)).join(',') || 'none'}`, OPTIONS) const detailResponse = await context.request('GET', `${FIRST_LEG}/loading-plans/${encodeURIComponent(planId)}?warehouseId=${encodeURIComponent(fixture.warehouseId)}`) const detail = context.apiSuccess(MODULE, '读取推送后正式预装清单详情', detailResponse, '', OPTIONS) context.assert(MODULE, '预装清单保持 EDITING 且无号阶段不能确认或创建 PDA', detailResponse, id(detail?.summary?.planId) === planId && detail?.summary?.status === 'EDITING' && detail?.summary?.readiness?.canConfirm === false && detail?.summary?.readiness?.readyForPda === false && detail?.summary?.readiness?.pdaTaskId == null, `planId=${id(detail?.summary?.planId) || 'missing'}, status=${detail?.summary?.status || 'missing'}, canConfirm=${detail?.summary?.readiness?.canConfirm}, readyForPda=${detail?.summary?.readiness?.readyForPda}`, OPTIONS) const taskResponse = await context.request('GET', `${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(fixture.warehouseId)}`) context.addStep(MODULE, '无真实柜号推送后 PDA 查询仍为 A0404', taskResponse, taskResponse.http === 200 && codeOf(taskResponse) === 'A0404', `HTTP=${taskResponse.http}, code=${codeOf(taskResponse)}, requestId=${taskResponse.requestId || 'missing'}, traceId=${taskResponse.traceId || 'missing'}`, OPTIONS) const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`) const container = context.apiSuccess(MODULE, '回查推送后内部柜次身份', containerResponse, '', OPTIONS)?.container context.assert(MODULE, '推送预装不伪造真实柜号或订舱关联', containerResponse, id(container?.id) === containerId && container?.cabinetNo === fixture.cabinetNo && container?.containerNo == null && container?.oceanBookingId == null, `containerId=${id(container?.id) || 'missing'}, cabinetNo=${container?.cabinetNo || 'missing'}, containerNo=${container?.containerNo || 'null'}, oceanBookingId=${id(container?.oceanBookingId) || 'null'}`, OPTIONS) context.createdResources.manualPreloadPushVerification = { sourceMarker: fixture.sourceMarker, verifiedAt: new Date().toISOString(), stage: 'PRELOAD_EDITING', loadingPlanId: planId, containerBookingId: containerId, cabinetNo: fixture.cabinetNo, tenantId: id(fixture.tenantId), warehouseId: id(fixture.warehouseId), channelCode: fixture.channelCode, planVersion: Number(plan.version), preloadLinkCount: linkRows.length, pdaTaskCreated: false, expectedAssignments: fixture.expectedAssignments, nextStage: '关联 RELEASED 订舱与真实柜号后,人工执行整份预装计划确认。', } }