import { readFile } from 'node:fs/promises' import { resolve } from 'node:path' const MODULE = 'manual-order-splitting-verify' const OPTIONS = { scenario: 'manual-order-splitting-readonly-verification', deploymentBlocker: true } const SMART = '/api/platform/v1/first-leg/smart-loading' const CONTAINERS = '/api/platform/v1/containers' function id(value) { return value == null ? '' : String(value) } function rows(value) { return Array.isArray(value) ? value : [] } async function loadManifest(context) { const path = resolve(context.options.manualSplitFixtureReport) const report = JSON.parse(await readFile(path, 'utf8')) const manifest = report?.createdResources?.manualOrderSplittingFixture const sourceGateway = String(report?.gateway || '').replace(/\/$/, '') const sameGateway = sourceGateway && sourceGateway === context.options.gateway context.assert(MODULE, '读取人工分柜准备报告', null, manifest?.schemaVersion === 1 && manifest?.stage === 'WAIT_SPLIT_READY_FOR_MANUAL_ASSIGNMENT' && Array.isArray(manifest.assignments) && manifest.assignments.length === 3 && sameGateway, `sourceMarker=${report?.marker || 'missing'}, schemaVersion=${manifest?.schemaVersion ?? 'missing'}, assignments=${manifest?.assignments?.length ?? 'missing'}, sameGateway=${Boolean(sameGateway)}`, OPTIONS) return sameGateway ? manifest : null } export async function run(context) { const manifest = await loadManifest(context) if (!manifest?.assignments?.length) return const planId = id(context.options.manualSplitPlanId) const planResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`) const plan = context.apiSuccess(MODULE, '读取人工操作后的智能装柜计划', planResponse, `loadingPlanId=${planId}`, OPTIONS) if (!plan) return const bookingId = id(plan.bookingId || plan.containerBookingId) const planItems = rows(plan.items) context.assert(MODULE, '智能装柜计划身份与状态可继续人工验收', planResponse, id(plan.id) === planId && bookingId && ['EDITING', 'LOADING'].includes(plan.status), `planId=${id(plan.id) || 'missing'}, bookingId=${bookingId || 'missing'}, status=${plan.status || 'missing'}, version=${plan.version ?? 'missing'}`, OPTIONS) if (!bookingId) return const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(bookingId)}`) const container = context.apiSuccess(MODULE, '读取人工分柜关联柜次', containerResponse, '', OPTIONS)?.container context.assert(MODULE, '柜次保持准备清单的仓库和渠道范围', containerResponse, id(container?.id) === bookingId && id(container?.warehouseId) === id(manifest.warehouseId) && container?.channelCode === manifest.channelCode, `containerId=${id(container?.id) || 'missing'}, warehouseId=${id(container?.warehouseId) || 'missing'}, channelCode=${container?.channelCode || 'missing'}`, OPTIONS) const expectedByInbound = new Map(manifest.assignments.map((item) => [id(item.inboundId), item])) for (const expected of manifest.assignments) { const matches = planItems.filter((item) => id(item.inboundId) === id(expected.inboundId)) const allocated = matches.reduce((sum, item) => sum + Number(item.packageCount || 0), 0) context.assert(MODULE, `${expected.role} 按清单完成计划分配`, planResponse, matches.length >= 1 && allocated === Number(expected.expectedAllocatedPackageCount), `inboundCode=${expected.inboundCode || 'missing'}, expected=${expected.expectedAllocatedPackageCount}, actual=${allocated}, itemCount=${matches.length}`, OPTIONS) } const foreignItems = planItems.filter((item) => !expectedByInbound.has(id(item.inboundId))) const controlItems = planItems.filter((item) => id(item.inboundId) === id(manifest.unassignedControl?.inboundId)) context.assert(MODULE, '计划未混入清单外或未分配对照入库单', planResponse, foreignItems.length === 0 && controlItems.length === 0, `foreignInboundIds=${foreignItems.map((item) => id(item.inboundId)).join(',') || 'none'}, controlItemCount=${controlItems.length}`, OPTIONS) const candidatesPath = `${SMART}/inbound-candidates?bookingId=${encodeURIComponent(bookingId)}&warehouseId=${encodeURIComponent(manifest.warehouseId)}` const candidatesResponse = await context.request('GET', candidatesPath) const candidates = context.apiSuccess(MODULE, '回查人工分柜后的入仓码候选余量', candidatesResponse, '', OPTIONS) const candidateRows = rows(candidates) const candidateScopeValid = candidateRows.every((item) => (!item.channelCode || item.channelCode === manifest.channelCode) && (!item.warehouseId || id(item.warehouseId) === id(manifest.warehouseId))) context.assert(MODULE, '候选列表未出现异渠道或异仓入库单', candidatesResponse, candidateScopeValid, `candidateCount=${candidateRows.length}, channelCode=${manifest.channelCode}, warehouseId=${manifest.warehouseId}`, OPTIONS) for (const expected of [...manifest.assignments, manifest.unassignedControl]) { const candidate = candidateRows.find((item) => id(item.inboundId) === id(expected.inboundId)) const actualRemaining = candidate ? Number(candidate.availablePackageCount) : 0 context.assert(MODULE, `${expected.role} 分配后剩余箱数正确`, candidatesResponse, actualRemaining === Number(expected.expectedRemainingPackageCount), `inboundCode=${expected.inboundCode || 'missing'}, expectedRemaining=${expected.expectedRemainingPackageCount}, actualRemaining=${actualRemaining}`, OPTIONS) } context.createdResources.manualOrderSplittingVerification = { sourceMarker: manifest.marker, verifiedAt: new Date().toISOString(), loadingPlanId: planId, containerBookingId: bookingId, tenantId: id(manifest.tenantId), warehouseId: id(manifest.warehouseId), channelCode: manifest.channelCode, planStatus: plan.status, planVersion: Number(plan.version), verifiedAssignments: manifest.assignments.map((item) => ({ role: item.role, orderId: id(item.orderId), inboundId: id(item.inboundId), expectedAllocatedPackageCount: Number(item.expectedAllocatedPackageCount) })), note: '本模块只读核验计划分柜;父子订单在 PDA 实装结算后另行核验。', } }