const MODULE = 'phase3-loading-finalize' const OPTIONS = { scenario: 'phase3-loading-hard-gate-finalize', deploymentBlocker: true } function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } function evidenceSignature(data) { return JSON.stringify((data?.evidences || []).map((item) => ({ id: id(item?.id), type: item?.evidenceType || null, fileId: id(item?.fileId), status: item?.status || null, version: Number(item?.version), sealNo: item?.sealNo || null, voidReason: item?.voidReason || null, voidedBy: id(item?.voidedBy), voidedAt: item?.voidedAt || null, }))) } export function buildFinalizeResource(fixture, finalData, completeResponse) { const observedStatus = finalData?.summary?.processStatus || fixture?.inboundStatus || 'UNKNOWN' const observedVersion = Number(finalData?.summary?.version) const loaded = observedStatus === 'LOADED' const version = Number.isInteger(observedVersion) ? observedVersion : fixture?.inboundVersion return { ...fixture, ...(Number.isInteger(version) ? { inboundVersion: version } : {}), inboundStatus: observedStatus, finalStatus: observedStatus, finalizeSucceeded: loaded, notCleanedReason: loaded ? null : `装柜完成未成功,保留真实状态 ${observedStatus};completeCode=${codeOf(completeResponse)}`, } } export function collectPhase4InboundProducerFixture(previous, producer) { const existing = previous?.marker === producer?.marker && Array.isArray(previous?.containers) ? previous.containers : [] const containers = [...existing.filter((item) => item.eventKey !== producer.eventKey), producer] const byDestinationType = Object.fromEntries(['FBA', 'OVERSEAS'].map((destinationType) => [ destinationType, containers.filter((item) => item.destinationType === destinationType), ])) const dropshipContainers = containers.filter((item) => item.fulfillmentType === 'ONE_CLICK_DROPSHIP') const distinctContainerCount = new Set(containers.map((item) => item.containerNo).filter(Boolean)).size return { marker: producer.marker, containers, byDestinationType, readiness: { fbaContainerCount: byDestinationType.FBA.length, overseasContainerCount: byDestinationType.OVERSEAS.length, dropshipContainerCount: dropshipContainers.length, distinctContainerCount, readyForPhase4Pda: byDestinationType.FBA.length >= 2 && byDestinationType.OVERSEAS.length >= 1 && dropshipContainers.length >= 1 && distinctContainerCount >= 4, }, requiredEntryOrchestration: [ 'DROPSHIP-FBA: public order fulfillmentType -> inbound-measurement -> phase3-loading -> phase3-loading-finalize -> overseas PDA', 'FBA: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize', 'OVERSEAS-PRIMARY: orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize', 'SECONDARY(FBA|OVERSEAS): orders -> inbound-measurement -> phase3-loading -> phase3-customs -> phase3-loading-finalize', ], } } 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 } } } async function waitForCompletion(context, taskDetailPath, operationsPath, attempts = 90) { let taskResponse = null let task = null let operationsResponse = null let operations = null for (let attempt = 1; attempt <= attempts; attempt += 1) { taskResponse = await context.request('GET', taskDetailPath) task = taskResponse?.http === 200 && String(taskResponse?.body?.code) === '200' ? taskResponse.body.data : null if (task?.taskStatus === 'COMPLETED' || task?.taskStatus === 'COMPLETION_FAILED') { operationsResponse = await context.request('GET', operationsPath) operations = operationsResponse?.http === 200 && String(operationsResponse?.body?.code) === '200' ? operationsResponse.body.data : null return { taskResponse, task, operationsResponse, operations, attempts: attempt } } if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000)) } return { taskResponse, task, operationsResponse, operations, attempts } } async function waitForEvidenceSnapshot(context, evidencePath, expectedTypes, attempts = 30) { let response = null for (let attempt = 1; attempt <= attempts; attempt += 1) { response = await context.request('GET', evidencePath) const evidences = response?.http === 200 && String(response?.body?.code) === '200' ? response.body.data?.evidences : null if (Array.isArray(evidences) && expectedTypes.every((type) => evidences.some((item) => item?.evidenceType === type && item?.status !== 'VOID'))) { return { response, attempts: attempt } } if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000)) } return { response, attempts } } export async function run(context) { const fixture = context.createdResources?.phase3Loading const tenantId = id(fixture?.tenantId); const inboundId = id(fixture?.inboundId) const orderId = id(fixture?.orderId) const taskId = id(fixture?.canonicalPdaTaskId); const loadingPlanId = id(fixture?.loadingPlanId) const containerBookingId = id(fixture?.containerBookingId) if (!tenantId || !orderId || !inboundId || fixture?.marker !== context.marker || !taskId || !loadingPlanId || !containerBookingId || fixture?.loadingPlanStatus !== 'LOADING' || fixture?.finalStatus !== 'AWAITING_REVIEW') { 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 (before?.summary?.processStatus !== 'LOADING' || !Number.isInteger(version)) return const base = `/api/platform/v1/inbounds/${inboundId}` const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}` const taskDetailPath = `${taskPath}?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}` const taskBeforeResponse = await context.request('GET', taskDetailPath) const taskBefore = context.apiSuccess(MODULE, '读取最终完成前 PDA 任务即时版本', taskBeforeResponse, '', OPTIONS) context.assert(MODULE, '最终完成前四元身份稳定且任务等待人工确认', taskBeforeResponse, id(taskBefore?.id) === taskId && id(taskBefore?.inboundId) === inboundId && id(taskBefore?.loadingPlanId) === loadingPlanId && id(taskBefore?.containerBookingId) === containerBookingId && taskBefore?.taskStatus === 'LOADING' && taskBefore?.workflowStatus === 'AWAITING_REVIEW', `taskId=${id(taskBefore?.id) || 'missing'}, workflow=${taskBefore?.workflowStatus || 'missing'}, version=${taskBefore?.version ?? 'missing'}`, OPTIONS) if (!Number.isInteger(Number(taskBefore?.version))) return const planPath = `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}` const planBeforeCompleteResponse = await context.request('GET', planPath) const planBeforeComplete = context.apiSuccess(MODULE, '读取结算前计划即时版本', planBeforeCompleteResponse, '', OPTIONS) const operationsBeforeResponse = await context.request('GET', `${taskPath}/operations`) const operationsBefore = context.apiSuccess(MODULE, '读取结算前 PDA 操作历史基线', operationsBeforeResponse, '', OPTIONS) const taskBeforeSignature = JSON.stringify({ version: Number(taskBefore?.version), taskStatus: taskBefore?.taskStatus, workflowStatus: taskBefore?.workflowStatus, items: (taskBefore?.items || []).map((item) => ({ id: id(item?.id), status: item?.itemStatus, picked: Number(item?.pickedPackageCount), version: Number(item?.version) })), }) const planBeforeSignature = JSON.stringify({ version: Number(planBeforeComplete?.version), status: planBeforeComplete?.status, items: (planBeforeComplete?.items || []).map((item) => ({ id: id(item?.id), version: Number(item?.version), packageCount: Number(item?.packageCount), picked: Number(item?.pickedPackageCount) })), }) const operationsBeforeSignature = JSON.stringify(operationsBefore || []) const completeAttempts = [1, 2].map((index) => ({ body: { expectedTaskVersion: Number(taskBefore.version), operationId: `${context.marker}-pda-final-complete-concurrent-${index}`.slice(0, 128), remark: context.marker, }, key: `${context.marker}-p3-complete-concurrent-key-${index}`.slice(0, 128), })) const concurrentCompleteResponses = await Promise.all(completeAttempts.map((attempt) => request(context, 'POST', `${taskPath}/complete`, attempt.body, { 'Idempotency-Key': attempt.key }))) const successfulCompleteIndexes = concurrentCompleteResponses.flatMap((response, index) => response.http === 200 && codeOf(response) === '200' ? [index] : []) const conflictedCompleteIndexes = concurrentCompleteResponses.flatMap((response, index) => response.http === 200 && codeOf(response) === 'A0409' ? [index] : []) const conflictedCompleteResponse = conflictedCompleteIndexes.length === 1 ? concurrentCompleteResponses[conflictedCompleteIndexes[0]] : null const winningCompleteIndex = successfulCompleteIndexes[0] ?? -1 const acceptedCompleteResponse = winningCompleteIndex >= 0 ? concurrentCompleteResponses[winningCompleteIndex] : concurrentCompleteResponses[0] const completeBody = winningCompleteIndex >= 0 ? completeAttempts[winningCompleteIndex].body : completeAttempts[0].body const completeKey = winningCompleteIndex >= 0 ? completeAttempts[winningCompleteIndex].key : completeAttempts[0].key const completion = winningCompleteIndex >= 0 ? await waitForCompletion(context, taskDetailPath, `${taskPath}/operations`) : { taskResponse: acceptedCompleteResponse, task: null, operationsResponse: null, operations: null, attempts: 0 } context.assert(MODULE, '异步完成命令收敛到终态', completion.taskResponse, completion.task?.taskStatus === 'COMPLETED', `taskStatus=${completion.task?.taskStatus || 'missing'}, workflow=${completion.task?.workflowStatus || 'missing'}, attempts=${completion.attempts}`, OPTIONS) const operationsAfterConcurrentResponse = completion.operationsResponse const operationsAfterConcurrent = completion.operations const baselineCompleteOperationIds = new Set((operationsBefore || []) .filter((item) => item?.actionType === 'COMPLETE_LOADING').map((item) => String(item?.operationId || ''))) const concurrentCompleteOperations = (operationsAfterConcurrent || []).filter((item) => item?.actionType === 'COMPLETE_LOADING' && completeAttempts.some((attempt) => attempt.body.operationId === item?.operationId) && !baselineCompleteOperationIds.has(String(item?.operationId || ''))) const concurrentCompleteVerified = successfulCompleteIndexes.length === 1 && conflictedCompleteIndexes.length === 1 && Boolean(conflictedCompleteResponse?.requestId) && Boolean(conflictedCompleteResponse?.traceId) && Array.isArray(operationsAfterConcurrent) && Array.isArray(operationsBefore) && operationsAfterConcurrent.length === operationsBefore.length + 1 && concurrentCompleteOperations.length === 1 && concurrentCompleteOperations[0]?.operationId === completeBody.operationId && id(concurrentCompleteOperations[0]?.operatorId) && Boolean(concurrentCompleteOperations[0]?.createdAt) && completion.task?.taskStatus === 'COMPLETED' && completion.task?.workflowStatus === 'COMPLETED' const completeResponse = completion.task?.taskStatus === 'COMPLETED' ? await request(context, 'POST', `${taskPath}/complete`, completeBody, { 'Idempotency-Key': completeKey }) : acceptedCompleteResponse context.assert(MODULE, '两个不同幂等键并发 complete 仅一成功且只写一条完成审计', completeResponse, concurrentCompleteVerified, `results=${concurrentCompleteResponses.map((response) => `${response.http}/${codeOf(response)}`).join(',')}, success=${successfulCompleteIndexes.length}, conflict=${conflictedCompleteIndexes.length}, completeAudits=${concurrentCompleteOperations.length}, winner=${completeBody.operationId}`, OPTIONS) if (completeResponse.http !== 200 || codeOf(completeResponse) !== '200' || completion.task?.taskStatus !== 'COMPLETED') { context.addStep(MODULE, '结算递增 inboundVersion 后 final complete 必须使用最新版本', completeResponse, false, `expected=success after settlement refresh; HTTP=${completeResponse.http}, code=${codeOf(completeResponse)}, message=${completeResponse.body?.message || 'missing'}, taskId=${taskId}, inboundId=${inboundId}, taskVersion=${taskBefore?.version ?? 'missing'}, inboundVersion=${version}, requestId=${completeResponse.requestId || 'missing'}, traceId=${completeResponse.traceId || 'missing'}`, OPTIONS) const taskAfterFailureResponse = await context.request('GET', taskDetailPath) const taskAfterFailure = context.apiSuccess(MODULE, 'final complete 失败后回查 PDA 任务', taskAfterFailureResponse, '', OPTIONS) const planAfterFailureResponse = await context.request('GET', planPath) const planAfterFailure = context.apiSuccess(MODULE, 'final complete 失败后回查计划', planAfterFailureResponse, '', OPTIONS) const inboundAfterFailureResponse = await context.request('GET', detailPath) const inboundAfterFailure = context.apiSuccess(MODULE, 'final complete 失败后回查入库单', inboundAfterFailureResponse, '', OPTIONS) const operationsAfterFailureResponse = await context.request('GET', `${taskPath}/operations`) const operationsAfterFailure = context.apiSuccess(MODULE, 'final complete 失败后回查操作历史', operationsAfterFailureResponse, '', OPTIONS) const taskAfterSignature = JSON.stringify({ version: Number(taskAfterFailure?.version), taskStatus: taskAfterFailure?.taskStatus, workflowStatus: taskAfterFailure?.workflowStatus, items: (taskAfterFailure?.items || []).map((item) => ({ id: id(item?.id), status: item?.itemStatus, picked: Number(item?.pickedPackageCount), version: Number(item?.version) })), }) const planAfterSignature = JSON.stringify({ version: Number(planAfterFailure?.version), status: planAfterFailure?.status, items: (planAfterFailure?.items || []).map((item) => ({ id: id(item?.id), version: Number(item?.version), packageCount: Number(item?.packageCount), picked: Number(item?.pickedPackageCount) })), }) context.assert(MODULE, '结算或版本冲突失败后 task/plan/inbound/operation 全部零写入', inboundAfterFailureResponse, taskAfterSignature === taskBeforeSignature && planAfterSignature === planBeforeSignature && inboundAfterFailure?.summary?.processStatus === 'LOADING' && Number(inboundAfterFailure?.summary?.version) === version && JSON.stringify(operationsAfterFailure || []) === operationsBeforeSignature, `taskUnchanged=${taskAfterSignature === taskBeforeSignature}, planUnchanged=${planAfterSignature === planBeforeSignature}, inboundStatus=${inboundAfterFailure?.summary?.processStatus || 'missing'}, inboundVersion=${inboundAfterFailure?.summary?.version ?? 'missing'}, operationsUnchanged=${JSON.stringify(operationsAfterFailure || []) === operationsBeforeSignature}`, OPTIONS) return } if (!concurrentCompleteVerified) return const completed = context.apiSuccess(MODULE, '订舱与五类证据等保留门禁齐备后完成装柜', completeResponse, '', OPTIONS) context.assert(MODULE, '统一硬门禁通过后 task 与 workflow 同事务完成且身份不漂移', completeResponse, completed?.taskStatus === 'COMPLETED' && completed?.workflowStatus === 'COMPLETED' && id(completed?.id) === taskId && id(completed?.loadingPlanId) === loadingPlanId && id(completed?.containerBookingId) === containerBookingId && id(completed?.inboundId) === inboundId, `taskStatus=${completed?.taskStatus || 'missing'}, workflow=${completed?.workflowStatus || 'missing'}`, OPTIONS) const replayResponse = await request(context, 'POST', `${taskPath}/complete`, completeBody, { 'Idempotency-Key': completeKey }) context.assert(MODULE, 'LOADED 完成请求 Idempotency-Key 重放保持终态', replayResponse, replayResponse.http === 200 && String(replayResponse.body?.code) === '200' && replayResponse.body?.data?.taskStatus === 'COMPLETED' && id(replayResponse.body?.data?.id) === taskId, `HTTP=${replayResponse.http}, code=${replayResponse.body?.code ?? 'missing'}, status=${replayResponse.body?.data?.taskStatus || '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 && 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) const slicesPath = `/api/platform/v1/orders/container-bookings/${encodeURIComponent(containerBookingId)}/fulfillment-slices?tenantId=${encodeURIComponent(tenantId)}` const slicesResponse = await context.request('GET', slicesPath) const slices = context.apiSuccess(MODULE, '结算后回查柜次实际履约分片', slicesResponse, '', OPTIONS) const loadedSlices = Array.isArray(slices) ? slices.filter((item) => id(item?.tenantId) === tenantId && id(item?.containerBookingId) === containerBookingId && id(item?.loadingPlanId) === loadingPlanId && item?.splitRole === 'LOADED') : [] const loadedSlice = loadedSlices.find((item) => id(item?.sourceOrderId) === orderId || id(item?.childOrderId) === orderId) || loadedSlices[0] const remainderSlice = Array.isArray(slices) && loadedSlice ? slices.find((item) => item?.splitRole === 'REMAINDER' && id(item?.sourceOrderId) === id(loadedSlice?.sourceOrderId)) : null const settlementMode = remainderSlice ? 'PARTIAL' : 'FULL' const loadedSourceIds = loadedSlices.map((item) => id(item?.sourceOrderId)).filter(Boolean) context.assert(MODULE, '结算后每个实装来源分片唯一且柜次身份稳定', slicesResponse, loadedSlices.length > 0 && Boolean(loadedSlice) && new Set(loadedSourceIds).size === loadedSlices.length && loadedSlices.every((item) => id(item?.tenantId) === tenantId && id(item?.containerBookingId) === containerBookingId && id(item?.loadingPlanId) === loadingPlanId && Number.isInteger(Number(item?.packageCount)) && Number(item.packageCount) > 0), `mode=${settlementMode}, loadedSlices=${loadedSlices.length}, sourceOrderId=${id(loadedSlice?.sourceOrderId) || 'missing'}, childOrderId=${id(loadedSlice?.childOrderId) || 'missing'}, packageCount=${loadedSlice?.packageCount ?? 'missing'}`, OPTIONS) const unifiedDetailPath = `/api/platform/v1/orders/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}` const unifiedDetailResponse = await context.request('GET', unifiedDetailPath) const unifiedDetail = context.apiSuccess(MODULE, 'LOADED 后回查同一统一订单详情', unifiedDetailResponse, '', OPTIONS) const orderNo = String(unifiedDetail?.summary?.orderNo || '').trim() context.assert(MODULE, '统一订单详情投影同一订单为 LOADED', unifiedDetailResponse, id(unifiedDetail?.summary?.orderId) === orderId && unifiedDetail?.summary?.mainStatus === 'LOADED' && Boolean(orderNo), `orderId=${id(unifiedDetail?.summary?.orderId) || 'missing'}, mainStatus=${unifiedDetail?.summary?.mainStatus || 'missing'}, orderNo=${orderNo || 'missing'}`, OPTIONS) if (id(unifiedDetail?.summary?.orderId) !== orderId || unifiedDetail?.summary?.mainStatus !== 'LOADED' || !orderNo) return const unifiedListPath = `/api/platform/v1/orders?page=1&size=100&tenantId=${encodeURIComponent(tenantId)}&orderNo=${encodeURIComponent(orderNo)}` const unifiedListResponse = await context.request('GET', unifiedListPath) const unifiedPage = context.apiSuccess(MODULE, 'LOADED 后回查同一统一订单列表', unifiedListResponse, '', OPTIONS) const unifiedRow = unifiedPage?.records?.find?.((row) => id(row?.orderId) === orderId) context.assert(MODULE, '统一订单列表投影同一订单为 LOADED', unifiedListResponse, Boolean(unifiedRow) && unifiedRow?.mainStatus === 'LOADED', `orderId=${orderId}, found=${Boolean(unifiedRow)}, mainStatus=${unifiedRow?.mainStatus || 'missing'}`, OPTIONS) const evidencePath = `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}` const immutableEvidenceResult = await waitForEvidenceSnapshot( context, evidencePath, fixture.evidenceTypes || []) const immutableEvidenceResponse = immutableEvidenceResult.response const immutableEvidence = context.apiSuccess(MODULE, '读取 LOADED 后不可变证据快照', immutableEvidenceResponse, '', OPTIONS) context.assert(MODULE, '统一硬门禁通过后五类有效证据仍完整', immutableEvidenceResponse, fixture.evidenceTypes?.every?.((type) => immutableEvidence?.evidences?.some?.((item) => item.evidenceType === type && item.status !== 'VOID')), `evidences=${immutableEvidence?.evidences?.length ?? 'missing'}, attempts=${immutableEvidenceResult.attempts}`, OPTIONS) const immutablePlanResponse = await context.request('GET', planPath) const immutablePlan = context.apiSuccess(MODULE, '读取 LOADED 后装柜计划快照', immutablePlanResponse, '', OPTIONS) const finalVersion = Number(finalData?.summary?.version) const historySignature = JSON.stringify((finalData?.statusHistory || []).map((item) => ({ action: item?.action || null, from: item?.fromStatus || null, to: item?.toStatus || null, operatorId: id(item?.operatorId), occurredAt: item?.occurredAt || item?.createdAt || null, }))) const immutableEvidenceSignature = evidenceSignature(immutableEvidence) const immutablePlanSignature = JSON.stringify({ version: Number(immutablePlan?.version), status: immutablePlan?.status || null, containerNo: immutablePlan?.containerNo || null, voyageNo: immutablePlan?.voyageNo || null, items: (immutablePlan?.items || []).map((item) => ({ id: id(item?.id), frozenAt: item?.frozenAt || null })), }) const completeNewKey = await request(context, 'POST', `${taskPath}/complete`, { expectedTaskVersion: Number(completed?.version), operationId: `${context.marker}-pda-final-complete-new-key`.slice(0, 128), remark: `${context.marker}-loaded-new-key` }, { 'Idempotency-Key': `${context.marker}-p3-complete-loaded-new-key`.slice(0, 128) }) context.assert(MODULE, 'LOADED 后使用新键重复 complete 返回冲突', completeNewKey, completeNewKey.http === 200 && codeOf(completeNewKey) === 'A0409' && Boolean(completeNewKey.requestId) && Boolean(completeNewKey.traceId), `HTTP=${completeNewKey.http}, code=${codeOf(completeNewKey)}, status=${completeNewKey.body?.data?.taskStatus || 'missing'}`, OPTIONS) const completeSameKeyDifferentBody = await request(context, 'POST', `${taskPath}/complete`, { ...completeBody, remark: `${context.marker}-different-complete-body` }, { 'Idempotency-Key': completeKey }) context.addStep(MODULE, 'LOADED complete 同键异参返回冲突', completeSameKeyDifferentBody, completeSameKeyDifferentBody.http === 200 && codeOf(completeSameKeyDifferentBody) === 'A0409' && Boolean(completeSameKeyDifferentBody.requestId) && Boolean(completeSameKeyDifferentBody.traceId), `HTTP=${completeSameKeyDifferentBody.http}, code=${codeOf(completeSameKeyDifferentBody)}, requestId=${completeSameKeyDifferentBody.requestId || 'missing'}, traceId=${completeSameKeyDifferentBody.traceId || 'missing'}`, OPTIONS) const postLoadedEvidence = await request(context, 'POST', evidencePath, { expectedVersion: 0, evidenceType: 'PHOTO_STEP_2', fileId: fixture.postLoadedFileId, fileName: 'client-must-not-override.png', sealNo: null, remark: context.marker, }, { 'Idempotency-Key': `${context.marker}-p3-evidence-after-loaded`.slice(0, 128) }) context.addStep(MODULE, 'LOADED 后新增装柜证据被拒绝', postLoadedEvidence, postLoadedEvidence.http === 200 && codeOf(postLoadedEvidence) === 'A0409' && Boolean(postLoadedEvidence.requestId) && Boolean(postLoadedEvidence.traceId), `HTTP=${postLoadedEvidence.http}, code=${codeOf(postLoadedEvidence)}, requestId=${postLoadedEvidence.requestId || 'missing'}, traceId=${postLoadedEvidence.traceId || 'missing'}`, OPTIONS) const postLoadedVoid = await request(context, 'POST', `${base}/loading/evidence/${encodeURIComponent(id(fixture.immutableEvidenceId))}/void?tenantId=${encodeURIComponent(tenantId)}`, { expectedVersion: Number(fixture.immutableEvidenceVersion), reason: `${context.marker}-void-after-loaded` }, { 'Idempotency-Key': `${context.marker}-p3-evidence-void-after-loaded`.slice(0, 128) }) context.addStep(MODULE, 'LOADED 后作废装柜证据被拒绝', postLoadedVoid, postLoadedVoid.http === 200 && codeOf(postLoadedVoid) === 'A0409' && Boolean(postLoadedVoid.requestId) && Boolean(postLoadedVoid.traceId), `HTTP=${postLoadedVoid.http}, code=${codeOf(postLoadedVoid)}, requestId=${postLoadedVoid.requestId || 'missing'}, traceId=${postLoadedVoid.traceId || 'missing'}`, OPTIONS) const immutableTransport = await request(context, 'PATCH', `/api/platform/v1/first-leg/loading-plans/${encodeURIComponent(id(fixture.loadingPlanId))}/transport?warehouseId=${encodeURIComponent(id(fixture.warehouseId))}`, { containerNo: `MUTATE-${context.marker}`.slice(0, 64), voyageNo: 'MUTATE', expectedVersion: Number(immutablePlan?.version), operationReason: `${context.marker}-after-loaded` }, { 'Idempotency-Key': `${context.marker}-p3-transport-after-loaded`.slice(0, 128) }) context.addStep(MODULE, 'LOADED 后运输信息修改被拒绝', immutableTransport, immutableTransport.http === 200 && codeOf(immutableTransport) === 'A0409' && Boolean(immutableTransport.requestId) && Boolean(immutableTransport.traceId), `HTTP=${immutableTransport.http}, code=${codeOf(immutableTransport)}, requestId=${immutableTransport.requestId || 'missing'}, traceId=${immutableTransport.traceId || 'missing'}`, OPTIONS) const completedPlanItem = immutablePlan?.items?.[0] const assignmentAfterCompleted = await request(context, 'POST', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}/assignments`, { inboundId: fixture.incrementalInboundId || inboundId, inboundDetailId: id(completedPlanItem?.inboundDetailId), packageCount: 1, expectedVersion: Number(immutablePlan?.version), operationId: `${context.marker}-assign-after-completed`.slice(0, 128), }) context.addStep(MODULE, 'COMPLETED 后新增计划明细被拒绝', assignmentAfterCompleted, assignmentAfterCompleted.http === 200 && codeOf(assignmentAfterCompleted) === 'A0409', `HTTP=${assignmentAfterCompleted.http}, code=${codeOf(assignmentAfterCompleted)}`, OPTIONS) const adjustAfterCompleted = await request(context, 'PATCH', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}/items/${encodeURIComponent(id(completedPlanItem?.id))}`, { packageCount: Number(completedPlanItem?.packageCount || 1), expectedPlanVersion: Number(immutablePlan?.version), expectedItemVersion: Number(completedPlanItem?.version), operationId: `${context.marker}-adjust-after-completed`.slice(0, 128), }) context.addStep(MODULE, 'COMPLETED 后调整计划明细被拒绝', adjustAfterCompleted, adjustAfterCompleted.http === 200 && codeOf(adjustAfterCompleted) === 'A0409', `HTTP=${adjustAfterCompleted.http}, code=${codeOf(adjustAfterCompleted)}`, OPTIONS) const deleteAfterCompleted = await request(context, 'DELETE', `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}/items/${encodeURIComponent(id(completedPlanItem?.id))}`, { expectedPlanVersion: Number(immutablePlan?.version), expectedItemVersion: Number(completedPlanItem?.version), operationId: `${context.marker}-delete-after-completed`.slice(0, 128), }) context.addStep(MODULE, 'COMPLETED 后删除计划明细被拒绝', deleteAfterCompleted, deleteAfterCompleted.http === 200 && codeOf(deleteAfterCompleted) === 'A0409', `HTTP=${deleteAfterCompleted.http}, code=${codeOf(deleteAfterCompleted)}`, OPTIONS) const afterBoundaryResponse = await context.request('GET', detailPath) const afterBoundary = context.apiSuccess(MODULE, '回查 LOADED 高风险边界后的入库快照', afterBoundaryResponse, '', OPTIONS) const afterEvidenceResponse = await context.request('GET', evidencePath) const afterEvidence = context.apiSuccess(MODULE, '回查 LOADED 高风险边界后的证据快照', afterEvidenceResponse, '', OPTIONS) const afterPlanResponse = await context.request('GET', planPath) const afterPlan = context.apiSuccess(MODULE, '回查 LOADED 高风险边界后的计划快照', afterPlanResponse, '', OPTIONS) const afterHistorySignature = JSON.stringify((afterBoundary?.statusHistory || []).map((item) => ({ action: item?.action || null, from: item?.fromStatus || null, to: item?.toStatus || null, operatorId: id(item?.operatorId), occurredAt: item?.occurredAt || item?.createdAt || null, }))) const afterPlanSignature = JSON.stringify({ version: Number(afterPlan?.version), status: afterPlan?.status || null, containerNo: afterPlan?.containerNo || null, voyageNo: afterPlan?.voyageNo || null, items: (afterPlan?.items || []).map((item) => ({ id: id(item?.id), frozenAt: item?.frozenAt || null })), }) context.assert(MODULE, 'LOADED 后拒绝与幂等请求保持状态、版本、证据、计划和审计零漂移', afterBoundaryResponse, afterBoundary?.summary?.processStatus === 'LOADED' && Number(afterBoundary?.summary?.version) === finalVersion && afterHistorySignature === historySignature && evidenceSignature(afterEvidence) === immutableEvidenceSignature && afterPlanSignature === immutablePlanSignature, `status=${afterBoundary?.summary?.processStatus || 'missing'}, version=${afterBoundary?.summary?.version ?? 'missing'}, histories=${afterBoundary?.statusHistory?.length ?? 'n/a'}, evidences=${afterEvidence?.evidences?.length ?? 'n/a'}, planVersion=${afterPlan?.version ?? 'missing'}`, OPTIONS) context.createdResources.phase3Loading = { ...buildFinalizeResource(fixture, finalData, completeResponse), settlementMode, settledOrderId: id(loadedSlice?.childOrderId) || orderId, } const producer = { marker: context.context?.phase4ProducerMatrixMarker || context.marker, eventType: 'FIRST_LEG_LOADING_PLAN_COMPLETED', eventVersion: 2, eventKey: `FIRSTLEG_LOADING_COMPLETED:${loadingPlanId}:${tenantId}:2`, resourceKey: `${context.marker}:${fixture.destinationType || 'UNKNOWN'}:${fixture.containerNo || containerBookingId}`, tenantId, aggregateType: 'LOADING_PLAN', aggregateId: loadingPlanId, loadingPlanId, containerBookingId, sourceOrderId: orderId, sourceInboundId: inboundId, cartonMark: fixture.cartonMark || null, cartonMarks: Array.isArray(fixture.cartonMarks) ? fixture.cartonMarks : [], skuCodes: Array.isArray(fixture.skuCodes) ? fixture.skuCodes : [], skuLines: Array.isArray(fixture.skuLines) ? fixture.skuLines : [], fulfillmentType: fixture.fulfillmentType || 'STANDARD', destinationType: fixture.destinationType || null, destinationWarehouseId: id(fixture.destinationWarehouseId), destinationWarehouseCode: fixture.destinationWarehouseCode || null, containerNo: fixture.containerNo || null, sourceStatus: 'LOADED', verification: 'PRODUCED_IN_COMPLETE_TRANSACTION_PENDING_BRIDGE_CONSUMER_HTTP', } context.createdResources.phase3LoadingCompletedProducer = producer context.createdResources.phase4InboundProducerFixtures = collectPhase4InboundProducerFixture( context.createdResources.phase4InboundProducerFixtures, producer) }