test(order): verify qualification and isolated negative balance gates

This commit is contained in:
2026-10-03 00:27:45 +08:00
parent c0c6e21833
commit 9803dcfd2c
2 changed files with 232 additions and 9 deletions
@@ -28,6 +28,78 @@ function runtime(extra = {}) {
}
const plain = value => JSON.parse(JSON.stringify(value))
function balanceGateHarness(fault='') {
const calls=[],ledger=[],report={resources:{tenantId:'20',orderIds:[]},steps:[{module:'registration',name:'enterprise self-registration',passed:true,evidence:{tenantId:'20'}}]}
const wallet={currencyCode:'CNY',version:0,creditActive:false,actualBalance:0,availableBalance:0,frozenBalance:0,
creditLimit:0,creditUsed:0,creditFrozen:0,availableCredit:0}
const detail={order:{id:'42',tenantId:'20',customerId:'20',orderNo:'FL42',status:'DRAFT',version:3},cargoes:[{skuNo:'SKU'}],parties:[],internalNotes:[],statusHistory:[]}
let sequence=0,task=null
if(fault==='other-customer')report.steps[0].evidence.tenantId='21'
if(fault==='funded-account')wallet.actualBalance=1
const r=runtime({report,marker:'isolated-fixture',merchantToken:'owner',platformToken:'platform',newIdempotencyKey:()=>`key-${++sequence}`,
orderAcceptanceFixture:{payload:{cargoes:[{skuNo:'SKU'}]}},
apiStep:async(_module,name,request,validate)=>{
calls.push({name,request:structuredClone(request)})
let data=null,code='200',message='success'
if(request.path.endsWith('/tenant/profile'))data={id:'20',status:'ACTIVE'}
else if(request.path==='/api/client/finance/v1/account')data=structuredClone(wallet)
else if(request.path.includes('/account/ledger'))data={total:ledger.length,records:structuredClone(ledger)}
else if(request.path.endsWith('/adjustments')) {
assert.equal(request.path,'/api/platform/finance/v1/accounts/20/adjustments')
assert.equal(request.token,'platform');assert.ok(request.headers['Idempotency-Key'])
assert.equal(request.body.amount,0.01);assert.equal(request.body.expectedAccountVersion,wallet.version)
const deduct=request.body.type==='DEDUCTION'
if(deduct&&fault==='deduction-not-applied')throw new Error('not applied')
wallet.actualBalance+=deduct?-0.01:0.01;wallet.availableBalance=wallet.actualBalance;wallet.version++
ledger.push({sourceType:deduct?'MANUAL_DEDUCTION':'MANUAL_RECHARGE',referenceNo:request.body.referenceNo,amount:deduct?-0.01:0.01})
if(deduct&&fault==='lost-deduction-response')throw new Error('response lost')
data=ledger.at(-1)
} else if(request.method==='POST'&&request.path==='/api/merchant/v1/orders/first-leg')data='42'
else if(request.path.includes('/submit')) {
assert.equal(wallet.actualBalance,-0.01)
assert.equal(request.body.expectedVersion,3)
assert.ok(request.headers['Idempotency-Key'])
code='A0403';message=fault==='wrong-gate'?'企业未审核':'账户实际余额为负,请充值后再下单'
if(fault==='order-drift')detail.order.version++
if(fault==='task-drift')task={orderId:'42',tenantId:'20',id:'unexpected'}
} else if(request.path.includes('/tasks/by-order/'))data=structuredClone(task)
else if(request.path.includes('/inbounds?'))data={page:1,total:0,records:[]}
else if(request.path==='/api/merchant/v1/orders/first-leg/42')data=structuredClone(detail)
else throw new Error(`unhandled offline request ${request.path}`)
const response={status:200,json:{code,message,data}};validate(response);return response
},
})
return {r,calls,report,wallet,ledger}
}
test('negative actual funds fixture touches only the new registered customer and restores an audited cent',async()=>{
const {r,calls,report,wallet,ledger}=balanceGateHarness()
await r.orderSubmissionBalanceGateModule()
assert.equal(wallet.actualBalance,0);assert.equal(wallet.version,2);assert.equal(ledger.length,2)
assert.equal(report.orderSubmissionGates.balance.status,'REJECTIONS_VERIFIED')
assert.equal(report.orderSubmissionGates.balance.restoration,'RESTORED_WITH_AUDIT')
assert.equal(calls.filter(call=>call.request.path.includes('/submit')).length,2)
assert.equal(calls.filter(call=>call.request.path.endsWith('/adjustments')).length,2)
assert.ok(calls.every(call=>!call.request.path.includes('channels')&&!call.request.path.includes('price-books')))
})
for(const fault of ['other-customer','funded-account'])test(`financial fixture refuses unsafe scope: ${fault}`,async()=>{
const {r,calls}=balanceGateHarness(fault)
await assert.rejects(r.orderSubmissionBalanceGateModule())
assert.equal(calls.some(call=>call.request.method==='POST'),false)
})
for(const fault of ['wrong-gate','order-drift','task-drift','lost-deduction-response'])test(`gate evidence fails and restores its proven deduction after ${fault}`,async()=>{
const {r,wallet,ledger}=balanceGateHarness(fault)
await assert.rejects(r.orderSubmissionBalanceGateModule())
assert.equal(wallet.actualBalance,0);assert.equal(ledger.length,2)
})
test('unapplied deduction does not trigger a blind restoring credit',async()=>{
const {r,wallet,ledger,calls}=balanceGateHarness('deduction-not-applied')
await assert.rejects(r.orderSubmissionBalanceGateModule(),/not applied/)
assert.equal(wallet.actualBalance,0);assert.equal(ledger.length,0)
assert.equal(calls.filter(call=>call.request.path.endsWith('/adjustments')).length,1)
})
test('frozen edit command preserves original version, key and nested payload through retries', () => {
const r = runtime(), snapshot = { order: { version: 7 } }, payload = { draft: { cargoes: [{ skuNo: 'ORIGINAL' }] } }
const command = r.orderMutationCommand(snapshot, payload, 'original-key')