const MODULE = 'phase3-evidence-gate' const OPTIONS = { scenario: 'loading-evidence-gate', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function successful(response) { return response?.http === 200 && String(response?.body?.code ?? '') === '200' } export async function run(context) { const fixture = context.createdResources?.phase3Loading const tenantId = id(fixture?.tenantId) const inboundId = id(fixture?.inboundId) const taskId = id(fixture?.canonicalPdaTaskId || fixture?.pickingTaskId) const loadingPlanId = id(fixture?.loadingPlanId) const containerBookingId = id(fixture?.containerBookingId) const warehouseId = id(fixture?.warehouseId) if (!tenantId || !inboundId || !taskId || !loadingPlanId || !containerBookingId || !warehouseId || fixture.marker !== context.marker) { context.addCoverageGap(MODULE, OPTIONS.scenario, '五类证据多图追加、作废审计和有效证据门禁', '本轮未取得带 taskId/inboundId/loadingPlanId/containerBookingId 的 phase3-loading marker 隔离证据 fixture,未使用历史资源', 'fixture') return } // AWAITING_REVIEW is a PDA workflow state, not evidence permission. Evidence is // opened only after the server-side inbound aggregate has entered LOADING. // The list endpoint only exposes tasks that are currently in the loading queue. // Once PDA completion advances the task, use its stable task identity instead. const taskResponse = await context.request('GET', `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(warehouseId)}`) const task = successful(taskResponse) ? taskResponse.body?.data : null const taskItem = task?.items?.find?.((item) => id(item?.inboundId) === inboundId) const taskIdentityValid = id(task?.id) === taskId && id(task?.loadingPlanId) === loadingPlanId && id(task?.containerBookingId) === containerBookingId && Boolean(taskItem) && id(taskItem?.inboundId) === inboundId context.assert(MODULE, '证据前置任务身份使用本轮四元稳定关联', taskResponse, taskIdentityValid, `taskId=${id(task?.id) || 'missing'}, inboundId=${id(taskItem?.inboundId) || 'missing'}, loadingPlanId=${id(task?.loadingPlanId) || 'missing'}, containerBookingId=${id(task?.containerBookingId) || 'missing'}`, OPTIONS) if (!taskIdentityValid) return const inboundResponse = await context.request('GET', `/api/platform/inbounds/${encodeURIComponent(inboundId)}?tenantId=${encodeURIComponent(tenantId)}`) const inbound = successful(inboundResponse) ? inboundResponse.body?.data : null const evidenceReadable = ['LOADING', 'LOADED'].includes(inbound?.summary?.processStatus) context.assert(MODULE, '服务端入库进入 LOADING 或 LOADED 后开放装柜证据只读回查', inboundResponse, evidenceReadable, `processStatus=${inbound?.summary?.processStatus || 'missing'}, taskWorkflow=${task?.workflowStatus || 'missing'}`, OPTIONS) if (!evidenceReadable) { context.addCoverageGap(MODULE, OPTIONS.scenario, '服务端装柜阶段前不请求证据', `taskId=${taskId}, inboundId=${inboundId}, loadingPlanId=${loadingPlanId}, containerBookingId=${containerBookingId}, processStatus=${inbound?.summary?.processStatus || 'missing'};AWAITING_REVIEW 不作为证据开放条件`, 'deployment-blocker') return } const path = `/api/platform/v1/inbounds/${inboundId}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}` const response = await context.request('GET', path) const data = context.apiSuccess(MODULE, '复核本轮装柜证据聚合', response, `inboundId=${fixture.inboundId}`, OPTIONS) const evidences = Array.isArray(data?.evidences) ? data.evidences : [] const required = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'] const active = evidences.filter((item) => item.status !== 'VOID') const voided = evidences.find((item) => id(item.id) === id(fixture.voidedEvidenceId)) const missingTypeVoided = evidences.find((item) => id(item.id) === id(fixture.missingTypeVoidedEvidenceId)) context.assert(MODULE, '五类有效证据齐全且同类型多图追加不覆盖历史', response, required.every((type) => active.some((item) => item.evidenceType === type)) && evidences.filter((item) => item.evidenceType === 'PHOTO_STEP_1').length >= 2 && active.some((item) => item.evidenceType === 'SEAL' && item.sealNo === fixture.sealNo) && active.every((item) => String(item.fileName || '').includes(context.marker) && String(item.remark || '').includes(context.marker)), `active=${active.length}, types=${[...new Set(active.map((item) => item.evidenceType))].join(',')}`, OPTIONS) context.assert(MODULE, '作废证据保留原因、操作者和时间且不计入有效门禁', response, Boolean(voided) && voided.status === 'VOID' && voided.voidReason === `${context.marker}-wrong-photo` && Boolean(voided.voidedBy) && Boolean(voided.voidedAt) && !active.some((item) => id(item.id) === id(fixture.voidedEvidenceId)), `voidedEvidenceId=${id(fixture.voidedEvidenceId) || 'missing'}, status=${voided?.status || 'missing'}`, OPTIONS) context.assert(MODULE, '缺类作废审计保留且补传证据恢复有效门禁', response, Boolean(missingTypeVoided) && missingTypeVoided.status === 'VOID' && missingTypeVoided.voidReason === `${context.marker}-temporary-missing-type` && Boolean(missingTypeVoided.voidedBy) && Boolean(missingTypeVoided.voidedAt) && active.some((item) => item.evidenceType === 'PHOTO_STEP_4' && id(item.fileId) === id(fixture.replacementEvidenceFileId)), `voidedEvidenceId=${id(fixture.missingTypeVoidedEvidenceId) || 'missing'}, replacementFileId=${id(fixture.replacementEvidenceFileId) || 'missing'}`, OPTIONS) }