const MODULE = 'phase3-loading-finalize' const OPTIONS = { scenario: 'phase3-loading-hard-gate-finalize', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } async function request(context, method, path, body, headers = {}) { try { const response = await fetch(`${context.options.gateway}${path}`, { method, headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000), }) const raw = await response.text(); let payload = null try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ } return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null, requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id') } } catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } } } export async function run(context) { const fixture = context.createdResources?.phase3Loading const tenantId = id(fixture?.tenantId); const inboundId = id(fixture?.inboundId) if (!tenantId || !inboundId || fixture?.marker !== context.marker || fixture?.loadingPlanStatus !== 'COMPLETED') { context.addStep(MODULE, '取得下游门禁待完成装柜 fixture', null, false, `tenantId=${tenantId || 'missing'}, inboundId=${inboundId || 'missing'}, marker=${fixture?.marker || 'missing'}, plan=${fixture?.loadingPlanStatus || 'missing'}`, OPTIONS) return } const detailPath = `/api/platform/inbounds/${inboundId}?tenantId=${encodeURIComponent(tenantId)}` const beforeResponse = await context.request('GET', detailPath) const before = context.apiSuccess(MODULE, '读取下游门禁齐备后的装柜状态', beforeResponse, '', OPTIONS) const version = Number(before?.summary?.version) context.assert(MODULE, '最终装柜前保持 LOADING 且版本有效', beforeResponse, before?.summary?.processStatus === 'LOADING' && Number.isInteger(version), `status=${before?.summary?.processStatus || 'missing'}, version=${before?.summary?.version ?? 'missing'}`, OPTIONS) if (!Number.isInteger(version)) return const base = `/api/platform/v1/inbounds/${inboundId}` const completeResponse = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`, { expectedVersion: version, remark: context.marker }, { 'Idempotency-Key': `${context.marker}-p3-complete-after-downstream`.slice(0, 128) }) const completed = context.apiSuccess(MODULE, '订舱拖车报关齐备后完成装柜', completeResponse, '', OPTIONS) const evidences = completed?.evidences?.evidences || [] context.assert(MODULE, '统一硬门禁通过后推进 LOADED 且五类有效证据仍完整', completeResponse, completed?.mainStatus === 'LOADED' && fixture.evidenceTypes?.every?.((type) => evidences.some((item) => item.evidenceType === type && item.status !== 'VOID')), `mainStatus=${completed?.mainStatus || 'missing'}, evidences=${evidences.length}`, OPTIONS) const replayResponse = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`, { expectedVersion: version, remark: context.marker }, { 'Idempotency-Key': `${context.marker}-p3-complete-after-downstream`.slice(0, 128) }) context.assert(MODULE, 'LOADED 完成请求 Idempotency-Key 重放保持终态', replayResponse, replayResponse.http === 200 && String(replayResponse.body?.code) === '200' && replayResponse.body?.data?.mainStatus === 'LOADED', `HTTP=${replayResponse.http}, code=${replayResponse.body?.code ?? 'missing'}, status=${replayResponse.body?.data?.mainStatus || 'missing'}`, OPTIONS) const finalResponse = await context.request('GET', detailPath) const finalData = context.apiSuccess(MODULE, '回查三期装柜完成终态', finalResponse, '', OPTIONS) context.assert(MODULE, '入库单终态 LOADED、版本递增且审计历史存在', finalResponse, finalData?.summary?.processStatus === 'LOADED' && Number(finalData?.summary?.version) === version + 1 && finalData?.statusHistory?.some?.((item) => item.action === 'COMPLETE_LOADING' && item.operatorName && item.fromStatus === 'LOADING' && item.toStatus === 'LOADED'), `status=${finalData?.summary?.processStatus || 'missing'}, version=${finalData?.summary?.version ?? 'missing'}, histories=${finalData?.statusHistory?.length ?? 'n/a'}`, OPTIONS) context.createdResources.phase3Loading = { ...fixture, inboundVersion: Number(finalData?.summary?.version), inboundStatus: 'LOADED', finalStatus: 'LOADED', } }