|
|
|
@@ -69,11 +69,6 @@ export async function run(context) {
|
|
|
|
|
if (before?.summary?.processStatus !== 'LOADING' || !Number.isInteger(version)) return
|
|
|
|
|
|
|
|
|
|
const base = `/api/platform/v1/inbounds/${inboundId}`
|
|
|
|
|
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA complete 联动拖车并原子推进 LOADED',
|
|
|
|
|
'BLOCKED_CONTRACT:用户已确认拖车缺失或未完成不再阻断;等待后端冻结 complete 响应中的拖车投影、LOADING→COMPLETED 历史动作、错柜隔离、重放与失败回滚字段后再执行写 HTTP。当前脚本主动停止,禁止沿用“拖车必须预先 COMPLETED”的旧门禁。',
|
|
|
|
|
'contract')
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}`
|
|
|
|
|
const taskBeforeResponse = await context.request('GET', taskPath)
|
|
|
|
|
const taskBefore = context.apiSuccess(MODULE, '读取最终完成前 PDA 任务即时版本', taskBeforeResponse, '', OPTIONS)
|
|
|
|
@@ -85,54 +80,57 @@ export async function run(context) {
|
|
|
|
|
`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 planBeforeRaceResponse = await context.request('GET', planPath)
|
|
|
|
|
const planBeforeRace = context.apiSuccess(MODULE, '读取最终完成并发前计划即时版本', planBeforeRaceResponse, '', OPTIONS)
|
|
|
|
|
const raceItem = planBeforeRace?.items?.find?.((item) => Number(item?.pickedPackageCount) > 0)
|
|
|
|
|
context.assert(MODULE, '最终完成并发使用同一已拣计划明细与即时版本', planBeforeRaceResponse,
|
|
|
|
|
Boolean(raceItem?.id) && Number.isInteger(Number(planBeforeRace?.version))
|
|
|
|
|
&& Number.isInteger(Number(raceItem?.version)),
|
|
|
|
|
`planVersion=${planBeforeRace?.version ?? 'missing'}, itemId=${id(raceItem?.id) || 'missing'}, itemVersion=${raceItem?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!raceItem?.id || !Number.isInteger(Number(planBeforeRace?.version))) return
|
|
|
|
|
let completeBody = {
|
|
|
|
|
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 = {
|
|
|
|
|
expectedTaskVersion: Number(taskBefore.version),
|
|
|
|
|
operationId: `${context.marker}-pda-final-complete`.slice(0, 128), remark: context.marker,
|
|
|
|
|
}
|
|
|
|
|
let completeKey = `${context.marker}-p3-complete-after-downstream`.slice(0, 128)
|
|
|
|
|
const concurrentAdjustBody = {
|
|
|
|
|
packageCount: Number(raceItem.packageCount), expectedPlanVersion: Number(planBeforeRace.version),
|
|
|
|
|
expectedItemVersion: Number(raceItem.version), operationId: `${context.marker}-race-adjust`.slice(0, 128),
|
|
|
|
|
}
|
|
|
|
|
const [initialCompleteResponse, concurrentAdjustResponse] = await Promise.all([
|
|
|
|
|
request(context, 'POST', `${taskPath}/complete`, completeBody, { 'Idempotency-Key': completeKey }),
|
|
|
|
|
request(context, 'PATCH',
|
|
|
|
|
`/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(loadingPlanId)}/items/${encodeURIComponent(id(raceItem.id))}`,
|
|
|
|
|
concurrentAdjustBody),
|
|
|
|
|
])
|
|
|
|
|
const completeWon = initialCompleteResponse.http === 200 && codeOf(initialCompleteResponse) === '200'
|
|
|
|
|
const adjustWon = concurrentAdjustResponse.http === 200 && codeOf(concurrentAdjustResponse) === '200'
|
|
|
|
|
context.assert(MODULE, 'Admin 增量与 PDA final complete 并发恰好一个事务成功', initialCompleteResponse,
|
|
|
|
|
completeWon !== adjustWon
|
|
|
|
|
&& (completeWon ? codeOf(concurrentAdjustResponse) === 'A0409' : codeOf(initialCompleteResponse) === 'A0409'),
|
|
|
|
|
`completeCode=${codeOf(initialCompleteResponse)}, adjustCode=${codeOf(concurrentAdjustResponse)}, completeRequestId=${initialCompleteResponse.requestId || 'missing'}, adjustRequestId=${concurrentAdjustResponse.requestId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
let completeResponse = initialCompleteResponse
|
|
|
|
|
if (adjustWon) {
|
|
|
|
|
const taskAfterRaceResponse = await context.request('GET', taskPath)
|
|
|
|
|
const taskAfterRace = context.apiSuccess(MODULE, '增量赢得并发后刷新 PDA 任务版本', taskAfterRaceResponse, '', OPTIONS)
|
|
|
|
|
const planAfterRaceResponse = await context.request('GET', planPath)
|
|
|
|
|
const planAfterRace = context.apiSuccess(MODULE, '增量赢得并发后回查唯一计划事实', planAfterRaceResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, 'final complete 失败方零写入且增量版本成为唯一事实', taskAfterRaceResponse,
|
|
|
|
|
taskAfterRace?.taskStatus === 'LOADING' && taskAfterRace?.workflowStatus === 'AWAITING_REVIEW'
|
|
|
|
|
&& Number(taskAfterRace?.version) > Number(taskBefore?.version)
|
|
|
|
|
&& Number(planAfterRace?.version) > Number(planBeforeRace?.version),
|
|
|
|
|
`taskVersion=${taskAfterRace?.version ?? 'missing'}, planVersion=${planAfterRace?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
completeBody = {
|
|
|
|
|
expectedTaskVersion: Number(taskAfterRace?.version),
|
|
|
|
|
operationId: `${context.marker}-pda-final-complete-retry`.slice(0, 128), remark: context.marker,
|
|
|
|
|
}
|
|
|
|
|
completeKey = `${context.marker}-p3-complete-after-race-refresh`.slice(0, 128)
|
|
|
|
|
completeResponse = await request(context, 'POST', `${taskPath}/complete`, completeBody,
|
|
|
|
|
{ 'Idempotency-Key': completeKey })
|
|
|
|
|
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', taskPath)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
const completed = context.apiSuccess(MODULE, '订舱拖车报关齐备后完成装柜', completeResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '统一硬门禁通过后 task 与 workflow 同事务完成且身份不漂移', completeResponse,
|
|
|
|
@@ -157,6 +155,30 @@ export async function run(context) {
|
|
|
|
|
&& 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'
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
@@ -293,5 +315,9 @@ export async function run(context) {
|
|
|
|
|
&& 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)
|
|
|
|
|
context.createdResources.phase3Loading = {
|
|
|
|
|
...buildFinalizeResource(fixture, finalData, completeResponse),
|
|
|
|
|
settlementMode,
|
|
|
|
|
settledOrderId: id(loadedSlice?.childOrderId) || orderId,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|