fix(order): enforce versioned platform commands and submissions
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# Admin 测试环境 HTTP 模块
|
||||
|
||||
头程提交、平台编辑与取消按首次原始详情版本冻结请求。原键原载荷重放成功;同键异载荷、旧版本及新键重复操作分别核验拒绝后事实不变。Platform 编辑使用平台权限与显式客户范围,客户 Token 不得调用。
|
||||
`phase5-overseas-fulfillment.mjs` 的 `pickAll`、`saveReceiptAndComplete` 也供显式授权的 `../mixed-container-outbound-test-env.mjs` 留存混柜续跑入口复用;仍使用正式 PDA 拣货和签收文件绑定接口,不代替全模块验收。
|
||||
|
||||
总入口是 `../admin-test-env-acceptance.mjs`。每个业务模块使用独立的同名 `.mjs` 文件,避免多个对话同时修改总入口。
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
`phase5-root-fulfillment.mjs` 是显式 TEST 的 F40 两箱根/余量专项,通过总入口 `--modules phase5-root-fulfillment` 准备本轮资质客户;模块用正式下单、测量、国内 PDA 拆分装柜、船节点、海外 PDA 收货、拣货发车及文件签收链创建两柜真实来源。默认根先签,编程调用 `AcceptanceRun.context.f40SignatureOrder='CHILD_FIRST'` 切换子先签;分别核对第一片签收不得根完成、最后一片签收才根完成、真实签收时间、状态历史和重放零新增。
|
||||
|
||||
`AcceptanceRun.context.f40SignatureOrder='ROOT_SIGNED_REMAINDER_CANCELLED'` 单独验证合法末票余量取消:本轮新根通过 `orderStopAtApproved=true` 停在原始 `APPROVED`,只装运签收首箱;回查真实履约树及未装子单原始 `APPROVED`、入仓 `WAIT_SPLIT` 和一箱余量后,调用公开取消接口。`PROCESSING` 子单不得作为取消样本。根完成事件必须唯一发布,取消子单完成事件显式为零;原取消请求重放须 `A0409`,且根子版本、历史、摘要、履约树及完成 Outbox 零漂移。两种双签模式仍须两片实际签收,不能用取消替代第二签。
|
||||
`AcceptanceRun.context.f40SignatureOrder='ROOT_SIGNED_REMAINDER_CANCELLED'` 单独验证合法末票余量取消:本轮新根通过 `orderStopAtApproved=true` 停在原始 `APPROVED`,只装运签收首箱;回查真实履约树及未装子单原始 `APPROVED`、入仓 `WAIT_SPLIT` 和一箱余量后,调用公开取消接口。`PROCESSING` 子单不得作为取消样本。根完成事件必须唯一发布,取消子单完成事件显式为零;原键原版本原载荷取消重放成功;同键异载荷及新键重复取消返回 `A0409`,根子版本、历史、摘要、履约树及完成 Outbox 均不漂移。两种双签模式仍须两片实际签收,不能用取消替代第二签。
|
||||
|
||||
会写已授权测试环境,保留新客户、单据、文件及报告,临时海外 PDA 在 finally 停用;失败保留样本,不自动删除或重复执行部分已完成链。签收资料使用新 key 保存同文件时,按正式保存命令递增一次资料版本、新增一条版本历史及 `SAVE_DOCUMENTS` 审计;签收时间、状态、原完成记录、文件绑定和根子完成事件不得变化。同一新命令再用原 key、原请求重放,必须明确 `replayed=true` 且完整资料、派送事实、版本和审计历史零漂移。资料保存前后及原 key 重放响应保存在 `phase5RootFulfillment.receiptReplayProofs`,失败也保留已取得的快照。
|
||||
|
||||
|
||||
@@ -4,6 +4,20 @@ import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { transition } from './order-inbound-status-fixtures.mjs'
|
||||
|
||||
test('dynamic Platform cancellation freezes version and key while reject body stays unchanged',async()=>{
|
||||
const calls=[]
|
||||
const context={marker:'fixture',token:'admin',apiSuccess(_m,_n,r){assert.equal(r.body.code,'200');return r.body.data},
|
||||
async request(method,path,body,token,headers){calls.push({method,path,body,token,headers});return {http:200,body:{code:'200',data:{order:{version:9}}}}}}
|
||||
await transition(context,'merchant',{tenantId:'2'},'1','CANCELLED','cancel')
|
||||
const command=calls.find(row=>row.method==='POST')
|
||||
assert.equal(command.token,'admin');assert.equal(command.body.expectedVersion,9);assert.ok(command.headers['Idempotency-Key'])
|
||||
calls.length=0
|
||||
await transition(context,'merchant',{tenantId:'2'},'1','REJECTED','reject')
|
||||
assert.equal(calls.length,1);assert.deepEqual(calls[0].body,{reason:'fixture 保留 REJECTED 人工样本'})
|
||||
})
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'order-inbound-status-fixtures.mjs'), 'utf8')
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { findPricedOrderRouteAcrossOrigins } from './orders.mjs'
|
||||
import { findPricedOrderRouteAcrossOrigins, freezeOrderCommand } from './orders.mjs'
|
||||
|
||||
const MODULE = 'order-inbound-status-fixtures'
|
||||
const OPTIONS = { scenario: 'manual-order-inbound-status-matrix', deploymentBlocker: true }
|
||||
@@ -117,17 +117,21 @@ async function createOrder(context, token, fixture, target) {
|
||||
return orderId
|
||||
}
|
||||
|
||||
async function transition(context, token, fixture, orderId, target, action) {
|
||||
export async function transition(context, token, fixture, orderId, target, action) {
|
||||
const merchantPath = `/api/merchant/v1/orders/first-leg/${query(orderId)}`
|
||||
if (action === 'submit') {
|
||||
const response = await merchantRequest(context, 'POST', `${merchantPath}/submit`, undefined, token,
|
||||
{ 'Idempotency-Key': key(context, `${target}-submit`) })
|
||||
const detail = context.apiSuccess(MODULE, `${target} 冻结提交版本`, await merchantRequest(context, 'GET', merchantPath, undefined, token))
|
||||
const command = freezeOrderCommand(detail, key(context, `${target}-submit`))
|
||||
const response = await merchantRequest(context, 'POST', `${merchantPath}/submit`, command.body, token, command.headers)
|
||||
context.apiSuccess(MODULE, `${target} 样本提交订单`, response)
|
||||
return
|
||||
}
|
||||
const platformPath = `/api/platform/v1/orders/first-leg/${query(orderId)}/${action}?tenantId=${query(fixture.tenantId)}`
|
||||
const body = action === 'reject' || action === 'cancel' ? { reason: `${context.marker} 保留 ${target} 人工样本` } : undefined
|
||||
const response = await context.request('POST', platformPath, body)
|
||||
const command = action === 'cancel' ? freezeOrderCommand(context.apiSuccess(MODULE, `${target} 冻结取消版本`,
|
||||
await context.request('GET', `/api/platform/v1/orders/first-leg/${query(orderId)}?tenantId=${query(fixture.tenantId)}`)),
|
||||
key(context, `${target}-cancel`), body) : null
|
||||
const response = await context.request('POST', platformPath, command?.body ?? body, context.token, command?.headers ?? {})
|
||||
context.apiSuccess(MODULE, `${target} 样本执行 ${action}`, response)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,36 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType } from './orders.mjs'
|
||||
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit } from './orders.mjs'
|
||||
|
||||
test('order command freezes original detail version/key and never accepts an absent version',()=>{
|
||||
const detail={order:{version:3}},draft={customerRemark:'original'}
|
||||
const command=freezeOrderCommand(detail,'key',{draft})
|
||||
detail.order.version=8;draft.customerRemark='changed'
|
||||
assert.deepEqual(command,{body:{draft:{customerRemark:'original'},expectedVersion:3},headers:{'Idempotency-Key':'key'}})
|
||||
assert.ok(Object.isFrozen(command.body.draft))
|
||||
for(const version of [undefined,null,'3',-1])assert.throws(()=>freezeOrderCommand({order:{version}},'key'))
|
||||
})
|
||||
|
||||
test('Platform edit uses exact customer and admin identity; replay/stale/foreign writes preserve full facts',async()=>{
|
||||
let detail={order:{id:'10',tenantId:'20',status:'DRAFT',version:3,customerRemark:'marker'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]}
|
||||
let saved
|
||||
const writes=[]
|
||||
const context={marker:'marker',token:'admin',createdResources:{orders:{}},apiSuccess(_m,_n,r){assert.equal(r.body.code,'200');return r.body.data},
|
||||
assert(_m,name,_r,ok){assert.ok(ok,name)},async request(method,path,body,token,headers){
|
||||
assert.equal(path,'/api/platform/v1/orders/first-leg/10?tenantId=20')
|
||||
if(method==='GET')return {http:200,body:{code:'200',data:structuredClone(detail)}}
|
||||
writes.push({body,token,headers})
|
||||
let code='200'
|
||||
if(token!=='admin')code='A0403'
|
||||
else if(saved)code=saved.key===headers['Idempotency-Key']&&saved.payload===JSON.stringify(body)?'200':'A0409'
|
||||
else {assert.equal(body.expectedVersion,3);saved={key:headers['Idempotency-Key'],payload:JSON.stringify(body)};detail.order.version++;detail.order.customerRemark=body.draft.customerRemark;detail.statusHistory.push({action:'UPDATE'})}
|
||||
return {http:200,body:{code}}
|
||||
}}
|
||||
const result=await verifyPlatformDraftEdit(context,'20','10','merchant',{customerRemark:'marker'})
|
||||
assert.equal(result.order.version,4);assert.equal(writes.length,5)
|
||||
assert.deepEqual(writes[1],writes[2]);assert.equal(writes[0].token,'merchant')
|
||||
})
|
||||
|
||||
const source = readFileSync(new URL('orders.mjs', import.meta.url), 'utf8')
|
||||
|
||||
|
||||
@@ -34,6 +34,47 @@ function rotate(values, offset) {
|
||||
return [...values.slice(shift), ...values.slice(0, shift)]
|
||||
}
|
||||
function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) }
|
||||
export function freezeOrderCommand(detail, idempotencyKey, payload = {}) {
|
||||
const version = detail?.order?.version
|
||||
if (!Number.isSafeInteger(version) || version < 0 || !idempotencyKey) throw new Error('订单首次详情版本或幂等键缺失')
|
||||
const command = structuredClone({ body: { ...payload, expectedVersion: version }, headers: { 'Idempotency-Key': idempotencyKey } })
|
||||
const freeze = value => { if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value) } }
|
||||
freeze(command)
|
||||
return command
|
||||
}
|
||||
export async function verifyPlatformDraftEdit(context, tenantId, orderId, merchantToken, payload) {
|
||||
const path = `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}?tenantId=${encodeURIComponent(tenantId)}`
|
||||
const read = async label => context.apiSuccess(MODULE, label, await context.request('GET', path))
|
||||
const before = await read('Admin 编辑前冻结客户订单版本')
|
||||
if (id(before?.order?.tenantId) !== id(tenantId) || id(before?.order?.id) !== id(orderId) || before.order.status !== 'DRAFT') throw new Error('平台编辑客户/草稿身份不符')
|
||||
const editedPayload = structuredClone(payload)
|
||||
editedPayload.customerRemark = `${payload.customerRemark} 平台编辑验收`
|
||||
const command = freezeOrderCommand(before, key(context, 'platform-draft-edit'), { draft: editedPayload })
|
||||
context.createdResources.orders.platformEditCommand = command
|
||||
const send = (body, headers, token = context.token) => context.request('PUT', path, body, token, headers)
|
||||
const unauthorized = await send(command.body, command.headers, merchantToken)
|
||||
context.assert(MODULE, 'Client不得调用Platform编辑接口', unauthorized,
|
||||
[200,403].includes(unauthorized.http) && String(unauthorized.body?.code) === 'A0403', 'merchant-token/platform-route', CONTRACT_OPTIONS)
|
||||
const afterDenied = await read('平台编辑越权拒绝后回查')
|
||||
context.assert(MODULE, '越权平台编辑后完整事实不变', null, orderPersistenceFacts(before) === orderPersistenceFacts(afterDenied), 'no-drift', CONTRACT_OPTIONS)
|
||||
context.apiSuccess(MODULE, 'Admin按客户范围版本化编辑草稿', await send(command.body, command.headers))
|
||||
const edited = await read('平台编辑后权威详情')
|
||||
context.assert(MODULE, '平台编辑保持身份且版本与UPDATE历史递增', null,
|
||||
id(edited?.order?.id) === id(orderId) && id(edited.order.tenantId) === id(tenantId) && edited.order.status === 'DRAFT'
|
||||
&& edited.order.version > before.order.version && edited.order.customerRemark === editedPayload.customerRemark
|
||||
&& edited.statusHistory.length === before.statusHistory.length + 1, 'versioned-edit', CONTRACT_OPTIONS)
|
||||
context.apiSuccess(MODULE, 'Platform编辑原键原版本重放成功', await send(command.body, command.headers))
|
||||
for (const [label, body, headers] of [
|
||||
['Platform编辑原键异载荷冲突', {...command.body, draft:{...editedPayload,customerRemark:`${editedPayload.customerRemark}-conflict`}},command.headers],
|
||||
['Platform编辑旧页面版本冲突', command.body, {'Idempotency-Key':key(context,'platform-edit-stale')}],
|
||||
]) {
|
||||
const rejected = await send(body, headers)
|
||||
context.assert(MODULE, label, rejected, rejected.http === 200 && String(rejected.body?.code) === 'A0409', 'exact-conflict', CONTRACT_OPTIONS)
|
||||
}
|
||||
const stable = await read('Platform重放及冲突后完整事实')
|
||||
context.assert(MODULE, 'Platform重放与拒绝不重复写入', null, orderPersistenceFacts(stable) === orderPersistenceFacts(edited), 'order/cargo/parties/history', CONTRACT_OPTIONS)
|
||||
return stable
|
||||
}
|
||||
export function fixtureInboundCode(policy, marker) {
|
||||
if (policy?.mode === 'AUTO' && policy.manualEntryRequired === false) return {}
|
||||
if (policy?.mode !== 'LEGACY_MANUAL' || policy.manualEntryRequired !== true || !marker) {
|
||||
@@ -82,8 +123,8 @@ async function verifyCargoRequirementNegatives(context, payload, policy, merchan
|
||||
const orderId = id(created.body?.data)
|
||||
if (!orderId) throw new Error('negative draft missing order id')
|
||||
const before = await request(context, 'GET', `${base}/${orderId}`, undefined, merchantToken)
|
||||
const submitted = await request(context, 'POST', `${base}/${orderId}/submit`, undefined, merchantToken,
|
||||
{ 'Idempotency-Key': key(context, `cargo-negative-${index}-submit`) })
|
||||
const submitCommand = freezeOrderCommand(context.apiSuccess(MODULE, '负向提交冻结草稿版本', before), key(context, `cargo-negative-${index}-submit`))
|
||||
const submitted = await request(context, 'POST', `${base}/${orderId}/submit`, submitCommand.body, merchantToken, submitCommand.headers)
|
||||
const after = await request(context, 'GET', `${base}/${orderId}`, undefined, merchantToken)
|
||||
context.assert(MODULE, `${payload.destinationType}/${payload.fulfillmentType} ${item.name}提交拒绝且草稿零漂移`, submitted,
|
||||
submitted.http === 200 && String(submitted.body?.code) === 'A0422'
|
||||
@@ -805,7 +846,7 @@ export async function run(context) {
|
||||
|
||||
const clientDetailPath = `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}`
|
||||
const draftResponse = await request(context, 'GET', clientDetailPath, undefined, merchantToken)
|
||||
const draft = context.apiSuccess(MODULE, 'Client 查询隔离订单草稿', draftResponse)
|
||||
let draft = context.apiSuccess(MODULE, 'Client 查询隔离订单草稿', draftResponse)
|
||||
const orderNo = id(draft?.order?.orderNo)
|
||||
context.assert(MODULE, 'Client 草稿身份、marker 和状态正确', draftResponse,
|
||||
id(draft?.order?.id) === orderId && draft?.order?.status === 'DRAFT'
|
||||
@@ -813,6 +854,7 @@ export async function run(context) {
|
||||
`orderNo=${orderNo || 'missing'}, status=${draft?.order?.status ?? 'missing'}`)
|
||||
assertCargoContract(context, draftResponse, draft?.cargoes, payload)
|
||||
context.createdResources.orders.orderNo = orderNo
|
||||
draft = await verifyPlatformDraftEdit(context, tenantId, orderId, merchantToken, payload)
|
||||
|
||||
const createConflictPayload = {
|
||||
...payload,
|
||||
@@ -829,18 +871,25 @@ export async function run(context) {
|
||||
orderPersistenceFacts(draftAfterConflict) === orderPersistenceFacts(draft),
|
||||
`orderId=${orderId}, status=${draftAfterConflict?.order?.status ?? 'missing'}`, CONTRACT_OPTIONS)
|
||||
|
||||
const submitResponse = await requestTransportRetry(context, 'POST', `${clientDetailPath}/submit`, undefined,
|
||||
merchantToken, { 'Idempotency-Key': key(context, 'client-submit') })
|
||||
const submitCommand = freezeOrderCommand(draft, key(context, 'client-submit'))
|
||||
context.createdResources.orders.submitCommand = submitCommand
|
||||
const submitResponse = await requestTransportRetry(context, 'POST', `${clientDetailPath}/submit`, submitCommand.body,
|
||||
merchantToken, submitCommand.headers)
|
||||
context.apiSuccess(MODULE, 'Client 提交隔离订单', submitResponse, `orderId=${orderId}`)
|
||||
const submitReplayResponse = await request(context, 'POST', `${clientDetailPath}/submit`, undefined,
|
||||
merchantToken, { 'Idempotency-Key': key(context, 'client-submit') })
|
||||
const submitReplayResponse = await request(context, 'POST', `${clientDetailPath}/submit`, submitCommand.body,
|
||||
merchantToken, submitCommand.headers)
|
||||
context.apiSuccess(MODULE, 'Client 提交订单同键同载荷精确重放', submitReplayResponse, `orderId=${orderId}`)
|
||||
const submittedResponse = await request(context, 'GET', clientDetailPath, undefined, merchantToken)
|
||||
const submitted = context.apiSuccess(MODULE, 'Client 回查已提交订单', submittedResponse)
|
||||
context.assert(MODULE, 'Client 订单已进入待审核状态', submittedResponse,
|
||||
submitted?.order?.status === 'SUBMITTED', `status=${submitted?.order?.status ?? 'missing'}`)
|
||||
const duplicateSubmitResponse = await request(context, 'POST', `${clientDetailPath}/submit`, undefined,
|
||||
merchantToken, { 'Idempotency-Key': key(context, 'client-submit-new-key') })
|
||||
const conflictSubmitResponse = await request(context, 'POST', `${clientDetailPath}/submit`, { expectedVersion: submitted.order.version },
|
||||
merchantToken, submitCommand.headers)
|
||||
context.assert(MODULE, '提交同键异版本返回A0409', conflictSubmitResponse,
|
||||
conflictSubmitResponse.http === 200 && String(conflictSubmitResponse.body?.code) === 'A0409', 'frozen-key/different-payload', CONTRACT_OPTIONS)
|
||||
const duplicateCommand = freezeOrderCommand(submitted, key(context, 'client-submit-new-key'))
|
||||
const duplicateSubmitResponse = await request(context, 'POST', `${clientDetailPath}/submit`, duplicateCommand.body,
|
||||
merchantToken, duplicateCommand.headers)
|
||||
context.assert(MODULE, '已提交订单使用新键重复提交返回A0409', duplicateSubmitResponse,
|
||||
duplicateSubmitResponse.http === 200 && String(duplicateSubmitResponse.body?.code) === 'A0409',
|
||||
`http=${duplicateSubmitResponse.http}, code=${duplicateSubmitResponse.body?.code ?? 'missing'}`, CONTRACT_OPTIONS)
|
||||
|
||||
@@ -323,12 +323,16 @@ function cancellationFixture(change=null) {
|
||||
return cancellationOutbox()
|
||||
}}
|
||||
if(change)change({rootDetail,childDetail,childInbound,tree})
|
||||
context.request=async(method,path,body)=>{
|
||||
calls.push({method,path,body})
|
||||
let frozenCancel
|
||||
context.request=async(method,path,body,token,headers)=>{
|
||||
calls.push({method,path,body,headers})
|
||||
if(method==='POST'){
|
||||
assert.equal(path,'/api/platform/v1/orders/first-leg/101/cancel?tenantId=90')
|
||||
assert.ok(body.reason.includes('offline-only'))
|
||||
if(count++)return {http:200,body:{code:'A0409'}}
|
||||
assert.ok(headers?.['Idempotency-Key'])
|
||||
if(count++)return {http:200,body:{code:headers['Idempotency-Key']===frozenCancel.key && JSON.stringify(body)===frozenCancel.payload ? '200' : 'A0409'}}
|
||||
assert.equal(body.expectedVersion,childDetail.order.version)
|
||||
frozenCancel={key:headers['Idempotency-Key'],payload:JSON.stringify(body)}
|
||||
childDetail.order.status='CANCELLED';childDetail.order.version++
|
||||
childDetail.statusHistory=history('CANCELLED')
|
||||
rootDetail.order.status='COMPLETED';rootDetail.order.version++
|
||||
@@ -352,7 +356,13 @@ test('formal cancellation helper rereads physical eligibility, uses public cance
|
||||
const {context,proof,calls}=cancellationFixture()
|
||||
const result=await cancelUnloadedRemainder(context,proof,'90','100','101','300','301')
|
||||
assert.equal(validRemainderCancellationGate(result),true)
|
||||
assert.equal(calls.filter(row=>row.method==='POST').length,2)
|
||||
const writes=calls.filter(row=>row.method==='POST')
|
||||
assert.equal(writes.length,4)
|
||||
assert.deepEqual(writes[1].body,writes[0].body)
|
||||
assert.deepEqual(writes[1].headers,writes[0].headers)
|
||||
assert.equal(writes[2].headers['Idempotency-Key'],writes[0].headers['Idempotency-Key'])
|
||||
assert.notEqual(writes[3].headers['Idempotency-Key'],writes[0].headers['Idempotency-Key'])
|
||||
assert.ok(writes[3].body.expectedVersion>writes[0].body.expectedVersion)
|
||||
assert.equal(proof.snapshots.length,3)
|
||||
assert.deepEqual(proof.cancelReplayOutbox.before,proof.cancelReplayOutbox.after)
|
||||
assert.equal(context.steps.every(step=>step.passed),true)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loadOneFragment } from './phase3-order-splitting.mjs'
|
||||
import { freezeOrderCommand } from './orders.mjs'
|
||||
import { verifyPalletLabels } from './phase3-loading-finalize.mjs'
|
||||
import { stableLoadedCartonMarks } from './phase3-loading.mjs'
|
||||
import { awaitProducerConsumption, convertReceivingContainers, runReceiving, resolveSharedWarehouse,
|
||||
@@ -247,9 +248,12 @@ export async function cancelUnloadedRemainder(context, proof, customerId, rootOr
|
||||
'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} 未装柜余量取消验收`}
|
||||
const cancelCommand = freezeOrderCommand({order:before.child.rawOrder}, `${context.context.f40BaseMarker}-cancel-remainder`.slice(0,128),
|
||||
{reason:`${context.context.f40BaseMarker} 未装柜余量取消验收`})
|
||||
proof.cancelCommand = cancelCommand
|
||||
const cancelBody = cancelCommand.body
|
||||
proof.currentStage = 'REMAINDER_CANCELLATION'
|
||||
await api(context,'POST','正式取消未装柜 APPROVED 余量订单',cancelPath,cancelBody)
|
||||
await api(context,'POST','正式取消未装柜 APPROVED 余量订单',cancelPath,cancelBody,context.token,cancelCommand.headers)
|
||||
const after = await waitState(() => state(context,customerId,rootOrderId,childOrderId,rootInboundId),
|
||||
validRemainderCancellationGate)
|
||||
check(context,'F40 最后有效未装余量取消后根仅完成一次且子未签收',validRemainderCancellationGate(after),
|
||||
@@ -268,9 +272,13 @@ export async function cancelUnloadedRemainder(context, proof, customerId, rootOr
|
||||
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 replay = await context.request('POST',cancelPath,cancelBody,context.token,cancelCommand.headers)
|
||||
check(context,'F40 原键原版本取消重放成功',success(replay),'cancel-original-key-replay',replay)
|
||||
const conflict = await context.request('POST',cancelPath,{...cancelBody,reason:`${cancelBody.reason}-conflict`},context.token,cancelCommand.headers)
|
||||
check(context,'F40 原键不同取消载荷返回 A0409',conflict?.http === 200 && String(conflict?.body?.code) === 'A0409','cancel-payload-conflict',conflict)
|
||||
const duplicate = freezeOrderCommand({order:after.child.rawOrder}, `${context.context.f40BaseMarker}-cancel-new-key`.slice(0,128), {reason:cancelBody.reason})
|
||||
const rejected = await context.request('POST',cancelPath,duplicate.body,context.token,duplicate.headers)
|
||||
check(context,'F40 新键重复取消返回 A0409',rejected?.http === 200 && String(rejected?.body?.code) === 'A0409','cancel-new-key-conflict',rejected)
|
||||
const stable = await state(context,customerId,rootOrderId,childOrderId,rootInboundId)
|
||||
const replayTree = await api(context,'GET','取消重放后回查原履约树',treePath)
|
||||
check(context,'F40 取消重放订单版本历史根摘要及履约台账零变化',
|
||||
|
||||
Reference in New Issue
Block a user