test: align phase3 and overseas business acceptance
This commit is contained in:
@@ -339,9 +339,9 @@ export async function run(context) {
|
||||
&& closeTo(rescan?.lengthCm, replacement.length),
|
||||
`sameItem=${asId(rescan?.itemId) === asId(firstScan.itemId)}, confirmedPackageCount=${rescan?.confirmedPackageCount ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const measuredResponse = await context.request('GET', measurementPath)
|
||||
const measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
||||
const measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
let measuredResponse = await context.request('GET', measurementPath)
|
||||
let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS)
|
||||
let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
// The verified customer rule maps this scan to EXTRA_LARGE 60x50x50cm and adds 2cm per edge.
|
||||
const weightedOuter = { length: 64, width: 54, height: 54 }
|
||||
const unitVolume = halfUp(
|
||||
@@ -362,6 +362,63 @@ export async function run(context) {
|
||||
&& closeTo(measured.actualVolumeCbm, totalVolume) && closeTo(measured.volumeWeightKg, volumeWeight),
|
||||
`weightedOuter=${weightedOuter.length}x${weightedOuter.width}x${weightedOuter.height}, unitVolume=${unitVolume}, totalVolume=${totalVolume}, volumeWeight=${volumeWeight}, girth=${girth}`, OPTIONS)
|
||||
|
||||
const incrementalDetailCount = Number(context.context?.phase3LoadingIncrementalDetailCount || 1)
|
||||
if (incrementalDetailCount > 1) {
|
||||
const packagesPerDetail = Number(context.context?.phase3LoadingIncrementalPackageCount || 1)
|
||||
const expectedPackages = incrementalDetailCount * packagesPerDetail
|
||||
context.assert(MODULE, '动态增量多明细箱数前置与冻结总数一致', measuredResponse,
|
||||
expectedPackages === originalPackageCount,
|
||||
`detailCount=${incrementalDetailCount}, packagesPerDetail=${packagesPerDetail}, frozen=${originalPackageCount}`, OPTIONS)
|
||||
if (expectedPackages === originalPackageCount) {
|
||||
const resetFirstCountResponse = await context.request('PUT',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${firstScan.itemId}/confirmed-package-count?${query(tenantId)}`,
|
||||
{ measurementVersion: Number(measured?.version), confirmedPackageCount: packagesPerDetail })
|
||||
const resetFirstCount = context.apiSuccess(MODULE,
|
||||
'增量 fixture 将首条货物确认箱数恢复为自身两箱', resetFirstCountResponse,
|
||||
`itemId=${asId(firstScan.itemId)}, confirmedPackageCount=${packagesPerDetail}`, OPTIONS)
|
||||
context.assert(MODULE, '增量首条货物箱数和版本更新正确', resetFirstCountResponse,
|
||||
Number(resetFirstCount?.confirmedPackageCount) === packagesPerDetail
|
||||
&& Number(resetFirstCount?.measurementVersion) === Number(measured?.version) + 1,
|
||||
`version=${resetFirstCount?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
|
||||
for (let detailIndex = 1; detailIndex < incrementalDetailCount; detailIndex += 1) {
|
||||
const cartonIndex = detailIndex * packagesPerDetail + 1
|
||||
const detailBoxCode = `${inboundCode}#${originalPackageCount}-${cartonIndex}`
|
||||
const scanResponse = await multipartRequest(context, scanPath, {
|
||||
boxCode: detailBoxCode, ...replacement,
|
||||
scannedShipmentId: `FBA-${context.marker.slice(-12)}-${String(detailIndex + 2).padStart(2, '0')}`,
|
||||
machineNo: `MJ-ADMIN-E2E-INC-${detailIndex + 1}`,
|
||||
measuredAt: new Date().toISOString().slice(0, 19),
|
||||
})
|
||||
const scanned = context.apiSuccess(MODULE,
|
||||
`增量 fixture 扫描第${detailIndex + 1}条独立货物箱码`, scanResponse,
|
||||
`boxCode=${detailBoxCode}`, OPTIONS)
|
||||
context.assert(MODULE, `增量第${detailIndex + 1}条扫描解析独立货物`, scanResponse,
|
||||
scanned?.created === true && asId(scanned?.itemId)
|
||||
&& asId(scanned?.itemId) !== asId(firstScan.itemId),
|
||||
`itemId=${asId(scanned?.itemId) || 'missing'}, inboundDetailId=${asId(scanned?.inboundDetailId) || 'pending-query'}`, OPTIONS)
|
||||
if (!scanned?.itemId) continue
|
||||
const countDetailResponse = await context.request('PUT',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/items/${scanned.itemId}/confirmed-package-count?${query(tenantId)}`,
|
||||
{ measurementVersion: Number(scanned.measurementVersion), confirmedPackageCount: packagesPerDetail })
|
||||
const countDetail = context.apiSuccess(MODULE,
|
||||
`增量第${detailIndex + 1}条货物确认自身箱数`, countDetailResponse,
|
||||
`confirmedPackageCount=${packagesPerDetail}`, OPTIONS)
|
||||
context.assert(MODULE, `增量第${detailIndex + 1}条货物箱数更新正确`, countDetailResponse,
|
||||
Number(countDetail?.confirmedPackageCount) === packagesPerDetail,
|
||||
`measurementVersion=${countDetail?.measurementVersion ?? 'missing'}`, OPTIONS)
|
||||
}
|
||||
measuredResponse = await context.request('GET', measurementPath)
|
||||
measured = context.apiSuccess(MODULE, '回查增量 fixture 四条测量明细', measuredResponse, '', OPTIONS)
|
||||
measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId))
|
||||
const detailIds = new Set((measured?.items || []).map((item) => asId(item?.inboundDetailId)).filter(Boolean))
|
||||
context.assert(MODULE, '增量 fixture 形成四条真实独立货物测量明细', measuredResponse,
|
||||
detailIds.size >= incrementalDetailCount
|
||||
&& measured.items.filter((item) => detailIds.has(asId(item?.inboundDetailId))).length >= incrementalDetailCount,
|
||||
`measurementItems=${measured?.items?.length ?? 'missing'}, distinctInboundDetails=${detailIds.size}`, OPTIONS)
|
||||
}
|
||||
}
|
||||
|
||||
let inboundDetailId = measuredItem?.inboundDetailId
|
||||
if (!inboundDetailId) {
|
||||
const orderId = asId(frozenSummary?.orderId || context.createdResources?.orders?.orderId)
|
||||
@@ -387,10 +444,18 @@ export async function run(context) {
|
||||
return
|
||||
}
|
||||
|
||||
const draftPayload = {
|
||||
measurementId: measured.measurementId, version: Number(measured.version),
|
||||
actualGoodsName: `墨家测量验收-${context.marker.slice(-12)}`,
|
||||
items: [{
|
||||
const draftItems = incrementalDetailCount > 1
|
||||
? (measured?.items || []).filter((item) => asId(item?.inboundDetailId) && asId(item?.intelligentImageFileId))
|
||||
.slice(0, incrementalDetailCount).map((item) => ({
|
||||
inboundDetailId: item.inboundDetailId, itemNo: item.itemNo,
|
||||
boxSpecification: item.boxSpecification, packageCount: Number(item.confirmedPackageCount),
|
||||
lengthCm: Number(item.lengthCm), widthCm: Number(item.widthCm), heightCm: Number(item.heightCm),
|
||||
intelligentImageFileId: item.intelligentImageFileId,
|
||||
overLimitWarning: item.overLimitWarning ?? null,
|
||||
outerLengthCm: item.outerLengthCm ?? null, outerWidthCm: item.outerWidthCm ?? null,
|
||||
outerHeightCm: item.outerHeightCm ?? null, remark: context.marker,
|
||||
}))
|
||||
: [{
|
||||
inboundDetailId, itemNo: measuredItem.itemNo,
|
||||
boxSpecification: measuredItem.boxSpecification, packageCount: originalPackageCount,
|
||||
lengthCm: replacement.length, widthCm: replacement.width, heightCm: replacement.height,
|
||||
@@ -398,12 +463,16 @@ export async function run(context) {
|
||||
overLimitWarning: measuredItem.overLimitWarning ?? null,
|
||||
outerLengthCm: measuredItem.outerLengthCm ?? null, outerWidthCm: measuredItem.outerWidthCm ?? null,
|
||||
outerHeightCm: measuredItem.outerHeightCm ?? null, remark: context.marker,
|
||||
}],
|
||||
}]
|
||||
const draftPayload = {
|
||||
measurementId: measured.measurementId, version: Number(measured.version),
|
||||
actualGoodsName: `墨家测量验收-${context.marker.slice(-12)}`,
|
||||
items: draftItems,
|
||||
}
|
||||
const draftResponse = await context.request('PUT',
|
||||
`/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/draft?${query(tenantId)}`, draftPayload)
|
||||
const draft = context.apiSuccess(MODULE, '保存同一测量批次草稿', draftResponse,
|
||||
`measurementId=${asId(measured.measurementId)}, items=1`, OPTIONS)
|
||||
`measurementId=${asId(measured.measurementId)}, items=${draftItems.length}`, OPTIONS)
|
||||
context.assert(MODULE, '草稿沿用批次并递增版本', draftResponse,
|
||||
asId(draft?.measurementId) === asId(measured.measurementId)
|
||||
&& Number(draft?.version) === Number(measured.version) + 1 && draft?.status === 'DRAFT',
|
||||
|
||||
@@ -37,17 +37,26 @@ test('phase3 inventory mutation and downstream gates cannot pass as coverage gap
|
||||
assert.doesNotMatch(aggregate, /报关与船期历史并入统一时间线|当前服务端聚合未公开/)
|
||||
})
|
||||
|
||||
test('outbound lifecycle checks replay, hash conflict, stale version and final reread', () => {
|
||||
test('outbound lifecycle checks confirm replay, immutable departure reverse boundary and final reread', () => {
|
||||
const source = read('phase3-outbound-dispatch.mjs')
|
||||
for (const step of [
|
||||
'confirm-dispatch 幂等重放',
|
||||
'相同 Idempotency-Key 不同请求拒绝',
|
||||
'reverse-dispatch 幂等重放',
|
||||
'reverse-dispatch 同键异参返回 A0409',
|
||||
'撤销后旧版本再次确认返回 A0409',
|
||||
'实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409',
|
||||
'禁止撤销边界重复请求仍精确返回A0409',
|
||||
'负向与幂等验证后回查出仓聚合',
|
||||
'出仓聚合保持 READY_FOR_DISPATCH、LOADED 和确认撤销历史',
|
||||
'出仓聚合保持 WAITING_FOR_LOADING、LOADED 且无成功撤销历史',
|
||||
]) assert.match(source, new RegExp(step))
|
||||
assert.match(source, /if \(!confirmed\?\.aggregate\) return/)
|
||||
assert.match(source, /hashConflict\.http === 200 && codeOf\(hashConflict\) === 'A0409'/)
|
||||
const confirm = source.indexOf('确认国内出仓进入等待装船')
|
||||
const reverse = source.indexOf('实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409')
|
||||
const finalRead = source.indexOf('负向与幂等验证后回查出仓聚合')
|
||||
assert.ok(confirm >= 0 && reverse > confirm && finalRead > reverse)
|
||||
assert.match(source, /confirmedStatus: confirmed\.aggregate\.dispatchStatus/)
|
||||
assert.doesNotMatch(source, /outbound-confirm-final|finalConfirmed|reversedVersion/)
|
||||
const waiting = read('phase3-waiting-for-loading.mjs')
|
||||
assert.match(waiting, /confirmations\.length === 1 && reversed\.length === 0/)
|
||||
assert.match(waiting, /实际离仓节点冻结后保持一次确认且零次成功撤销/)
|
||||
assert.match(waiting, /detail\?\.aggregate\?\.dispatchStatus === 'WAITING_FOR_LOADING'/)
|
||||
})
|
||||
|
||||
@@ -127,52 +127,40 @@ export async function run(context) {
|
||||
const reverseResponse = await request(context, 'POST', `${BASE}/reverse-dispatch`, {
|
||||
inboundId, containerBookingId, expectedVersion: Number(confirmed.aggregate.version), remark: context.marker,
|
||||
}, { 'Idempotency-Key': reverseKey })
|
||||
const reversed = context.apiSuccess(MODULE, '撤销国内出仓恢复装柜完成', reverseResponse, '', OPTIONS)
|
||||
context.assert(MODULE, 'reverse-dispatch 恢复 READY_FOR_DISPATCH 与 LOADED', reverseResponse,
|
||||
reversed?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH' && reversed?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& reversed?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'REVERSE_DISPATCH'),
|
||||
`dispatch=${reversed?.aggregate?.dispatchStatus || 'missing'}, inbound=${reversed?.aggregate?.inboundStatus || 'missing'}`, OPTIONS)
|
||||
context.addStep(MODULE, '实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409', reverseResponse,
|
||||
reverseResponse.http === 200 && codeOf(reverseResponse) === 'A0409'
|
||||
&& String(reverseResponse.body?.message || '').includes('DEPARTED_WAREHOUSE')
|
||||
&& String(reverseResponse.body?.message || '').includes('禁止撤销'),
|
||||
`HTTP=${reverseResponse.http}, code=${codeOf(reverseResponse)}, message=${reverseResponse.body?.message || 'missing'}`, OPTIONS)
|
||||
const reverseReplayResponse = await request(context, 'POST', `${BASE}/reverse-dispatch`, {
|
||||
inboundId, containerBookingId, expectedVersion: Number(confirmed.aggregate.version), remark: context.marker,
|
||||
}, { 'Idempotency-Key': reverseKey })
|
||||
const reverseReplay = context.apiSuccess(MODULE, 'reverse-dispatch 幂等重放', reverseReplayResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '撤销重放保持 READY_FOR_DISPATCH 与同一版本', reverseReplayResponse,
|
||||
reverseReplay?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH'
|
||||
&& Number(reverseReplay?.aggregate?.version) === Number(reversed?.aggregate?.version),
|
||||
`dispatch=${reverseReplay?.aggregate?.dispatchStatus || 'missing'}, version=${reverseReplay?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const reverseHashConflict = await request(context, 'POST', `${BASE}/reverse-dispatch`, {
|
||||
inboundId, containerBookingId, expectedVersion: Number(confirmed.aggregate.version), remark: 'forged',
|
||||
}, { 'Idempotency-Key': reverseKey })
|
||||
context.addStep(MODULE, 'reverse-dispatch 同键异参返回 A0409', reverseHashConflict,
|
||||
reverseHashConflict.http === 200 && codeOf(reverseHashConflict) === 'A0409',
|
||||
`HTTP=${reverseHashConflict.http}, code=${codeOf(reverseHashConflict)}`, OPTIONS)
|
||||
|
||||
const staleConfirmResponse = await request(context, 'POST', `${BASE}/confirm-dispatch`, actionBody,
|
||||
{ 'Idempotency-Key': `${context.marker}-outbound-confirm-stale`.slice(0, 128) })
|
||||
context.addStep(MODULE, '撤销后旧版本再次确认返回 A0409', staleConfirmResponse,
|
||||
staleConfirmResponse.http === 200 && codeOf(staleConfirmResponse) === 'A0409',
|
||||
`HTTP=${staleConfirmResponse.http}, code=${codeOf(staleConfirmResponse)}`, OPTIONS)
|
||||
context.addStep(MODULE, '禁止撤销边界重复请求仍精确返回A0409', reverseReplayResponse,
|
||||
reverseReplayResponse.http === 200 && codeOf(reverseReplayResponse) === 'A0409'
|
||||
&& String(reverseReplayResponse.body?.message || '').includes('DEPARTED_WAREHOUSE'),
|
||||
`HTTP=${reverseReplayResponse.http}, code=${codeOf(reverseReplayResponse)}`, OPTIONS)
|
||||
|
||||
const finalDetailResponse = await request(context, 'GET', `${BASE}/detail?${selector}`)
|
||||
const finalDetail = context.apiSuccess(MODULE, '负向与幂等验证后回查出仓聚合', finalDetailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '出仓聚合保持 READY_FOR_DISPATCH、LOADED 和确认撤销历史', finalDetailResponse,
|
||||
finalDetail?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH'
|
||||
context.assert(MODULE, '出仓聚合保持 WAITING_FOR_LOADING、LOADED 且无成功撤销历史', finalDetailResponse,
|
||||
finalDetail?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING'
|
||||
&& finalDetail?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& Number(finalDetail?.aggregate?.version) === Number(reversed?.aggregate?.version)
|
||||
&& finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'CONFIRM_DISPATCH')
|
||||
&& finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH' && item.action === 'REVERSE_DISPATCH'),
|
||||
&& Number(finalDetail?.aggregate?.version) === Number(confirmed?.aggregate?.version)
|
||||
&& finalDetail?.timeline?.filter?.((item) => item.module === 'OUTBOUND_DISPATCH'
|
||||
&& item.action === 'CONFIRM_DISPATCH').length === 1
|
||||
&& !finalDetail?.timeline?.some?.((item) => item.module === 'OUTBOUND_DISPATCH'
|
||||
&& item.action === 'REVERSE_DISPATCH'),
|
||||
`dispatch=${finalDetail?.aggregate?.dispatchStatus || 'missing'}, inbound=${finalDetail?.aggregate?.inboundStatus || 'missing'}, version=${finalDetail?.aggregate?.version ?? 'missing'}`, OPTIONS)
|
||||
if (reversed?.aggregate) {
|
||||
if (confirmed?.aggregate) {
|
||||
context.createdResources.phase3OutboundDispatch = {
|
||||
marker: context.marker, inboundId, containerBookingId,
|
||||
orderId: id(reversed.aggregate.orderId), tenantId: id(reversed.aggregate.tenantId),
|
||||
confirmedStatus: confirmed?.aggregate?.dispatchStatus || null,
|
||||
confirmedInboundStatus: confirmed?.aggregate?.inboundStatus || null,
|
||||
confirmedVersion: Number(confirmed?.aggregate?.version),
|
||||
finalStatus: reversed.aggregate.dispatchStatus,
|
||||
finalInboundStatus: reversed.aggregate.inboundStatus,
|
||||
finalVersion: Number(reversed.aggregate.version),
|
||||
orderId: id(confirmed.aggregate.orderId), tenantId: id(confirmed.aggregate.tenantId),
|
||||
confirmedStatus: confirmed.aggregate.dispatchStatus,
|
||||
confirmedInboundStatus: confirmed.aggregate.inboundStatus,
|
||||
confirmedVersion: Number(confirmed.aggregate.version),
|
||||
finalStatus: confirmed.aggregate.dispatchStatus,
|
||||
finalInboundStatus: confirmed.aggregate.inboundStatus,
|
||||
finalVersion: Number(confirmed.aggregate.version),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,6 +460,8 @@ test('phase3 loading proves stale inventory zero-write and defers final completi
|
||||
|
||||
test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstream completion', () => {
|
||||
const loading = source('./phase3-loading.mjs')
|
||||
const entry = source('../admin-test-env-acceptance.mjs')
|
||||
const measurement = source('./inbound-measurement.mjs')
|
||||
assertOrder(loading, [
|
||||
'本轮增量 fixture 提供独立 SKIP 与 PARTIAL 合法货物明细',
|
||||
'加入本轮待跳拣异常明细',
|
||||
@@ -486,6 +488,11 @@ test('phase3 loading resolves legal SKIP and PARTIAL exceptions before downstrea
|
||||
assert.match(loading, /exceptionOverview\?\.pda\?\.exceptionReasons\?\.includes\?\.\(skipReason\)/)
|
||||
assert.match(loading, /Number\(exceptionOverview\?\.pda\?\.unresolvedItemCount\) === 0/)
|
||||
assert.doesNotMatch(loading, /addCoverageGap\([^)]*(SKIP|PARTIAL|未解决异常)/)
|
||||
assert.match(entry, /this\.context\.phase3LoadingIncrementalDetailCount = 4/)
|
||||
assert.match(measurement, /增量 fixture 形成四条真实独立货物测量明细/)
|
||||
assert.match(measurement, /detailBoxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-\$\{cartonIndex\}`/)
|
||||
assert.match(measurement, /items: draftItems/)
|
||||
assert.match(measurement, /expectedPackages === originalPackageCount/)
|
||||
})
|
||||
|
||||
test('phase3 loading fixture forces A0 through reversible public location status APIs', () => {
|
||||
|
||||
@@ -17,17 +17,17 @@ export async function run(context) {
|
||||
const detail = context.apiSuccess(MODULE, '回查 WAITING_FOR_LOADING 状态历史', response, selector, OPTIONS)
|
||||
const history = Array.isArray(detail?.timeline)
|
||||
? detail.timeline.filter((item) => item.module === 'OUTBOUND_DISPATCH') : []
|
||||
const confirmed = history.find((item) => item.action === 'CONFIRM_DISPATCH')
|
||||
const reversed = history.find((item) => item.action === 'REVERSE_DISPATCH')
|
||||
const confirmations = history.filter((item) => item.action === 'CONFIRM_DISPATCH')
|
||||
const confirmed = confirmations[0]
|
||||
const reversed = history.filter((item) => item.action === 'REVERSE_DISPATCH')
|
||||
context.assert(MODULE, '确认出仓真实进入 WAITING_FOR_LOADING', response,
|
||||
fixture.confirmedStatus === 'WAITING_FOR_LOADING'
|
||||
&& fixture.confirmedInboundStatus === 'LOADED'
|
||||
&& confirmed?.fromStatus === 'READY_FOR_DISPATCH' && confirmed?.toStatus === 'WAITING_FOR_LOADING'
|
||||
&& Boolean(confirmed?.operatorId) && Boolean(confirmed?.operatorName) && Boolean(confirmed?.occurredAt),
|
||||
`confirmed=${confirmed?.fromStatus || 'missing'}->${confirmed?.toStatus || 'missing'}, version=${fixture.confirmedVersion ?? 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '撤销后状态恢复且 WAITING 历史不可变', response,
|
||||
detail?.aggregate?.dispatchStatus === 'READY_FOR_DISPATCH' && detail?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& reversed?.fromStatus === 'WAITING_FOR_LOADING' && reversed?.toStatus === 'READY_FOR_DISPATCH'
|
||||
&& Boolean(reversed?.operatorId) && Boolean(reversed?.occurredAt),
|
||||
`current=${detail?.aggregate?.dispatchStatus || 'missing'}, reversed=${reversed?.fromStatus || 'missing'}->${reversed?.toStatus || 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '实际离仓节点冻结后保持一次确认且零次成功撤销', response,
|
||||
detail?.aggregate?.dispatchStatus === 'WAITING_FOR_LOADING' && detail?.aggregate?.inboundStatus === 'LOADED'
|
||||
&& confirmations.length === 1 && reversed.length === 0,
|
||||
`current=${detail?.aggregate?.dispatchStatus || 'missing'}, confirmations=${confirmations.length}, reversals=${reversed.length}`, OPTIONS)
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ function fixture(options = {}) {
|
||||
const state = {
|
||||
warehouseStatus: 'ACTIVE', purchase: null, purchaseHash: '', truck: null, truckHash: '', looseboard: null, looseboardHash: '',
|
||||
purchaseHistories: [], truckHistories: [], looseboardHistories: [],
|
||||
purchaseReviews: new Map(), truckReviews: new Map(), truckStatuses: new Map(), looseboardStatuses: new Map(),
|
||||
}
|
||||
const activeCustomers = options.activeCustomers ?? [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]
|
||||
const warehouseOptions = options.warehouseOptions ?? [
|
||||
@@ -71,11 +72,19 @@ function fixture(options = {}) {
|
||||
if (requestPath.includes('/warehouse-options')) return response(warehouseOptions)
|
||||
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page(state.purchase ? [state.purchase] : []))
|
||||
if (requestPath.includes('/overseas-procurements/OP1/review?') && method === 'POST') {
|
||||
assert.equal(Number.isInteger(Number(body.version)), true)
|
||||
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
|
||||
const hash = JSON.stringify(body)
|
||||
const replay = state.purchaseReviews.get(body.idempotencyKey)
|
||||
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
|
||||
if (state.purchase.status !== 'PENDING_REVIEW') return response(null, 'A0409')
|
||||
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
|
||||
const next = body.decision === 'REJECT' ? 'REJECTED' : 'APPROVED'
|
||||
state.purchase = { ...state.purchase, status: next, version: state.purchase.version + 1 }
|
||||
state.purchaseHistories.push({ operationType: 'REVIEW', fromStatus: 'PENDING_REVIEW', toStatus: next, remark: body.remark })
|
||||
return response(purchaseDetail())
|
||||
const result = structuredClone(purchaseDetail())
|
||||
state.purchaseReviews.set(body.idempotencyKey, { hash, result })
|
||||
return response(result)
|
||||
}
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response(purchaseDetail())
|
||||
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') {
|
||||
@@ -102,15 +111,37 @@ function fixture(options = {}) {
|
||||
return response(truckDetail())
|
||||
}
|
||||
if (requestPath.includes('/us-truck-orders?') && method === 'GET') return response(page(state.truck ? [state.truck] : []))
|
||||
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') {
|
||||
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
|
||||
const allowed = (state.truck.status === 'PENDING_REVIEW' && ['WAITING_INBOUND', 'CANCELLED'].includes(body.status))
|
||||
|| (state.truck.status === 'WAITING_INBOUND' && body.status === 'CANCELLED')
|
||||
if (!allowed) return response(null, 'A0409')
|
||||
if (requestPath.includes('/us-truck-orders/TK1/review?') && method === 'POST') {
|
||||
assert.equal(Number.isInteger(Number(body.version)), true)
|
||||
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
|
||||
const hash = JSON.stringify(body)
|
||||
const replay = state.truckReviews.get(body.idempotencyKey)
|
||||
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
|
||||
if (state.truck.status !== 'PENDING_REVIEW' || Number(body.version) !== state.truck.version) {
|
||||
return response(null, 'A0409')
|
||||
}
|
||||
const next = body.decision === 'APPROVE' ? 'WAITING_INBOUND' : 'REJECTED'
|
||||
const from = state.truck.status
|
||||
state.truck = { ...state.truck, status: body.status, version: state.truck.version + 1 }
|
||||
state.truckHistories.push({ fromStatus: from, toStatus: body.status, remark: body.remark })
|
||||
return response(truckDetail())
|
||||
state.truck = { ...state.truck, status: next, version: state.truck.version + 1 }
|
||||
state.truckHistories.push({ fromStatus: from, toStatus: next, remark: body.remark })
|
||||
const result = structuredClone(truckDetail())
|
||||
state.truckReviews.set(body.idempotencyKey, { hash, result })
|
||||
return response(result)
|
||||
}
|
||||
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') {
|
||||
assert.equal(body.status, 'CANCELLED')
|
||||
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
|
||||
const hash = JSON.stringify(body)
|
||||
const replay = state.truckStatuses.get(body.idempotencyKey)
|
||||
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
|
||||
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
|
||||
if (!['PENDING_REVIEW', 'WAITING_INBOUND'].includes(state.truck.status)) return response(null, 'A0409')
|
||||
const from = state.truck.status
|
||||
state.truck = { ...state.truck, status: 'CANCELLED', version: state.truck.version + 1 }
|
||||
state.truckHistories.push({ fromStatus: from, toStatus: 'CANCELLED', remark: body.remark })
|
||||
const result = structuredClone(truckDetail())
|
||||
state.truckStatuses.set(body.idempotencyKey, { hash, result })
|
||||
return response(result)
|
||||
}
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'GET') return response(truckDetail())
|
||||
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'PATCH') {
|
||||
@@ -130,6 +161,10 @@ function fixture(options = {}) {
|
||||
}
|
||||
if (requestPath.includes('/local-looseboard-orders?') && method === 'GET') return response(page(state.looseboard ? [state.looseboard] : []))
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1/status?') && method === 'PATCH') {
|
||||
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
|
||||
const hash = JSON.stringify(body)
|
||||
const replay = state.looseboardStatuses.get(body.idempotencyKey)
|
||||
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
|
||||
if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409')
|
||||
const allowed = (state.looseboard.status === 'PENDING_ACCEPTANCE' && ['DISPATCHED', 'CANCELLED'].includes(body.status))
|
||||
|| (state.looseboard.status === 'DISPATCHED' && body.status === 'CANCELLED')
|
||||
@@ -137,7 +172,9 @@ function fixture(options = {}) {
|
||||
const from = state.looseboard.status
|
||||
state.looseboard = { ...state.looseboard, status: body.status, version: state.looseboard.version + 1 }
|
||||
state.looseboardHistories.push({ operationType: 'STATUS', fromStatus: from, toStatus: body.status, remark: body.remark })
|
||||
return response(looseboardDetail())
|
||||
const result = structuredClone(looseboardDetail())
|
||||
state.looseboardStatuses.set(body.idempotencyKey, { hash, result })
|
||||
return response(result)
|
||||
}
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'GET') return response(looseboardDetail())
|
||||
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'PUT') {
|
||||
@@ -189,20 +226,41 @@ test('complete procurement truck and looseboard lifecycles pass with legal marke
|
||||
|
||||
const procurementWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
|
||||
assert.deepEqual(procurementWrites.map((call) => call.method),
|
||||
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST'])
|
||||
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST', 'POST', 'POST'])
|
||||
assert.equal(procurementWrites.filter((call) => call.method === 'PATCH').length, 4)
|
||||
assert.ok(procurementWrites.some((call) => call.method === 'PATCH' && call.body.productSpecification === null && call.body.productUrl === ''))
|
||||
const procurementReviews = procurementWrites.filter((call) => call.path.includes('/review?'))
|
||||
assert.equal(procurementReviews.length, 4)
|
||||
assert.ok(procurementReviews.every((call) => Number.isInteger(Number(call.body.version))
|
||||
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
|
||||
assert.equal(new Set(procurementReviews.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
|
||||
assert.notEqual(procurementReviews[3].body.idempotencyKey, procurementReviews[0].body.idempotencyKey)
|
||||
|
||||
const truckCalls = context.calls.filter((call) => call.path.includes('/us-truck-orders'))
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'POST').length, 3)
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'POST').length, 7)
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
|
||||
assert.equal(truckCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?')).length, 3)
|
||||
const truckReviews = truckCalls.filter((call) => call.method === 'POST' && call.path.includes('/review?'))
|
||||
assert.equal(truckReviews.length, 4)
|
||||
assert.ok(truckReviews.every((call) => Number.isInteger(Number(call.body.version))
|
||||
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
|
||||
assert.equal(new Set(truckReviews.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
|
||||
assert.notEqual(truckReviews[3].body.idempotencyKey, truckReviews[0].body.idempotencyKey)
|
||||
const truckStatuses = truckCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?'))
|
||||
assert.equal(truckStatuses.length, 1)
|
||||
assert.ok(truckStatuses.every((call) => call.body.status === 'CANCELLED'
|
||||
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
|
||||
|
||||
const looseboardCalls = context.calls.filter((call) => call.path.includes('/local-looseboard-orders'))
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'POST').length, 3)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'PUT').length, 2)
|
||||
assert.equal(looseboardCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?')).length, 3)
|
||||
const looseboardStatuses = looseboardCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?'))
|
||||
assert.equal(looseboardStatuses.length, 5)
|
||||
assert.ok(looseboardStatuses.every((call) => typeof call.body.idempotencyKey === 'string'
|
||||
&& call.body.idempotencyKey.length > 0))
|
||||
assert.equal(new Set(looseboardStatuses.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
|
||||
assert.equal(new Set(looseboardStatuses.slice(3).map((call) => call.body.idempotencyKey)).size, 2)
|
||||
assert.ok(looseboardStatuses.slice(3).every((call) => call.body.idempotencyKey !== looseboardStatuses[0].body.idempotencyKey))
|
||||
|
||||
const registry = context.createdResources.phase4OverseasBusiness
|
||||
assert.equal(registry.purchase.finalStatus, 'REJECTED')
|
||||
@@ -265,5 +323,6 @@ test('contract locks candidates three-state idempotency histories legal terminal
|
||||
'/api/merchant/v1/last-leg/overseas-procurements', '显式 null 与空串清空', '字段缺失不修改',
|
||||
'同幂等键同载荷精确重放', '同幂等键异载荷返回冲突', '旧 version 返回 A0409',
|
||||
'状态正向推进', '状态逆向回退返回冲突', '流水覆盖', "status: 'CANCELLED'",
|
||||
'/review', 'truck-review-approve', 'looseboard-status-dispatched',
|
||||
'requestId=', 'traceId=', 'createdResources.phase4OverseasBusiness']) assert.ok(source.includes(token), token)
|
||||
})
|
||||
|
||||
@@ -248,14 +248,32 @@ async function procurementFlow(context, prerequisite) {
|
||||
|
||||
const rejected = await overseas(context, 'POST', '驳回本轮采购订单并进入合法终态',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
{ decision: 'REJECT', remark: `${context.marker}-REJECTED` })
|
||||
{ version: Number(afterConflictSummary?.version), decision: 'REJECT',
|
||||
idempotencyKey: `${context.marker}-procurement-review-reject`.slice(0, 255),
|
||||
remark: `${context.marker}-REJECTED` })
|
||||
const rejectedSummary = summary(rejected.data)
|
||||
context.assert(MODULE, '采购审核正向驳回成功', rejected.response,
|
||||
rejectedSummary?.status === 'REJECTED' && Number(rejectedSummary?.version) === Number(clearedSummary?.version) + 1,
|
||||
`status=${rejectedSummary?.status || 'missing'}, version=${rejectedSummary?.version ?? 'missing'}; ${trace(rejected.response)}`, OPTIONS)
|
||||
const rejectedReplayBody = { version: Number(afterConflictSummary?.version), decision: 'REJECT',
|
||||
idempotencyKey: `${context.marker}-procurement-review-reject`.slice(0, 255),
|
||||
remark: `${context.marker}-REJECTED` }
|
||||
const rejectedReplay = await overseas(context, 'POST', '采购审核同键同载荷精确重放',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
rejectedReplayBody)
|
||||
context.assert(MODULE, '采购审核重放复用终态且版本不漂移', rejectedReplay.response,
|
||||
summary(rejectedReplay.data)?.status === 'REJECTED'
|
||||
&& Number(summary(rejectedReplay.data)?.version) === Number(rejectedSummary?.version),
|
||||
trace(rejectedReplay.response), OPTIONS)
|
||||
const reviewHashConflict = await context.requestOverseas('POST',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
{ ...rejectedReplayBody, remark: 'DIFFERENT-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '采购审核同键异载荷返回冲突', reviewHashConflict)
|
||||
const reverseReview = await context.requestOverseas('POST',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(owned.orderNo)}/review`, { customerId }),
|
||||
{ decision: 'APPROVE', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
{ version: Number(rejectedSummary?.version), decision: 'APPROVE',
|
||||
idempotencyKey: `${context.marker}-procurement-review-terminal-reverse`.slice(0, 255),
|
||||
remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '采购审核终态逆向返回冲突', reverseReview)
|
||||
|
||||
const final = await overseas(context, 'GET', '采购最终状态与流水回查', path)
|
||||
@@ -328,8 +346,11 @@ async function cancelMarkerOrder(context, resource, base, cancellableStatuses, l
|
||||
return
|
||||
}
|
||||
const statusPath = query(`${base}/${encodeURIComponent(resource.orderNo)}/status`, { customerId: resource.customerId })
|
||||
const keyScope = label === '卡车' ? 'truck' : 'looseboard'
|
||||
const cancelled = await overseas(context, 'PATCH', `取消 marker ${label}订单`, statusPath,
|
||||
{ version: Number(currentSummary.version), status: 'CANCELLED', remark: `${context.marker}-acceptance-cleanup` })
|
||||
{ version: Number(currentSummary.version), status: 'CANCELLED',
|
||||
idempotencyKey: `${context.marker}-${keyScope}-${resource.orderNo}-cancel`.slice(0, 255),
|
||||
remark: `${context.marker}-acceptance-cleanup` })
|
||||
const cancelledSummary = summary(cancelled.data)
|
||||
const verified = await overseas(context, 'GET', `回查 marker ${label}订单最终状态`, detailPath)
|
||||
const finalSummary = summary(verified.data)
|
||||
@@ -381,17 +402,29 @@ async function truckFlow(context, customerId) {
|
||||
afterStaleSummary?.destination === after?.destination && Number(afterStaleSummary?.version) === Number(after?.version),
|
||||
`destination=${afterStaleSummary?.destination || 'missing'}, version=${afterStaleSummary?.version ?? 'missing'}; ${trace(afterStale.response)}`, OPTIONS)
|
||||
|
||||
const statusPath = query(`${base}/${encodeURIComponent(orderNo)}/status`, { customerId })
|
||||
const forwarded = await overseas(context, 'PATCH', '卡车状态正向推进到待入仓', statusPath,
|
||||
{ version: Number(afterStaleSummary?.version), status: 'WAITING_INBOUND', remark: `${context.marker}-FORWARD` })
|
||||
const reviewPath = query(`${base}/${encodeURIComponent(orderNo)}/review`, { customerId })
|
||||
const reviewBody = { version: Number(afterStaleSummary?.version), decision: 'APPROVE',
|
||||
idempotencyKey: `${context.marker}-truck-review-approve`.slice(0, 255),
|
||||
remark: `${context.marker}-FORWARD` }
|
||||
const forwarded = await overseas(context, 'POST', '卡车审核正向推进到待入仓', reviewPath, reviewBody)
|
||||
const forwardedSummary = summary(forwarded.data)
|
||||
context.assert(MODULE, '卡车状态正向推进成功', forwarded.response,
|
||||
forwardedSummary?.status === 'WAITING_INBOUND'
|
||||
&& Number(forwardedSummary?.version) === Number(afterStaleSummary?.version) + 1,
|
||||
`status=${forwardedSummary?.status || 'missing'}, version=${forwardedSummary?.version ?? 'missing'}; ${trace(forwarded.response)}`, OPTIONS)
|
||||
const reversed = await context.requestOverseas('PATCH', statusPath,
|
||||
{ version: Number(forwardedSummary?.version), status: 'PENDING_REVIEW', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '卡车状态逆向回退返回冲突', reversed)
|
||||
const reviewReplay = await overseas(context, 'POST', '卡车审核同键同载荷精确重放', reviewPath, reviewBody)
|
||||
context.assert(MODULE, '卡车审核重放复用待入仓终态且版本不漂移', reviewReplay.response,
|
||||
summary(reviewReplay.data)?.status === 'WAITING_INBOUND'
|
||||
&& Number(summary(reviewReplay.data)?.version) === Number(forwardedSummary?.version),
|
||||
trace(reviewReplay.response), OPTIONS)
|
||||
const reviewMismatch = await context.requestOverseas('POST', reviewPath,
|
||||
{ ...reviewBody, remark: 'DIFFERENT-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '卡车审核同键异载荷返回冲突', reviewMismatch)
|
||||
const reversed = await context.requestOverseas('POST', reviewPath,
|
||||
{ version: Number(forwardedSummary?.version), decision: 'REJECT',
|
||||
idempotencyKey: `${context.marker}-truck-review-terminal-reverse`.slice(0, 255),
|
||||
remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '卡车审核终态逆向返回冲突', reversed)
|
||||
const history = await overseas(context, 'GET', '卡车状态冲突后流水回查', path)
|
||||
const historySummary = summary(history.data)
|
||||
const histories = Array.isArray(history.data?.histories) ? history.data.histories : []
|
||||
@@ -448,14 +481,30 @@ async function looseboardFlow(context, customerId) {
|
||||
|
||||
const statusPath = query(`${base}/${encodeURIComponent(orderNo)}/status`, { customerId })
|
||||
const forwarded = await overseas(context, 'PATCH', '散板状态正向推进到已派车', statusPath,
|
||||
{ version: Number(afterStaleSummary?.version), status: 'DISPATCHED', remark: `${context.marker}-FORWARD` })
|
||||
{ version: Number(afterStaleSummary?.version), status: 'DISPATCHED',
|
||||
idempotencyKey: `${context.marker}-looseboard-status-dispatched`.slice(0, 255),
|
||||
remark: `${context.marker}-FORWARD` })
|
||||
const forwardedSummary = summary(forwarded.data)
|
||||
context.assert(MODULE, '散板状态正向推进成功', forwarded.response,
|
||||
forwardedSummary?.status === 'DISPATCHED'
|
||||
&& Number(forwardedSummary?.version) === Number(afterStaleSummary?.version) + 1,
|
||||
`status=${forwardedSummary?.status || 'missing'}, version=${forwardedSummary?.version ?? 'missing'}; ${trace(forwarded.response)}`, OPTIONS)
|
||||
const forwardBody = { version: Number(afterStaleSummary?.version), status: 'DISPATCHED',
|
||||
idempotencyKey: `${context.marker}-looseboard-status-dispatched`.slice(0, 255),
|
||||
remark: `${context.marker}-FORWARD` }
|
||||
const forwardReplay = await overseas(context, 'PATCH', '散板状态同键同载荷精确重放', statusPath,
|
||||
forwardBody)
|
||||
context.assert(MODULE, '散板状态重放复用已派车终态且版本不漂移', forwardReplay.response,
|
||||
summary(forwardReplay.data)?.status === 'DISPATCHED'
|
||||
&& Number(summary(forwardReplay.data)?.version) === Number(forwardedSummary?.version),
|
||||
trace(forwardReplay.response), OPTIONS)
|
||||
const forwardMismatch = await context.requestOverseas('PATCH', statusPath,
|
||||
{ ...forwardBody, remark: 'DIFFERENT-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '散板状态同键异载荷返回冲突', forwardMismatch)
|
||||
const reversed = await context.requestOverseas('PATCH', statusPath,
|
||||
{ version: Number(forwardedSummary?.version), status: 'PENDING_ACCEPTANCE', remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
{ version: Number(forwardedSummary?.version), status: 'PENDING_ACCEPTANCE',
|
||||
idempotencyKey: `${context.marker}-looseboard-status-terminal-reverse`.slice(0, 255),
|
||||
remark: 'REVERSE-DO-NOT-APPLY' })
|
||||
expectedConflict(context, '散板状态逆向回退返回冲突', reversed)
|
||||
const history = await overseas(context, 'GET', '散板状态冲突后流水回查', path)
|
||||
const historySummary = summary(history.data)
|
||||
@@ -501,7 +550,9 @@ async function finalizePendingPurchase(context, resource) {
|
||||
const rejected = await overseas(context, 'POST', '驳回未完成的本轮采购订单',
|
||||
query(`${BASE}/overseas-procurements/${encodeURIComponent(resource.orderNo)}/review`,
|
||||
{ customerId: resource.customerId }),
|
||||
{ decision: 'REJECT', remark: `${context.marker}-prerequisite-cleanup` })
|
||||
{ version: Number(currentSummary?.version), decision: 'REJECT',
|
||||
idempotencyKey: `${context.marker}-procurement-review-cleanup`.slice(0, 255),
|
||||
remark: `${context.marker}-prerequisite-cleanup` })
|
||||
const rejectedSummary = summary(rejected.data)
|
||||
const verified = await overseas(context, 'GET', '回查未完成采购订单最终状态', path)
|
||||
const finalSummary = summary(verified.data)
|
||||
|
||||
Reference in New Issue
Block a user