import { validDirectDeliveryReplayProof } from './direct-delivery-replay-proof.mjs' const MODULE = 'phase4-cross-region-events' const OPTIONS = { scenario: 'phase4-cross-region-events', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function records(data) { return Array.isArray(data?.records) ? data.records : Array.isArray(data) ? data : [] } function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function denied(response) { return response?.http === 401 || response?.http === 403 || ['401', '403', 'A0401', 'A0403'].includes(String(response?.body?.code)) } function traced(response) { return Boolean(response?.requestId) && Boolean(response?.traceId) } function evidence(response, extra = '') { return `${extra}${extra ? '; ' : ''}HTTP=${response?.http ?? 'missing'}, code=${response?.body?.code ?? 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function blockingGap(context, name, reason, category, response = null) { context.addCoverageGap(MODULE, OPTIONS.scenario, name, reason, category, { deploymentBlocker: true }) context.addStep(MODULE, name, response, false, reason, OPTIONS) throw new Error(`blocking coverage gap: ${name}: ${reason}`) } async function poll(request, matches, attempts = 12) { let last = null for (let index = 0; index < attempts; index += 1) { last = await request() if (matches(last)) return { response: last, attempts: index + 1 } if (index + 1 < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000)) } return { response: last, attempts } } function sameIds(left, right) { const ids = (value) => records(value).map((item) => id(item?.id)).filter(Boolean).sort() return JSON.stringify(ids(left)) === JSON.stringify(ids(right)) } export async function run(context) { if (!context.token) blockingGap(context, '统一会话前置', '平台访问令牌缺失', 'missing-session') const authorized = await context.request('GET', '/api/platform/v1/last-leg/orders?page=1&size=1') context.assert(MODULE, '统一平台会话可访问Lastleg授权列表', authorized, success(authorized) && traced(authorized), evidence(authorized), OPTIONS) const anonymous = await context.request('GET', '/api/platform/v1/last-leg/orders?page=1&size=1', undefined, '') context.assert(MODULE, '无会话不得访问Lastleg', anonymous, denied(anonymous) && traced(anonymous), evidence(anonymous), OPTIONS) const phase3 = context.createdResources?.phase3Loading if (phase3?.marker !== context.marker || !phase3?.finalizeSucceeded || !phase3?.containerBookingId || !phase3?.containerNo || !phase3?.loadingPlanId) { blockingGap(context, '国内装柜完成事件前置', '缺少本轮marker的LOADED装柜事实;必须先完成三期装柜链,禁止借用历史事件', 'missing-marker-loading-completed') } const domestic = await context.request('GET', `/api/platform/v1/containers/domestic/${encodeURIComponent(id(phase3.containerBookingId))}`) context.assert(MODULE, '同一平台会话可访问国内授权详情', domestic, success(domestic) && traced(domestic), evidence(domestic), OPTIONS) const taillegPath = `/api/platform/v1/last-leg/orders?page=1&size=200&containerNo=${encodeURIComponent(phase3.containerNo)}` const delivered = await poll( () => context.request('GET', taillegPath), (response) => success(response) && records(response.body?.data) .some((item) => item?.containerNo === phase3.containerNo)) const deliveredRows = records(delivered.response?.body?.data) .filter((item) => item?.containerNo === phase3.containerNo) context.assert(MODULE, '国内装柜完成经Outbox直接投递生成Lastleg订单', delivered.response, deliveredRows.length > 0 && traced(delivered.response), evidence(delivered.response, `pollRequests=${delivered.attempts}, containerNo=${phase3.containerNo}, orders=${deliveredRows.length}`), OPTIONS) if (deliveredRows.length === 0) { blockingGap(context, '国内到海外事件闭环', `本轮柜号${phase3.containerNo}在轮询后仍无Lastleg订单;需结合requestId/traceId检查生产服务Outbox重试与下游诊断`, 'cn-overseas-event-not-delivered', delivered.response) } const deliveredAgain = await context.request('GET', taillegPath) context.assert(MODULE, '装柜结果重复读取稳定(非事件重放证据)', deliveredAgain, success(deliveredAgain) && sameIds(delivered.response.body?.data, deliveredAgain.body?.data) && traced(deliveredAgain), evidence(deliveredAgain, `firstOrders=${deliveredRows.map((item) => id(item.id)).join(',')}`), OPTIONS) const overseasPda = context.createdResources?.phase4OverseasPda if (overseasPda?.marker !== context.marker || overseasPda?.finalStatus !== 'COMPLETED' || !overseasPda?.customerId || !overseasPda?.orderId) { blockingGap(context, '海外柜完成事件前置', '缺少本轮marker的海外PDA COMPLETED柜事实;必须先完成四期海外PDA闭环', 'missing-marker-overseas-container-completed') } const orderDetailPath = `/api/platform/v1/last-leg/orders/${encodeURIComponent(overseasPda.orderId)}?customerId=${encodeURIComponent(overseasPda.customerId)}` const orderDetail = await context.request('GET', orderDetailPath) const order = orderDetail.body?.data?.order || orderDetail.body?.data const completedContainerNo = order?.containerNo || '' context.assert(MODULE, '读取海外完成柜来源尾程单', orderDetail, success(orderDetail) && Boolean(completedContainerNo) && traced(orderDetail), evidence(orderDetail, `orderId=${overseasPda.orderId}, containerNo=${completedContainerNo || 'missing'}`), OPTIONS) if (!completedContainerNo) { blockingGap(context, '海外完成柜来源身份', '尾程订单未返回containerNo,无法核对国内回写', 'missing-overseas-container-identity', orderDetail) } const containerPagePath = `/api/platform/v1/containers/domestic?scope=HISTORY&containerNo=${encodeURIComponent(completedContainerNo)}&page=1&size=20` const containerPage = await context.request('GET', containerPagePath) const domesticMatches = records(containerPage.body?.data) .map((item) => item?.container).filter((item) => item?.containerNo === completedContainerNo) context.assert(MODULE, '物理柜号唯一定位国内柜聚合', containerPage, success(containerPage) && domesticMatches.length === 1 && traced(containerPage), evidence(containerPage, `containerNo=${completedContainerNo}, matches=${domesticMatches.length}`), OPTIONS) if (domesticMatches.length !== 1) { blockingGap(context, '海外到国内柜身份映射', `物理柜号${completedContainerNo}匹配国内柜数量为${domesticMatches.length},无法验证回写`, 'domestic-container-identity-not-unique', containerPage) } const overviewPath = `/api/platform/v1/containers/${encodeURIComponent(id(domesticMatches[0].id))}/overview` const writtenBack = await poll( () => context.request('GET', overviewPath), (response) => { const container = response?.body?.data?.container return success(response) && Boolean(container?.unloadingCompletedAt) && Boolean(container?.unloadingPhotoUploadedAt) && Boolean(container?.estimatedEmptyReportedAt) }) const snapshot = writtenBack.response?.body?.data?.container || {} const completedAt = Date.parse(snapshot.unloadingCompletedAt) const uploadedAt = Date.parse(snapshot.unloadingPhotoUploadedAt) const estimatedAt = Date.parse(snapshot.estimatedEmptyReportedAt) const validTimeline = Number.isFinite(completedAt) && Number.isFinite(uploadedAt) && Number.isFinite(estimatedAt) && completedAt >= uploadedAt && estimatedAt - completedAt === 72 * 60 * 60 * 1_000 context.assert(MODULE, '海外柜完成经Outbox直接投递回写Firstleg时间事实', writtenBack.response, validTimeline && traced(writtenBack.response), evidence(writtenBack.response, `pollRequests=${writtenBack.attempts}, completed=${snapshot.unloadingCompletedAt || 'missing'}, photo=${snapshot.unloadingPhotoUploadedAt || 'missing'}, estimatedEmpty=${snapshot.estimatedEmptyReportedAt || 'missing'}`), OPTIONS) if (!validTimeline) { blockingGap(context, '海外到国内事件闭环', `柜号${completedContainerNo}轮询后仍无有效卸柜完成/拍照/预计报空回写;需检查生产服务Outbox重试与诊断证据`, 'overseas-cn-event-not-delivered', writtenBack.response) } const writtenBackAgain = await context.request('GET', overviewPath) const replaySnapshot = writtenBackAgain.body?.data?.container || {} const frozen = ['unloadingCompletedAt', 'unloadingPhotoUploadedAt', 'estimatedEmptyReportedAt', 'actualEmptyReportedAt', 'emptyReportSourceVersion'] context.assert(MODULE, '卸柜时间事实重复读取稳定(非事件重放证据)', writtenBackAgain, success(writtenBackAgain) && frozen.every((field) => (replaySnapshot[field] ?? null) === (snapshot[field] ?? null)) && traced(writtenBackAgain), evidence(writtenBackAgain, `containerId=${id(domesticMatches[0].id)}, sourceVersion=${snapshot.emptyReportSourceVersion ?? 'missing'}`), OPTIONS) const reader = context.context?.directDeliveryReplayEvidenceReader if (typeof reader !== 'function') blockingGap(context, '真实投递重试与同事件幂等证据', '两次GET仅证明读取稳定;需按event-outbox-operations.mjs的受保护内部运维流程,在本轮事件上验证故障后有限重试/DEAD及同eventId+payloadHash重复消费,保存逐次尝试与业务计数证据。不得重放SENT、改表或伪造新事件。', 'missing-direct-delivery-replay-evidence') const scope = { marker: context.marker, loadingPlanId: id(phase3.loadingPlanId), customerId: id(phase3.tenantId), containerBookingId: id(phase3.containerBookingId) } const proof = await reader(scope) context.createdResources.directDeliveryReplayProof = proof const valid = validDirectDeliveryReplayProof(proof, scope) context.assert(MODULE, '本轮原事件12次失败DEAD人工恢复DUPLICATE且业务零新增', null, valid, `loadingPlanId=${scope.loadingPlanId}, attempts=${proof?.attempts?.length || 0}, restored=${proof?.restored === true}`, OPTIONS) if (!valid) blockingGap(context, '真实投递重试证据不完整或身份漂移', '本轮事件身份/摘要、12次有限重试、DEAD、人工恢复审计、重复消费及业务计数和恢复直连必须同时核平', 'invalid-direct-delivery-replay-evidence') }