Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-loading-finalize.mjs
T

325 lines
25 KiB
JavaScript
Raw Normal View History

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) }
2026-08-12 12:35:29 +08:00
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)}`,
}
}
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)
2026-08-15 11:05:58 +08:00
const orderId = id(fixture?.orderId)
2026-08-17 15:41:42 +08:00
const taskId = id(fixture?.canonicalPdaTaskId); const loadingPlanId = id(fixture?.loadingPlanId)
const containerBookingId = id(fixture?.containerBookingId)
2026-08-15 11:05:58 +08:00
if (!tenantId || !orderId || !inboundId || fixture?.marker !== context.marker
2026-08-17 15:41:42 +08:00
|| !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)
2026-08-12 10:59:39 +08:00
if (before?.summary?.processStatus !== 'LOADING' || !Number.isInteger(version)) return
const base = `/api/platform/v1/inbounds/${inboundId}`
2026-08-17 15:41:42 +08:00
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)
2026-08-17 15:41:42 +08:00
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)}`
2026-08-17 16:07:20 +08:00
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 completeBody = {
2026-08-17 15:41:42 +08:00
expectedTaskVersion: Number(taskBefore.version),
operationId: `${context.marker}-pda-final-complete`.slice(0, 128), remark: context.marker,
}
2026-08-17 16:07:20 +08:00
const completeKey = `${context.marker}-p3-complete-after-downstream`.slice(0, 128)
const completeResponse = await request(context, 'POST', `${taskPath}/complete`, completeBody,
{ 'Idempotency-Key': completeKey })
if (completeResponse.http !== 200 || codeOf(completeResponse) !== '200') {
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)
2026-08-17 16:07:20 +08:00
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
2026-08-17 15:41:42 +08:00
}
2026-08-18 09:09:28 +08:00
const completed = context.apiSuccess(MODULE, '订舱与五类证据等保留门禁齐备后完成装柜', completeResponse, '', OPTIONS)
2026-08-17 15:41:42 +08:00
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',
2026-08-17 15:41:42 +08:00
`${taskPath}/complete`, completeBody, { 'Idempotency-Key': completeKey })
context.assert(MODULE, 'LOADED 完成请求 Idempotency-Key 重放保持终态', replayResponse,
replayResponse.http === 200 && String(replayResponse.body?.code) === '200'
2026-08-17 15:41:42 +08:00
&& 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,
2026-08-17 15:41:42 +08:00
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)
2026-08-17 16:07:20 +08:00
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'
context.assert(MODULE, '结算递增版本后实际完成分片唯一且柜次身份稳定', slicesResponse,
loadedSlices.length === 1 && Boolean(loadedSlice)
&& id(loadedSlice?.tenantId) === tenantId
&& id(loadedSlice?.containerBookingId) === containerBookingId
&& id(loadedSlice?.loadingPlanId) === loadingPlanId
&& Number.isInteger(Number(loadedSlice?.packageCount)) && Number(loadedSlice.packageCount) > 0,
`mode=${settlementMode}, loadedSlices=${loadedSlices.length}, sourceOrderId=${id(loadedSlice?.sourceOrderId) || 'missing'}, childOrderId=${id(loadedSlice?.childOrderId) || 'missing'}, packageCount=${loadedSlice?.packageCount ?? 'missing'}`, OPTIONS)
2026-08-15 11:05:58 +08:00
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)
2026-08-12 12:35:29 +08:00
const evidencePath = `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`
const immutableEvidenceResponse = await context.request('GET', evidencePath)
const immutableEvidence = context.apiSuccess(MODULE, '读取 LOADED 后不可变证据快照', immutableEvidenceResponse, '', OPTIONS)
2026-08-17 15:41:42 +08:00
context.assert(MODULE, '统一硬门禁通过后五类有效证据仍完整', immutableEvidenceResponse,
fixture.evidenceTypes?.every?.((type) => immutableEvidence?.evidences?.some?.((item) =>
item.evidenceType === type && item.status !== 'VOID')),
`evidences=${immutableEvidence?.evidences?.length ?? 'missing'}`, OPTIONS)
2026-08-12 12:35:29 +08:00
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',
2026-08-17 15:41:42 +08:00
`${taskPath}/complete`,
{ expectedTaskVersion: Number(completed?.version),
operationId: `${context.marker}-pda-final-complete-new-key`.slice(0, 128), remark: `${context.marker}-loaded-new-key` },
2026-08-12 12:35:29 +08:00
{ 'Idempotency-Key': `${context.marker}-p3-complete-loaded-new-key`.slice(0, 128) })
context.assert(MODULE, 'LOADED 后使用新键重复 complete 幂等保持终态', completeNewKey,
completeNewKey.http === 200 && codeOf(completeNewKey) === '200'
2026-08-17 15:41:42 +08:00
&& completeNewKey.body?.data?.taskStatus === 'COMPLETED',
`HTTP=${completeNewKey.http}, code=${codeOf(completeNewKey)}, status=${completeNewKey.body?.data?.taskStatus || 'missing'}`, OPTIONS)
2026-08-12 12:35:29 +08:00
const completeSameKeyDifferentBody = await request(context, 'POST',
2026-08-17 15:41:42 +08:00
`${taskPath}/complete`,
{ ...completeBody, remark: `${context.marker}-different-complete-body` },
{ 'Idempotency-Key': completeKey })
2026-08-12 12:35:29 +08:00
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)
2026-08-17 15:41:42 +08:00
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)
2026-08-12 12:35:29 +08:00
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)
2026-08-17 16:07:20 +08:00
context.createdResources.phase3Loading = {
...buildFinalizeResource(fixture, finalData, completeResponse),
settlementMode,
settledOrderId: id(loadedSlice?.childOrderId) || orderId,
}
}