Files

154 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

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)
})
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))
})
test('正式Long字符串版本贯穿采购正常履约', async () => {
const h = completeFixture(), request = h.context.request.bind(h.context)
h.context.request = async (...args) => {
const response = await request(...args)
const order = response.body?.data?.customerView?.order || response.body?.data?.order
if (order) order.version = String(order.version)
return response
}
assert.equal((await verifyProcurementSample(h.context, h.inputs)).status, '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)
})
}