fix(order): enforce versioned platform commands and submissions
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user