This commit is contained in:
2026-09-20 19:57:47 +08:00
parent db07111e63
commit bd03ed5671
17 changed files with 210 additions and 97 deletions
@@ -320,6 +320,49 @@ async function convertContainerPlans(context, producer, details) {
return plans
}
export async function verifyReceivedOrderHold(context) {
const owned = context.createdResources.phase4TailOrderControls
if (owned?.marker !== context.marker || !owned.representativeOrderId) throw new Error('缺少本轮暂扣目标订单')
const producer = targetProducer(context)
const loaded = await detail(context, { id: owned.representativeOrderId, customerId: producer?.tenantId }, '入库后读取本轮暂扣目标')
const representative = loaded.data?.order
if (!success(loaded.response) || id(representative?.id) !== id(owned.representativeOrderId)
|| id(representative?.customerId) !== id(producer?.tenantId)
|| representative?.currentStatus !== 'INBOUND_COMPLETED') throw new Error('暂扣正向验收必须使用本轮已完成入库订单')
const orderPath = query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/hold`, {
customerId: id(representative.customerId),
})
const holdKey = `${context.marker}-tail-hold`.slice(0, 128)
const holdBody = { held: true, chargeable: true, reason: `${context.marker} 暂扣验收`, expectedVersion: Number(representative.version) }
const held = await api(context, 'PUT', '暂扣本轮单票尾程订单', orderPath, holdBody, { 'Idempotency-Key': holdKey })
context.assert(MODULE, '暂扣只推进单票版本并保留计费原因', held.response,
held.data?.held === true && held.data?.chargeable === true
&& id(held.data?.orderId) === id(representative.id) && Number(held.data?.version) === Number(representative.version) + 1,
trace(held.response), OPTIONS)
if (!success(held.response)) return
const heldReplay = await api(context, 'PUT', '暂扣命令同键同载荷重放', orderPath, holdBody, { 'Idempotency-Key': holdKey })
context.assert(MODULE, '暂扣重放版本零漂移', heldReplay.response,
JSON.stringify(heldReplay.data) === JSON.stringify(held.data), trace(heldReplay.response), OPTIONS)
const heldConflict = await context.requestOverseas('PUT', orderPath,
{ ...holdBody, reason: `${holdBody.reason}-DIFFERENT` }, { 'Idempotency-Key': holdKey }, context.overseasToken)
context.addStep(MODULE, '暂扣同键异载荷返回A0409', heldConflict, conflict(heldConflict), trace(heldConflict), OPTIONS)
const blockedPlanBody = manualPlanBody(representative, held.data.version, context.marker)
const blockedPlan = await context.requestOverseas('POST', `${PLANS}/manual`, blockedPlanBody,
{ 'Idempotency-Key': `${context.marker}-held-plan`.slice(0, 128) }, context.overseasToken)
context.addStep(MODULE, '暂扣尾程订单不能生成人工计划', blockedPlan, conflict(blockedPlan), trace(blockedPlan), OPTIONS)
const releaseKey = `${context.marker}-tail-release`.slice(0, 128)
const releaseBody = { held: false, chargeable: false, reason: `${context.marker} 解除暂扣`, expectedVersion: Number(held.data.version) }
const released = await api(context, 'PUT', '解除本轮单票尾程暂扣', orderPath, releaseBody, { 'Idempotency-Key': releaseKey })
context.assert(MODULE, '解除暂扣清空计费与原因并推进版本', released.response,
released.data?.held === false && released.data?.chargeable === false && released.data?.reason == null
&& Number(released.data?.version) === Number(held.data.version) + 1, trace(released.response), OPTIONS)
if (!success(released.response)) return
owned.holdVerifiedAfterReceipt = true
}
export async function run(context) {
context.createdResources.phase4TailOrderControls = {
marker: context.marker, containerNo: null, orderIds: [], planNos: [], representativeOrderId: null,
@@ -355,48 +398,28 @@ export async function run(context) {
refreshedDetails.push(refreshed.data.order)
}
const representative = refreshedDetails[0]
const orderPath = query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/hold`, {
customerId: id(representative.customerId),
})
const holdKey = `${context.marker}-tail-hold`.slice(0, 128)
const holdBody = { held: true, chargeable: true, reason: `${context.marker} 暂扣验收`, expectedVersion: Number(representative.version) }
const held = await api(context, 'PUT', '暂扣本轮单票尾程订单', orderPath, holdBody, { 'Idempotency-Key': holdKey })
context.assert(MODULE, '暂扣只推进单票版本并保留计费原因', held.response,
held.data?.held === true && held.data?.chargeable === true
&& id(held.data?.orderId) === id(representative.id) && Number(held.data?.version) === Number(representative.version) + 1,
trace(held.response), OPTIONS)
if (!success(held.response)) return
const heldReplay = await api(context, 'PUT', '暂扣命令同键同载荷重放', orderPath, holdBody, { 'Idempotency-Key': holdKey })
context.assert(MODULE, '暂扣重放版本零漂移', heldReplay.response,
JSON.stringify(heldReplay.data) === JSON.stringify(held.data), trace(heldReplay.response), OPTIONS)
const heldConflict = await context.requestOverseas('PUT', orderPath,
{ ...holdBody, reason: `${holdBody.reason}-DIFFERENT` }, { 'Idempotency-Key': holdKey }, context.overseasToken)
context.addStep(MODULE, '暂扣同键异载荷返回A0409', heldConflict, conflict(heldConflict), trace(heldConflict), OPTIONS)
const blockedPlanBody = manualPlanBody(representative, held.data.version, context.marker)
const blockedPlan = await context.requestOverseas('POST', `${PLANS}/manual`, blockedPlanBody,
{ 'Idempotency-Key': `${context.marker}-held-plan`.slice(0, 128) }, context.overseasToken)
context.addStep(MODULE, '暂扣尾程订单不能生成人工计划', blockedPlan, conflict(blockedPlan), trace(blockedPlan), OPTIONS)
const releaseKey = `${context.marker}-tail-release`.slice(0, 128)
const releaseBody = { held: false, chargeable: false, reason: `${context.marker} 解除暂扣`, expectedVersion: Number(held.data.version) }
const released = await api(context, 'PUT', '解除本轮单票尾程暂扣', orderPath, releaseBody, { 'Idempotency-Key': releaseKey })
context.assert(MODULE, '解除暂扣清空计费与原因并推进版本', released.response,
released.data?.held === false && released.data?.chargeable === false && released.data?.reason == null
&& Number(released.data?.version) === Number(held.data.version) + 1, trace(released.response), OPTIONS)
if (!success(released.response)) return
const rejectedHold = await context.requestOverseas('PUT', query(
`${ORDERS}/${encodeURIComponent(id(representative.id))}/hold`, { customerId: id(representative.customerId) }),
{ held: true, chargeable: true, reason: `${context.marker} 入库前禁止暂扣`, expectedVersion: Number(representative.version) },
{ 'Idempotency-Key': `${context.marker}-pre-receipt-hold` })
context.addStep(MODULE, '入库前暂扣必须拒绝', rejectedHold, conflict(rejectedHold), trace(rejectedHold), OPTIONS)
const afterHold = await detail(context, representative, '入库前暂扣拒绝后回查订单')
const unchanged = success(afterHold.response) && JSON.stringify(afterHold.data?.order) === JSON.stringify(representative)
context.assert(MODULE, '拒绝暂扣后订单事实零漂移', afterHold.response, unchanged, trace(afterHold.response), OPTIONS)
if (!conflict(rejectedHold) || !unchanged) return
// The positive hold/release lifecycle runs only after PDA receiving.
const nodesPath = query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/manual-nodes`, {
customerId: id(representative.customerId),
})
const nodesKey = `${context.marker}-tail-manual-nodes`.slice(0, 128)
const nodesBody = { expectedVersion: Number(released.data.version), nodes: completeManualNodes(context.marker) }
const nodesBody = { expectedVersion: Number(representative.version), nodes: completeManualNodes(context.marker) }
const nodes = await api(context, 'PUT', '保存尾程八个人工节点快照', nodesPath, nodesBody,
{ 'Idempotency-Key': nodesKey })
context.assert(MODULE, '人工节点永久切换MANUAL且完整返回八行', nodes.response,
nodes.data?.manualOverrideEnabled === true && nodes.data?.nodes?.length === 8
&& nodes.data.nodes.every((node) => node?.source === 'MANUAL')
&& Number(nodes.data?.version) === Number(released.data.version) + 1,
&& Number(nodes.data?.version) === Number(representative.version) + 1,
`nodes=${nodes.data?.nodes?.length ?? 'missing'}; ${trace(nodes.response)}`, OPTIONS)
if (!success(nodes.response)) return
const nodesReplay = await api(context, 'PUT', '人工节点同键同载荷重放', nodesPath, nodesBody,