test(order): verify qualification and isolated negative balance gates
This commit is contained in:
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user