2026-10-10 02:19:38 +08:00
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { verifyProcurementSample } from './procurement-sample-fulfillment.mjs'
|
|
|
|
|
|
|
|
|
|
function fixture() {
|
|
|
|
|
const events = []
|
|
|
|
|
const context = { marker: 'PROC-TEST', token: 'admin-token', createdResources: {},
|
|
|
|
|
async checkpoint() { events.push('checkpoint') },
|
|
|
|
|
async request(method, path) { events.push(`${method} ${path}`); throw new Error('network outcome unknown') } }
|
|
|
|
|
const inputs = { marker: context.marker, customerId: '101', warehouseId: '201', customerName: 'QA', customerToken: 'customer-token',
|
|
|
|
|
async upload() { throw new Error('must not upload yet') },
|
|
|
|
|
async readWallet() { return { actualBalance: '10.00', availableBalance: '10.00', frozenBalance: '0.00', creditUsed: '0.00' } } }
|
|
|
|
|
return { context, inputs, events }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('missing owned fixture is rejected before any HTTP', async () => {
|
|
|
|
|
const { context, inputs, events } = fixture()
|
|
|
|
|
inputs.marker = 'other-run'
|
|
|
|
|
await assert.rejects(verifyProcurementSample(context, inputs), /本轮独立客户/)
|
|
|
|
|
assert.deepEqual(events, [])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('insufficient independent cash stops before creating an order', async () => {
|
|
|
|
|
const { context, inputs, events } = fixture()
|
|
|
|
|
inputs.readWallet = async () => ({ actualBalance: '1.00', availableBalance: '1.00', frozenBalance: '0.00', creditUsed: '0.00' })
|
|
|
|
|
await assert.rejects(verifyProcurementSample(context, inputs), /现金余额/)
|
|
|
|
|
assert.equal(events.some(event => event.startsWith('POST')), false)
|
|
|
|
|
assert.equal(context.createdResources.procurementSample.status, 'FAILED')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('unknown create outcome is persisted and never automatically retried', async () => {
|
|
|
|
|
const { context, inputs, events } = fixture()
|
|
|
|
|
await assert.rejects(verifyProcurementSample(context, inputs), /network outcome unknown/)
|
|
|
|
|
const proof = context.createdResources.procurementSample
|
|
|
|
|
assert.equal(proof.requests.length, 1)
|
|
|
|
|
assert.equal(proof.requests[0].status, 'UNKNOWN_RESULT')
|
|
|
|
|
assert.ok(proof.requests[0].body.idempotencyKey)
|
|
|
|
|
assert.equal(events.filter(event => event.startsWith('POST')).length, 1)
|
|
|
|
|
const write = events.findIndex(event => event.startsWith('POST'))
|
|
|
|
|
assert.equal(events[write - 1], 'checkpoint')
|
|
|
|
|
assert.equal(JSON.stringify(proof).includes('customer-token'), false)
|
|
|
|
|
await assert.rejects(verifyProcurementSample(context, inputs), /不自动重跑/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('failure to persist the request prevents network dispatch', async () => {
|
|
|
|
|
const { context, inputs, events } = fixture()
|
|
|
|
|
context.checkpoint = async () => { throw new Error('disk write failed') }
|
|
|
|
|
await assert.rejects(verifyProcurementSample(context, inputs), /disk write failed/)
|
|
|
|
|
assert.equal(events.length, 0)
|
|
|
|
|
})
|
2026-10-22 15:41:50 +08:00
|
|
|
|
|
|
|
|
function completeFixture(fault = '') {
|
|
|
|
|
const { context, inputs } = fixture()
|
|
|
|
|
const calls = [], seen = new Map()
|
|
|
|
|
let spent = 0, frozen = 0, creations = 0
|
|
|
|
|
const order = { orderNo: 'CG-TEST-1', customerId: '101', status: 'PENDING_REVIEW', version: 0, updatedAt: '2026-10-22T12:00:00' }
|
|
|
|
|
const fees = [], physical = { receipts: [], cartons: [], packingVersion: 0 }
|
|
|
|
|
const transport = { overseasLabels: { version: 0 }, domesticLabels: { version: 0 }, shipments: [] }
|
|
|
|
|
const detail = () => structuredClone({ customerView: { order, fees }, physical, transport, fees })
|
|
|
|
|
inputs.upload = async () => ({ fileId: '501' })
|
|
|
|
|
inputs.readWallet = async () => ({ actualBalance: ((1000 - spent) / 100).toFixed(2), availableBalance: ((1000 - spent - frozen) / 100).toFixed(2),
|
|
|
|
|
frozenBalance: (frozen / 100).toFixed(2), creditUsed: '0.00' })
|
|
|
|
|
context.request = async (method, path, body) => {
|
|
|
|
|
calls.push({ method, path, body: structuredClone(body) })
|
|
|
|
|
const reply = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'req-test', traceId: 'trace-test' })
|
|
|
|
|
if (method === 'GET') {
|
|
|
|
|
if (path.startsWith('/api/merchant/')) return reply({ order: { ...order, ...(fault === 'customer final order' ? { orderNo: 'OTHER' } : {}) } })
|
|
|
|
|
const result = detail()
|
|
|
|
|
if (fault === 'aggregate customer') result.customerView.order.customerId = '999'
|
|
|
|
|
if (fault === 'aggregate order') result.customerView.order.orderNo = 'OTHER'
|
|
|
|
|
if (fault === 'aggregate version') result.customerView.order.version = null
|
|
|
|
|
return reply(result)
|
|
|
|
|
}
|
|
|
|
|
if (path === '/api/merchant/v1/last-leg/overseas-procurements') {
|
|
|
|
|
creations++
|
|
|
|
|
return reply({ order: { ...order, ...(creations === 2 && fault === 'create replay customer' ? { customerId: '999' } : {}) } })
|
|
|
|
|
}
|
|
|
|
|
assert.equal(method, 'POST')
|
|
|
|
|
const action = path.split('/CG-TEST-1/')[1]?.split('?')[0]
|
|
|
|
|
assert.ok(action, path)
|
|
|
|
|
if (seen.has(body.idempotencyKey)) {
|
|
|
|
|
assert.deepEqual(body, seen.get(body.idempotencyKey))
|
|
|
|
|
if (fault === 'replay deducts' && action === 'purchase') spent += 100
|
|
|
|
|
return reply(detail())
|
|
|
|
|
}
|
|
|
|
|
assert.equal(body.version, order.version)
|
|
|
|
|
seen.set(body.idempotencyKey, structuredClone(body))
|
|
|
|
|
switch (action) {
|
|
|
|
|
case 'review': order.status = 'APPROVED'; break
|
|
|
|
|
case 'purchase':
|
|
|
|
|
spent += 100
|
|
|
|
|
fees.push({ active: true, purpose: 'SAMPLE_PAYMENT', walletStatus: 'SETTLED', sourceLinked: true, walletBusinessNo: 'PAY-1' })
|
|
|
|
|
break
|
|
|
|
|
case 'overseas-receipts': case 'domestic-receipts':
|
|
|
|
|
physical.receipts.push({ id: String(601 + physical.receipts.length), leg: body.leg, active: true, goodQuantity: body.goodQuantity }); break
|
|
|
|
|
case 'overseas-receipts/complete': case 'domestic-receipts/complete': break
|
|
|
|
|
case 'packing':
|
|
|
|
|
physical.cartons = body.cartons.map((row, index) => ({ ...row, id: String(701 + index) })); physical.packingVersion++; break
|
|
|
|
|
case 'packing/confirm': assert.equal(body.packingVersion, physical.packingVersion); break
|
|
|
|
|
case 'labels': transport[body.leg === 'OVERSEAS' ? 'overseasLabels' : 'domesticLabels'].version++; break
|
|
|
|
|
case 'international-estimate':
|
|
|
|
|
frozen = 200; fees.push({ active: true, purpose: 'INTERNATIONAL_ESTIMATE', walletStatus: 'FROZEN' }); break
|
|
|
|
|
case 'labels/apply': break
|
|
|
|
|
case 'overseas-shipment': case 'customer-shipment':
|
|
|
|
|
transport.shipments.push({ leg: body.leg })
|
|
|
|
|
if (action === 'customer-shipment') order.status = 'SHIPPED_TO_CUSTOMER'
|
|
|
|
|
break
|
|
|
|
|
case 'charges/confirm':
|
|
|
|
|
spent += 150; frozen = 0
|
|
|
|
|
fees.find(row => row.purpose === 'INTERNATIONAL_ESTIMATE').walletStatus = 'RELEASED'
|
|
|
|
|
fees.push({ active: true, purpose: 'INTERNATIONAL_FREIGHT', walletStatus: 'SETTLED', sourceLinked: true, walletBusinessNo: 'PAY-2' })
|
|
|
|
|
break
|
|
|
|
|
default: assert.fail(`Unexpected operation ${action}`)
|
|
|
|
|
}
|
|
|
|
|
order.version++
|
|
|
|
|
return reply(detail())
|
|
|
|
|
}
|
|
|
|
|
return { context, inputs, calls, seen, physical, transport }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('sample procurement traverses receipt, packing, both shipments and exact wallet settlement with same-key replays', async () => {
|
|
|
|
|
const h = completeFixture()
|
|
|
|
|
const proof = await verifyProcurementSample(h.context, h.inputs)
|
|
|
|
|
assert.equal(proof.status, 'PASSED')
|
|
|
|
|
assert.equal(proof.settledAmountCny, '2.50')
|
|
|
|
|
assert.deepEqual(proof.walletBusinessNos, ['PAY-1', 'PAY-2'])
|
|
|
|
|
assert.equal(h.physical.receipts.length, 4)
|
|
|
|
|
assert.equal(h.physical.cartons.length, 2)
|
|
|
|
|
assert.deepEqual(h.transport.shipments.map(row => row.leg), ['OVERSEAS', 'DOMESTIC'])
|
|
|
|
|
assert.equal(proof.walletChecks.length, 4)
|
|
|
|
|
assert.ok(proof.checks.every(check => check.passed))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const fault of ['aggregate customer', 'aggregate order', 'aggregate version', 'create replay customer', 'customer final order', 'replay deducts']) {
|
|
|
|
|
test(`procurement stops rather than passing ${fault}`, async () => {
|
|
|
|
|
const h = completeFixture(fault)
|
|
|
|
|
await assert.rejects(verifyProcurementSample(h.context, h.inputs))
|
|
|
|
|
assert.equal(h.context.createdResources.procurementSample.status, 'FAILED')
|
|
|
|
|
if (fault.startsWith('aggregate') || fault === 'create replay customer') assert.equal(h.seen.size, 0)
|
|
|
|
|
if (fault === 'replay deducts') assert.equal(h.physical.receipts.length, 0)
|
|
|
|
|
})
|
|
|
|
|
}
|