fix(order): enforce versioned platform commands and submissions

This commit is contained in:
2026-10-02 23:14:24 +08:00
parent 25580dab86
commit c55ecabbfb
27 changed files with 540 additions and 74 deletions
+21 -6
View File
@@ -221,7 +221,7 @@ async function merchantBeforeApprovalModule() {
await apiStep('order-before-approval', 'submit is blocked before approval', {
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${orderId}/submit`, token: merchantToken,
headers: { 'Idempotency-Key': newIdempotencyKey('pre-approval-submit') },
...orderMutationCommand(await ownedOrderSnapshot('order-before-approval', 'freeze pre-approval submit version', orderId), {}, newIdempotencyKey('pre-approval-submit')),
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0403', 'business code')
@@ -1118,7 +1118,7 @@ async function orderLifecycleModule() {
report.resources.orderIds.push(draft.json.data)
await softApiStep('order-validation', `${testCase.name} is rejected on submit`, {
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${draft.json.data}/submit`, token: merchantToken,
headers: { 'Idempotency-Key': newIdempotencyKey('incomplete-submit') },
...orderMutationCommand(await ownedOrderSnapshot('order-validation', 'freeze incomplete submit version', draft.json.data), {}, newIdempotencyKey('incomplete-submit')),
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0422', 'business code')
@@ -1269,15 +1269,29 @@ async function orderLifecycleModule() {
}, ({ json }) => ({ total: json.data?.total, orderId, status: 'DRAFT' }))
const submitKey = newIdempotencyKey('approved-submit')
const submitBefore = await ownedOrderSnapshot('order-lifecycle', 'freeze submission page version', orderId)
const submitCommand = orderMutationCommand(submitBefore, {}, submitKey)
recordOrderMutationRequest('initial lifecycle submit', {method:'POST',path:`/api/merchant/v1/orders/first-leg/${orderId}/submit`,...submitCommand}, submitBefore)
await apiStep('order-lifecycle', 'submit order', {
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${orderId}/submit`, token: merchantToken,
headers: { 'Idempotency-Key': submitKey },
...submitCommand,
}, ({ json }) => requireSuccess(json), () => ({ orderId }))
const submittedBeforeReplay = await ownedOrderSnapshot('order-lifecycle', 'freeze successful submission facts', orderId)
await apiStep('order-lifecycle', 'submit order idempotent replay', {
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${orderId}/submit`, token: merchantToken,
headers: { 'Idempotency-Key': submitKey },
...submitCommand,
}, ({ json }) => requireSuccess(json), () => ({ orderId, replayed: true }))
await ownedOrderSnapshot('order-lifecycle', 'submit replay keeps version and history unchanged', orderId, submittedBeforeReplay)
const submitPath = `/api/merchant/v1/orders/first-leg/${orderId}/submit`
await rejectOrderMutation('submit same key different version conflicts', orderId, {
method: 'POST', path: submitPath, token: merchantToken, headers: submitCommand.headers,
body: { expectedVersion: submittedBeforeReplay.order.version },
}, ['A0409'])
await rejectOrderMutation('submit new key cannot submit already submitted order', orderId, {
method: 'POST', path: submitPath, token: merchantToken,
...orderMutationCommand(submittedBeforeReplay, {}, newIdempotencyKey('new-key-submit')),
}, ['A0409'])
await apiStep('order-lifecycle', 'submitted status is persisted', {
method: 'GET', path: `/api/merchant/v1/orders/first-leg/${orderId}`, token: merchantToken,
@@ -1392,7 +1406,7 @@ async function orderLifecycleModule() {
}, ({ json }) => ({ orderId: cancelOrderId, rejectionCode: json.code, message: json.message }))
await softApiStep('order-boundary', 'cancelled order cannot be submitted', {
method: 'POST', path: `/api/merchant/v1/orders/first-leg/${cancelOrderId}/submit`, token: merchantToken,
headers: { 'Idempotency-Key': newIdempotencyKey('cancelled-submit') },
...orderMutationCommand(await ownedOrderSnapshot('order-boundary', 'freeze cancelled submit version', cancelOrderId), {}, newIdempotencyKey('cancelled-submit')),
}, ({ json, status }) => {
assertEqual(status, 200, 'HTTP status')
assertEqual(String(json?.code), 'A0409', 'business code')
@@ -2290,7 +2304,8 @@ async function orderMutationEvidenceModule() {
...orderMutationCommand(cancelled, { reason }, newIdempotencyKey('new-key-repeat-cancel')),
}, ['A0409'])
await rejectOrderMutation('cancelled draft cannot be submitted', id, {
method: 'POST', path: `${base}/${id}/submit`, token: merchantToken, headers: { 'Idempotency-Key': newIdempotencyKey('cancelled-submit-proof') },
method: 'POST', path: `${base}/${id}/submit`, token: merchantToken,
...orderMutationCommand(cancelled, {}, newIdempotencyKey('cancelled-submit-proof')),
}, ['A0409'])
report.orderMutationLimitations = [
'Requires deployed Merchant edit/cancel versioned idempotent protocol; local contracts do not prove TEST deployment.',