优化
This commit is contained in:
@@ -15,6 +15,36 @@ const success = response => response?.http === 200 && String(response?.body?.cod
|
||||
const query = (path, values) => `${path}?${new URLSearchParams(values)}`
|
||||
|
||||
export function f40LocalDateTime(value) { return value.toISOString().slice(0,19) }
|
||||
export function preserveReceivingEvidence(proof, scope) {
|
||||
proof.receivingCompletionEvidence = scope.createdResources?.phase4OverseasPda?.receivingCompletionEvidence || {}
|
||||
}
|
||||
export function validFreshReceiptSave(before, after) {
|
||||
const previousVersion = Number(before?.documents?.version)
|
||||
const nextVersion = Number(after?.documents?.version)
|
||||
if (!Number.isSafeInteger(previousVersion) || nextVersion !== previousVersion + 1
|
||||
|| Number(before?.delivery?.order?.version) !== previousVersion
|
||||
|| Number(after?.delivery?.order?.version) !== nextVersion
|
||||
|| before?.delivery?.order?.status !== 'DELIVERED' || !before.delivery.order.signedAt) return false
|
||||
const same = (left,right) => JSON.stringify(left) === JSON.stringify(right)
|
||||
if (!same(before.documents,{...after.documents,version:before.documents.version})) return false
|
||||
const oldOperations = before.delivery.operations, newOperations = after.delivery.operations
|
||||
if (!Array.isArray(oldOperations) || !Array.isArray(newOperations)
|
||||
|| newOperations.length !== oldOperations.length + 1) return false
|
||||
const oldIds = new Set(oldOperations.map(row => id(row.id)))
|
||||
const added = newOperations.filter(row => !oldIds.has(id(row.id)))
|
||||
if (added.length !== 1 || !id(added[0].id) || added[0].operationNode !== 'SAVE_DOCUMENTS'
|
||||
|| added[0].result !== `保存派送资料:${after.documents.uploadedCategoryCount}/3类`
|
||||
|| !added[0].operatedAt || !added[0].operatorName
|
||||
|| !same(oldOperations,newOperations.filter(row => oldIds.has(id(row.id))))) return false
|
||||
if (!same(before.delivery,{...after.delivery,order:{...after.delivery.order,version:before.delivery.order.version},
|
||||
operations:oldOperations})) return false
|
||||
if (!Array.isArray(before.revisions) || !Array.isArray(after.revisions)
|
||||
|| after.revisions.length !== before.revisions.length + 1) return false
|
||||
const addedRevisions = after.revisions.filter(row => Number(row.version) === nextVersion)
|
||||
return addedRevisions.length === 1 && Boolean(addedRevisions[0].savedAt && addedRevisions[0].operatorId)
|
||||
&& same(before.revisions,after.revisions.filter(row => Number(row.version) !== nextVersion))
|
||||
&& same(addedRevisions[0].snapshot,after.documents)
|
||||
}
|
||||
export function resolveF40MerchantCredentials(context) {
|
||||
const fixture = context.context?.merchantQualificationFixture
|
||||
if (!fixture) {
|
||||
@@ -103,6 +133,56 @@ export function exactCompletionOutboxProof(evidence, customerId, rootOrderId, so
|
||||
&& Number(row.count) === 1 && Number(row.publishedCount) === 1 && Number(row.pendingCount) === 0
|
||||
&& Number(row.failedCount) === 0)
|
||||
}
|
||||
export function validUnloadedRemainder(tree, child, inbound, customerId, rootOrderId, childOrderId) {
|
||||
const rows = Array.isArray(tree?.children) ? tree.children : []
|
||||
const loaded = rows.filter(row => row.splitRole === 'LOADED')
|
||||
const remainder = rows.filter(row => row.splitRole === 'REMAINDER')
|
||||
return Boolean(id(rootOrderId) && id(childOrderId) && id(rootOrderId) !== id(childOrderId))
|
||||
&& id(tree?.parentOrderId) === id(rootOrderId) && Number(tree.originalPackageCount) === 2
|
||||
&& Number(tree.cumulativeLoadedPackageCount) === 1 && Number(tree.currentRemainderPackageCount) === 1
|
||||
&& rows.length === 2 && loaded.length === 1 && remainder.length === 1
|
||||
&& id(loaded[0].orderId) === id(rootOrderId) && id(loaded[0].sourceOrderId) === id(rootOrderId)
|
||||
&& Number(loaded[0].packageCount) === 1
|
||||
&& id(remainder[0].orderId) === id(childOrderId) && id(remainder[0].sourceOrderId) === id(rootOrderId)
|
||||
&& Number(remainder[0].packageCount) === 1
|
||||
&& id(remainder[0].loadingPlanId) === id(loaded[0].loadingPlanId)
|
||||
&& id(remainder[0].containerBookingId) === id(loaded[0].containerBookingId)
|
||||
&& Boolean(id(loaded[0].loadingPlanId) && id(loaded[0].containerBookingId))
|
||||
&& child?.role === 'CHILD' && child.status === 'APPROVED'
|
||||
&& child.orderId === id(childOrderId) && child.rootOrderId === id(rootOrderId)
|
||||
&& child.parentOrderId === id(rootOrderId) && id(child.rawOrder?.tenantId) === id(customerId)
|
||||
&& completionCounts(child).own === 0 && completionCounts(child).final === 0
|
||||
&& Boolean(id(inbound?.id) && inbound?.inboundCode)
|
||||
&& id(inbound.orderId) === id(childOrderId) && id(inbound.tenantId) === id(customerId)
|
||||
&& inbound.inboundCode === child.inboundCode && inbound.orderNo === child.orderNo
|
||||
&& inbound.processStatus === 'WAIT_SPLIT' && inbound.orderStatusSnapshot === 'APPROVED'
|
||||
&& Number(inbound.totalPackageCount) === 1 && !inbound.containerNo && !inbound.cabinetNo
|
||||
}
|
||||
export function validRemainderCancellationGate(snapshot) {
|
||||
const root = completionCounts(snapshot?.root), child = completionCounts(snapshot?.child)
|
||||
return snapshot?.root?.status === 'COMPLETED' && snapshot?.child?.status === 'CANCELLED'
|
||||
&& root.own === 1 && root.final === 1 && child.own === 0 && child.final === 0
|
||||
&& snapshot.child.histories.filter(row => row.action === 'CANCELLED').length === 1
|
||||
&& snapshot.child.rawOrder?.completedAt == null
|
||||
&& snapshot?.rootInbound?.processStatus === 'DELIVERED'
|
||||
&& snapshot?.rootInbound?.orderStatusSnapshot === 'COMPLETED'
|
||||
}
|
||||
export function exactCancelledRemainderOutboxProof(evidence, customerId, rootOrderId, childOrderId) {
|
||||
const rows = Array.isArray(evidence?.orders) ? evidence.orders : []
|
||||
const root = rows.filter(row => id(row.orderId) === id(rootOrderId))
|
||||
const child = rows.filter(row => id(row.orderId) === id(childOrderId))
|
||||
return id(rootOrderId) !== id(childOrderId) && rows.length === 2 && root.length === 1 && child.length === 1
|
||||
&& exactCompletionOutboxProof({...evidence,orders:root},customerId,rootOrderId,[rootOrderId])
|
||||
&& id(child[0].customerId) === id(customerId) && id(child[0].rootOrderId) === id(rootOrderId)
|
||||
&& child[0].eventType === 'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED'
|
||||
&& ['count','publishedCount','pendingCount','failedCount'].every(field => child[0][field] != null && Number(child[0][field]) === 0)
|
||||
}
|
||||
function outboxCounts(evidence) {
|
||||
return JSON.stringify((evidence?.orders || []).map(row => ({
|
||||
orderId:id(row.orderId),customerId:id(row.customerId),rootOrderId:id(row.rootOrderId),eventType:row.eventType,
|
||||
count:Number(row.count),publishedCount:Number(row.publishedCount),pendingCount:Number(row.pendingCount),failedCount:Number(row.failedCount),
|
||||
})).sort((left,right) => left.orderId.localeCompare(right.orderId)))
|
||||
}
|
||||
function check(context, label, condition, evidence, response = null) {
|
||||
context.assert(MODULE, label, response, condition, evidence, OPTIONS)
|
||||
if (!condition) throw new Error(`${MODULE}: ${label}: ${evidence}`)
|
||||
@@ -138,6 +218,74 @@ async function waitState(read, predicate, attempts = 90) {
|
||||
}
|
||||
return last
|
||||
}
|
||||
export async function verifyRootProjections(context, customerId, rootOrderId, merchantToken, snapshot) {
|
||||
const completed = snapshot.root.status === 'COMPLETED'
|
||||
const ownSigned = completionCounts(snapshot.root).own === 1
|
||||
const valid = value => id(value?.summary?.orderId) === id(rootOrderId)
|
||||
&& (completed ? value.summary.mainStatus === 'COMPLETED'
|
||||
: ownSigned ? value.summary.mainStatus === 'DELIVERED'
|
||||
: Boolean(value.summary.mainStatus) && value.summary.mainStatus !== 'COMPLETED')
|
||||
const platform = await waitState(() => api(context,'GET','F40 平台统一根订单状态',
|
||||
query(`/api/platform/v1/orders/${rootOrderId}`,{tenantId:customerId})),valid)
|
||||
const customer = await api(context,'GET','F40 本企业客户统一根订单状态',
|
||||
`/api/merchant/v1/orders/${rootOrderId}`,undefined,merchantToken)
|
||||
check(context,'F40 平台客户统一状态与原始根完成门禁一致',valid(platform) && valid(customer)
|
||||
&& customer.summary.mainStatus === platform.summary.mainStatus,'exact-root-platform-customer-status')
|
||||
return {platform:platform.summary,customer:customer.summary}
|
||||
}
|
||||
export async function cancelUnloadedRemainder(context, proof, customerId, rootOrderId, childOrderId,
|
||||
rootInboundId, childInboundId) {
|
||||
proof.currentStage = 'REMAINDER_CANCEL_PRECONDITION'
|
||||
const before = await state(context,customerId,rootOrderId,childOrderId,rootInboundId)
|
||||
const treePath = query(`/api/platform/v1/orders/${rootOrderId}/fulfillment-tree`,{tenantId:customerId})
|
||||
const tree = await api(context,'GET','取消前重新核对唯一未消费余量履约树',treePath)
|
||||
const childInbound = await api(context,'GET','取消前回查未装余量入仓来源',
|
||||
query(`/api/platform/inbounds/${childInboundId}`,{tenantId:customerId}))
|
||||
check(context,'F40 仅取消真实签收根下未装 APPROVED 一箱余量', before.root.status === 'APPROVED'
|
||||
&& validSignatureGate(before,'MASTER')
|
||||
&& validUnloadedRemainder(tree,before.child,childInbound?.summary,customerId,rootOrderId,childOrderId),
|
||||
'raw-approved-root/child; one-loaded-root/one-unloaded-remainder')
|
||||
proof.snapshots.push({phase:'BEFORE_REMAINDER_CANCELLATION',...before,tree,childInbound:childInbound.summary})
|
||||
const cancelPath = query(`/api/platform/v1/orders/first-leg/${childOrderId}/cancel`,{tenantId:customerId})
|
||||
const cancelBody = {reason:`${context.context.f40BaseMarker} 未装柜余量取消验收`}
|
||||
proof.currentStage = 'REMAINDER_CANCELLATION'
|
||||
await api(context,'POST','正式取消未装柜 APPROVED 余量订单',cancelPath,cancelBody)
|
||||
const after = await waitState(() => state(context,customerId,rootOrderId,childOrderId,rootInboundId),
|
||||
validRemainderCancellationGate)
|
||||
check(context,'F40 最后有效未装余量取消后根仅完成一次且子未签收',validRemainderCancellationGate(after),
|
||||
'root-own=1/root-final=1/child-cancel=1/child-signature=0')
|
||||
const afterTree = await api(context,'GET','取消后保留真实分片数量台账',treePath)
|
||||
check(context,'F40 取消没有伪造第二柜实装或改写分片数量',
|
||||
JSON.stringify(tree.children) === JSON.stringify(afterTree.children)
|
||||
&& Number(afterTree.cumulativeLoadedPackageCount) === 1 && Number(afterTree.currentRemainderPackageCount) === 1,
|
||||
'ledger-retains-one-physical-box-and-one-cancelled-remainder')
|
||||
proof.snapshots.push({phase:'ROOT_SIGNED_REMAINDER_CANCELLED',...after,tree:afterTree})
|
||||
proof.currentStage = 'REMAINDER_CANCEL_REPLAY'
|
||||
const reader = context.context?.f40ReadOnlyOutboxEvidence
|
||||
const request = {customerId,rootOrderId,sourceOrderIds:[rootOrderId,childOrderId],
|
||||
eventType:'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED'}
|
||||
const beforeOutbox = typeof reader === 'function' ? await waitState(() => reader(request),
|
||||
value => exactCancelledRemainderOutboxProof(value,customerId,rootOrderId,childOrderId),30) : null
|
||||
if (beforeOutbox) check(context,'F40 取消后唯一根完成Outbox且取消子完成Outbox为零',
|
||||
exactCancelledRemainderOutboxProof(beforeOutbox,customerId,rootOrderId,childOrderId),'exact-root-one/cancelled-child-zero')
|
||||
const replay = await context.request('POST',cancelPath,cancelBody)
|
||||
check(context,'F40 取消重放按当前非幂等状态接口返回 A0409',
|
||||
replay?.http === 200 && String(replay?.body?.code) === 'A0409','cancel-replay-conflict',replay)
|
||||
const stable = await state(context,customerId,rootOrderId,childOrderId,rootInboundId)
|
||||
const replayTree = await api(context,'GET','取消重放后回查原履约树',treePath)
|
||||
check(context,'F40 取消重放订单版本历史根摘要及履约台账零变化',
|
||||
JSON.stringify(after) === JSON.stringify(stable) && JSON.stringify(afterTree) === JSON.stringify(replayTree),
|
||||
'exact-root/child/history/summary/tree')
|
||||
if (beforeOutbox) {
|
||||
const afterOutbox = await reader(request)
|
||||
check(context,'F40 取消重放完成Outbox计数发布状态零变化',
|
||||
exactCancelledRemainderOutboxProof(afterOutbox,customerId,rootOrderId,childOrderId)
|
||||
&& outboxCounts(beforeOutbox) === outboxCounts(afterOutbox),'exact-root-child-completion-counts')
|
||||
proof.cancelReplayOutbox = {before:beforeOutbox,after:afterOutbox}
|
||||
}
|
||||
proof.snapshots.push({phase:'AFTER_CANCELLATION_REPLAY',...stable,tree:replayTree})
|
||||
return stable
|
||||
}
|
||||
async function domesticLogin(context) {
|
||||
if (context.pdaToken) return
|
||||
const username = context.options.pdaUsername, password = context.options.pdaPassword
|
||||
@@ -242,7 +390,7 @@ async function sendFragment(context, producer, picker, merchantToken, index) {
|
||||
check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`)
|
||||
check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`)
|
||||
return { producer, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: deliveries[0].deliveryNo,
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt }
|
||||
receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed }
|
||||
}
|
||||
|
||||
/** Explicit TEST orchestrator, called by the parent acceptance context; never synthesizes an event. */
|
||||
@@ -266,11 +414,14 @@ async function runRootFulfillment(context) {
|
||||
cleanup: 'Fresh completed root/child/cabinet/PDA/receipt audit resources remain as acceptance evidence',
|
||||
}
|
||||
const signatureOrder = context.context?.f40SignatureOrder || 'ROOT_FIRST'
|
||||
check(context,'F40 签收次序为显式正式来源顺序', ['ROOT_FIRST','CHILD_FIRST'].includes(signatureOrder), signatureOrder)
|
||||
check(context,'F40 签收次序为显式正式来源顺序',
|
||||
['ROOT_FIRST','CHILD_FIRST','ROOT_SIGNED_REMAINDER_CANCELLED'].includes(signatureOrder), signatureOrder)
|
||||
const cancelRemainder = signatureOrder === 'ROOT_SIGNED_REMAINDER_CANCELLED'
|
||||
proof.signatureOrder = signatureOrder
|
||||
const scope = Object.create(context)
|
||||
scope.marker = `${context.marker}-F40`
|
||||
scope.context = { ...context.context, orderDestinationType: 'FBA', orderFulfillmentType: 'STANDARD',
|
||||
orderStopAtApproved:cancelRemainder,
|
||||
orderProfile: { taxMode: 'BUY_DECLARATION', fulfillmentType: 'STANDARD' },
|
||||
waitSplitVariant: { skuCount: 1, packageCountPerSku: 2, packageCounts: [2] },
|
||||
phase4OverseasPdaOperations: { completedFixtures: [] },
|
||||
@@ -292,6 +443,14 @@ async function runRootFulfillment(context) {
|
||||
const orderFixture = { ...scope.createdResources.orders }
|
||||
check(scope,'F40 正式订单创建客户与本轮登录客户一致', id(orderFixture.tenantId) === expectedCustomerId,
|
||||
'actual-order-customer-matches-profile')
|
||||
if (cancelRemainder) {
|
||||
const approvedRoot = fulfillmentHistorySnapshot(await api(scope,'GET','取消专项回查新根真实审核状态',
|
||||
query(`/api/platform/v1/orders/first-leg/${orderFixture.orderId}`,{tenantId:expectedCustomerId})))
|
||||
check(scope,'F40 取消专项新根必须为原始 APPROVED 而非快照推断',approvedRoot.role === 'MASTER'
|
||||
&& approvedRoot.status === 'APPROVED' && approvedRoot.orderId === id(orderFixture.orderId)
|
||||
&& id(approvedRoot.rawOrder?.tenantId) === expectedCustomerId,'raw-approved-fresh-root')
|
||||
proof.snapshots.push({phase:'APPROVED_NEW_ROOT',root:approvedRoot})
|
||||
}
|
||||
proof.currentStage = 'DOMESTIC_MEASUREMENT'
|
||||
await stage(scope, 'inbound-measurement')
|
||||
const rootOrderId = id(orderFixture.orderId), customerId = id(orderFixture.tenantId)
|
||||
@@ -323,15 +482,26 @@ async function runRootFulfillment(context) {
|
||||
check(scope, 'F40 子单有唯一独立真实入仓来源', childRows.length === 1, `sources=${childRows.length}`)
|
||||
const childInbound = await api(scope, 'GET', '读取子单可装柜来源',
|
||||
query(`/api/platform/inbounds/${childRows[0].id}`, { tenantId: customerId }))
|
||||
scope.marker = `${scope.context.f40BaseMarker}-CHILD`
|
||||
proof.currentStage = 'CHILD_LOAD_AND_SHIP'
|
||||
const second = await fragment(scope, childInbound, rootOrderId, 1)
|
||||
proof.fragments.push(second)
|
||||
let second = null
|
||||
if (cancelRemainder) {
|
||||
check(scope,'F40 取消专项真实余量未装且原始 APPROVED',
|
||||
validUnloadedRemainder(tree,fulfillmentHistorySnapshot(childDetail),childInbound?.summary,
|
||||
customerId,rootOrderId,childOrderId),'one-approved-unloaded-remainder')
|
||||
proof.unloadedRemainder = {childOrderId,childInboundId:id(childRows[0].id),tree,summary:childInbound.summary}
|
||||
} else {
|
||||
scope.marker = `${scope.context.f40BaseMarker}-CHILD`
|
||||
proof.currentStage = 'CHILD_LOAD_AND_SHIP'
|
||||
second = await fragment(scope, childInbound, rootOrderId, 1)
|
||||
proof.fragments.push(second)
|
||||
}
|
||||
proof.currentStage = 'FULFILLMENT_TREE'
|
||||
const finalTree = await api(scope, 'GET', '两柜装完后核对数量守恒履约树',
|
||||
query(`/api/platform/v1/orders/${rootOrderId}/fulfillment-tree`, { tenantId: customerId }))
|
||||
check(scope, 'F40 真实同根两个一箱来源和柜次数量守恒', validTwoFragmentTree(finalTree, rootOrderId, childOrderId)
|
||||
&& first.containerBookingId !== second.containerBookingId, `root=${rootOrderId}, child=${childOrderId}`)
|
||||
check(scope, 'F40 真实同根实装及余量数量守恒', cancelRemainder
|
||||
? validUnloadedRemainder(finalTree,fulfillmentHistorySnapshot(childDetail),childInbound?.summary,
|
||||
customerId,rootOrderId,childOrderId)
|
||||
: validTwoFragmentTree(finalTree, rootOrderId, childOrderId)
|
||||
&& first.containerBookingId !== second.containerBookingId, `root=${rootOrderId}, child=${childOrderId}`)
|
||||
const before = await state(scope, customerId, rootOrderId, childOrderId, first.inboundId)
|
||||
check(scope, 'F40 尚未签收两片不得出现完成事实', before.root.role === 'MASTER'
|
||||
&& id(before.root.rootOrderId) === rootOrderId && before.root.status !== 'COMPLETED'
|
||||
@@ -346,14 +516,17 @@ async function runRootFulfillment(context) {
|
||||
check(scope, 'F40 海外PDA操作员会话有效', Boolean(picker?.token), 'tokenPresent')
|
||||
picker.warehouseId = warehouse.id
|
||||
check(scope, 'F40 客户会话属于该根订单客户', id(profile.id) === customerId, 'customerOwned')
|
||||
scope.createdResources.phase4InboundProducerFixtures = { marker: scope.marker, containers:[first,second] }
|
||||
const producers = cancelRemainder ? [first] : [first,second]
|
||||
scope.createdResources.phase4InboundProducerFixtures = { marker: scope.marker, containers:producers }
|
||||
scope.createdResources.phase4OverseasPda = { marker:scope.marker, generatedPalletsByInbound:{},
|
||||
palletIds:[],locationIds:[],completedContainerFacts:[], completedContainers:[],
|
||||
overseasWarehouseId:warehouse.id, overseasWarehouseName:warehouse.name, customerId, finalStatus:null }
|
||||
overseasWarehouseId:warehouse.id, overseasWarehouseName:warehouse.name, customerId, finalStatus:null,
|
||||
receivingCompletionEvidence:{} }
|
||||
preserveReceivingEvidence(proof,scope)
|
||||
proof.currentStage = 'OVERSEAS_PLAN_CONVERSION'
|
||||
check(scope, 'F40 两柜真实装柜事件已消费', await awaitProducerConsumption(scope,[first,second]), 'two-producers-consumed')
|
||||
check(scope, 'F40 两柜真实计划及托盘转换成功', await convertReceivingContainers(scope,[first,second]), 'two-containers-converted')
|
||||
const signatureSources = signatureOrder === 'CHILD_FIRST' ? [second,first] : [first,second]
|
||||
check(scope, 'F40 实装来源真实装柜事件已消费', await awaitProducerConsumption(scope,producers), `producers=${producers.length}`)
|
||||
check(scope, 'F40 实装来源真实计划及托盘转换成功', await convertReceivingContainers(scope,producers), `producers=${producers.length}`)
|
||||
const signatureSources = signatureOrder === 'CHILD_FIRST' ? [second,first] : producers
|
||||
for (const [index, producer] of signatureSources.entries()) {
|
||||
proof.currentStage = `OVERSEAS_RECEIVING_${index+1}`
|
||||
scope.marker = `${scope.context.f40BaseMarker}-RECEIVE-${index}`
|
||||
@@ -376,12 +549,27 @@ async function runRootFulfillment(context) {
|
||||
proof.snapshots.push({ phase:index === 0 ? 'ONE_SIGNED_ROOT_PENDING' : 'ALL_SIGNED', ...observed })
|
||||
check(scope,index === 0 ? 'F40 第一片签收根仍未完成' : 'F40 最后一片签收才完成根及来源摘要且各历史只一次',
|
||||
validSignatureGate(observed, signatureOrder === 'CHILD_FIRST' ? 'CHILD' : 'MASTER', index === 1), signatureOrder)
|
||||
proof.snapshots.at(-1).projections = await verifyRootProjections(scope,customerId,rootOrderId,login.accessToken,observed)
|
||||
}
|
||||
if (cancelRemainder) await cancelUnloadedRemainder(scope,proof,customerId,rootOrderId,childOrderId,
|
||||
first.inboundId,id(childRows[0].id))
|
||||
proof.currentStage = 'RECEIPT_REPLAY'
|
||||
const stable = await state(scope,customerId,rootOrderId,childOrderId,first.inboundId)
|
||||
proof.finalProjections = await verifyRootProjections(scope,customerId,rootOrderId,login.accessToken,stable)
|
||||
const finalSignedAt = proof.signatures.map(row => row.signedAt).sort().at(-1)
|
||||
check(scope,'F40 根完成时间为实际两片签收时间最大值', stable.root.rawOrder?.completedAt === finalSignedAt,
|
||||
check(scope,'F40 根完成时间为实际有效来源签收时间最大值', stable.root.rawOrder?.completedAt === finalSignedAt,
|
||||
`completedAt=${stable.root.rawOrder?.completedAt}, maxSignedAt=${finalSignedAt}`)
|
||||
const reader = context.context?.f40ReadOnlyOutboxEvidence
|
||||
const outboxRequest = {customerId,rootOrderId,sourceOrderIds:[rootOrderId,childOrderId],
|
||||
eventType:'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED'}
|
||||
const validOutbox = evidence => cancelRemainder
|
||||
? exactCancelledRemainderOutboxProof(evidence,customerId,rootOrderId,childOrderId)
|
||||
: exactCompletionOutboxProof(evidence,customerId,rootOrderId,[rootOrderId,childOrderId])
|
||||
if (typeof reader === 'function') {
|
||||
proof.beforeReceiptReplayOutbox = await waitState(() => reader(outboxRequest),validOutbox)
|
||||
check(scope,'F40 重复签收前完成事件各实际来源唯一发布',validOutbox(proof.beforeReceiptReplayOutbox),
|
||||
'exact-source-before-receipt-replay')
|
||||
}
|
||||
// Existing receipt helper replays the exact original save command before each signature observation.
|
||||
// Re-saving completed receipts with a fresh business key is also a no-transition operation.
|
||||
for (const [index,signed] of proof.signatures.entries()) {
|
||||
@@ -390,21 +578,48 @@ async function runRootFulfillment(context) {
|
||||
query(`/api/platform/v1/last-leg/delivery-orders/${signed.deliveryNo}/documents`,{customerId}))
|
||||
const documents = (current.categories || []).filter(row => row.files?.length).map(row => ({category:row.category,
|
||||
fileIds:row.files.map(file => id(file.fileId))}))
|
||||
await api(scope,'PUT','重复已完成派送文件保存不得重发签收事实',
|
||||
const documentPath = query(`/api/platform/v1/last-leg/delivery-orders/${signed.deliveryNo}/documents`,{customerId})
|
||||
const deliveryPath = query(`/api/platform/v1/last-leg/delivery-orders/${signed.deliveryNo}`,{customerId})
|
||||
const historyPath = query(`/api/platform/v1/last-leg/delivery-orders/${signed.deliveryNo}/documents/history`,{customerId})
|
||||
const readReceipt = async target => {
|
||||
target.documents = await api(scope,'GET','F40 回查签收资料完整绑定',documentPath)
|
||||
target.delivery = await api(scope,'GET','F40 回查签收派送完整事实',deliveryPath)
|
||||
target.revisions = await api(scope,'GET','F40 回查签收资料完整版本历史',historyPath)
|
||||
return target
|
||||
}
|
||||
const beforeFresh = {documents:current,delivery:await api(scope,'GET','F40 新键前冻结派送业务与操作事实',deliveryPath),
|
||||
revisions:await api(scope,'GET','F40 新键前冻结资料版本历史',historyPath)}
|
||||
check(scope,'F40 新键前原签收事实保持完整稳定',
|
||||
JSON.stringify(signed.receiptSnapshot) === JSON.stringify(beforeFresh.delivery),`delivery=${signed.deliveryNo}`)
|
||||
const saveBody = {expectedVersion:Number(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents}
|
||||
const receiptProof = {deliveryNo:signed.deliveryNo,beforeFresh,saveBody,afterFresh:{},afterSameKey:{}}
|
||||
;(proof.receiptReplayProofs ||= []).push(receiptProof)
|
||||
const saved = await api(scope,'PUT','重复已完成派送文件保存不得重发签收事实',
|
||||
query(`/api/platform/v1/last-leg/delivery-orders/${signed.deliveryNo}/documents`,{customerId}),
|
||||
{expectedVersion:Number(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents})
|
||||
saveBody)
|
||||
receiptProof.saved = saved
|
||||
const afterFresh = await readReceipt(receiptProof.afterFresh)
|
||||
check(scope,'F40 新键资料保存仅递增一次版本及资料审计不得重复完成',
|
||||
saved.replayed === false && validFreshReceiptSave(beforeFresh,afterFresh),`delivery=${signed.deliveryNo}`)
|
||||
const sameKeyReplay = await api(scope,'PUT','F40 新资料命令原键原请求重放',documentPath,saveBody)
|
||||
receiptProof.sameKeyReplay = sameKeyReplay
|
||||
await readReceipt(receiptProof.afterSameKey)
|
||||
check(scope,'F40 新资料命令原键重放资料版本历史派送业务及操作零漂移',
|
||||
sameKeyReplay.replayed === true && JSON.stringify({...sameKeyReplay,replayed:false}) === JSON.stringify(saved)
|
||||
&& JSON.stringify(afterFresh) === JSON.stringify(receiptProof.afterSameKey),`delivery=${signed.deliveryNo}`)
|
||||
}
|
||||
const afterReplay = await state(scope,customerId,rootOrderId,childOrderId,first.inboundId)
|
||||
check(scope,'F40 已完成签收重放根子订单状态版本和历史零新增',
|
||||
JSON.stringify(stable) === JSON.stringify(afterReplay),'exact-root-child-inbound-snapshot')
|
||||
proof.snapshots.push({phase:'AFTER_RECEIPT_REPLAY',...afterReplay})
|
||||
proof.currentStage = 'READ_ONLY_OUTBOX_EVIDENCE'
|
||||
const reader = context.context?.f40ReadOnlyOutboxEvidence
|
||||
if (typeof reader === 'function') {
|
||||
proof.outboxProof = await reader({customerId, rootOrderId, sourceOrderIds:[rootOrderId,childOrderId],
|
||||
eventType:'FIRST_LEG_ORDER_FULFILLMENT_COMPLETED'})
|
||||
proof.outboxProof = await reader(outboxRequest)
|
||||
check(scope,'F40 每个实际来源完成Outbox唯一并已发布',
|
||||
exactCompletionOutboxProof(proof.outboxProof,customerId,rootOrderId,[rootOrderId,childOrderId]),'exact-root/source/customer/readOnly-proof')
|
||||
validOutbox(proof.outboxProof),
|
||||
'exact-root/source/customer/readOnly-proof')
|
||||
check(scope,'F40 新键重复签收完成Outbox数量与发布状态零漂移',
|
||||
outboxCounts(proof.beforeReceiptReplayOutbox) === outboxCounts(proof.outboxProof),'before-after-receipt-replay-outbox')
|
||||
proof.finalStatus='VERIFIED'
|
||||
} else {
|
||||
proof.finalStatus='HTTP_VERIFIED_OUTBOX_PENDING'
|
||||
@@ -417,7 +632,7 @@ async function runRootFulfillment(context) {
|
||||
proof.finalStatus = 'FAILED'
|
||||
proof.failedStage = proof.currentStage
|
||||
throw error
|
||||
} finally { proof.currentStage = 'PDA_IDENTITY_CLEANUP'; await disablePicker(scope,picker,7) }
|
||||
} finally { preserveReceivingEvidence(proof,scope); proof.currentStage = 'PDA_IDENTITY_CLEANUP'; await disablePicker(scope,picker,7) }
|
||||
if (proof.finalStatus === 'VERIFIED') {
|
||||
const failedSteps = (context.steps || []).slice(initialStepCount).filter(row => row.passed === false)
|
||||
if (failedSteps.length) proof.finalStatus = 'FAILED'
|
||||
|
||||
Reference in New Issue
Block a user