Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-trip-finance-acceptance-contract.test.mjs
2026-10-20 10:35:52 +08:00

123 lines
7.8 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { readFileSync } from 'node:fs'
import { moneyCents, validateTripFinance, verifyFleetTripFinance } from './fleet-trip-finance-acceptance.mjs'
const scope = { tripNo: 'TR-CURRENT', dispatchNos: ['DS-CURRENT'], receiptsVisible: true,
businesses: [{ customerId: '9007199254740993', deliveryNo: 'DL-CURRENT' }] }
test('default fulfillment runs finance verification using its own departed trip and delivery numbers', () => {
const source = readFileSync(new URL('./phase5-overseas-fulfillment.mjs', import.meta.url), 'utf8')
assert.match(source, /import \{ verifyFleetTripFinance \} from '\.\/fleet-trip-finance-acceptance\.mjs'/)
assert.match(source, /verifyFleetTripFinance\(context, MODULE, \{ tripNo: tripProof\.tripNo, dispatchNos: \[dispatchNo\]/)
assert.match(source, /businesses: deliveries\.map\(row => \(\{ customerId: id\(row\.customerId\), deliveryNo: row\.deliveryNo \}\)\)/)
})
function fixture(status = 'AVAILABLE', received = null) {
return { tripNo: scope.tripNo, dispatchNos: [...scope.dispatchNos], receiptsVisible: true,
attributionStatus: 'ATTRIBUTED', attributionReason: '唯一发车来源', finance: {
status: 'AVAILABLE', reason: '有效财务来源',
sources: [{ customerId: scope.businesses[0].customerId, businessNo: 'DL-CURRENT', sourceNo: 'FS-CURRENT',
sourceVersion: 1, status, currency: 'USD', amount: '100.03' }],
totals: [{ currency: 'USD', receivableAmount: '100.03', receivedAmount: received }],
documents: received === null ? [] : [{ billNo: 'BILL-CURRENT', currency: 'USD', receivedAmount: received,
allocationStatus: 'ATTRIBUTED', reason: '已确认收款分配' }],
} }
}
test('decimal amounts use integer cents and refuse unknown or unsafe precision', () => {
assert.equal(moneyCents('100.03'), 10003n)
assert.equal(moneyCents('9007199254740993.00'), 900719925474099300n)
for (const value of [null, undefined, '', -1, NaN, Infinity, '1.005', '1e3', 9007199254740993]) {
assert.throws(() => moneyCents(value))
}
})
test('available, billed and settled sources survive partial and full payment readback', () => {
for (const [state, paid] of [['AVAILABLE', null], ['BILLED', '30.01'], ['SETTLED', '100.03']]) {
assert.equal(validateTripFinance(fixture(state, paid), scope), true)
}
})
test('currencies are reconciled separately without floating point addition', () => {
const value = fixture()
value.finance.sources.push({ ...value.finance.sources[0], sourceNo: 'FS-CNY', currency: 'CNY', amount: '0.20' })
value.finance.totals.push({ currency: 'CNY', receivableAmount: '0.20', receivedAmount: null })
assert.equal(validateTripFinance(value, scope), true)
value.finance.totals[0].receivableAmount = '100.23'
assert.throws(() => validateTripFinance(value, scope), /原币/)
})
test('one dispatch retains each delivery customer in whole-trip finance attribution', () => {
const value = fixture()
const second = { customerId: '9007199254740994', deliveryNo: 'DL-SECOND' }
const mixed = { ...scope, businesses: [...scope.businesses, second] }
value.finance.sources.push({ ...value.finance.sources[0], customerId: second.customerId,
businessNo: second.deliveryNo, sourceNo: 'FS-SECOND', amount: '20.07' })
value.finance.totals[0].receivableAmount = '120.10'
assert.equal(validateTripFinance(value, mixed), true)
assert.throws(() => validateTripFinance(value, scope), /其他客户或派送/)
value.finance.sources[1].customerId = scope.businesses[0].customerId
assert.throws(() => validateTripFinance(value, mixed), /其他客户或派送/)
})
test('pending allocation is explicitly unknown and never promoted to complete or zero', () => {
const value = fixture('BILLED')
value.finance.documents = [{ billNo: 'MIXED-BILL', currency: 'USD', receivedAmount: null,
allocationStatus: 'PENDING_ALLOCATION', reason: '账单含本车次以外来源' }]
assert.equal(validateTripFinance(value, scope), true)
value.finance.totals[0].receivedAmount = 0
assert.throws(() => validateTripFinance(value, scope), /完整实收/)
const empty = fixture()
Object.assign(empty.finance, { sources: [], totals: [], documents: [], status: 'PENDING_ALLOCATION' })
assert.equal(validateTripFinance(empty, scope), true)
empty.finance.status = 'AVAILABLE'
assert.throws(() => validateTripFinance(empty, scope))
})
test('receipt restricted projection cannot leak totals or documents', () => {
const value = fixture('BILLED'); value.receiptsVisible = false
const restricted = { ...scope, receiptsVisible: false }
assert.equal(validateTripFinance(value, restricted), true)
value.finance.totals[0].receivedAmount = '30.01'
assert.throws(() => validateTripFinance(value, restricted), /泄露金额/)
value.finance.totals[0].receivedAmount = null
value.finance.documents = fixture('BILLED', '30.01').finance.documents
assert.throws(() => validateTripFinance(value, restricted), /泄露收款/)
})
test('NON_NULL省略未知实收仍保持未知,明确归属的实收和应收不得缺失', () => {
const withoutNulls = value => JSON.parse(JSON.stringify(value, (_key, item) => item === null ? undefined : item))
assert.equal(validateTripFinance(withoutNulls(fixture()), scope), true)
const restricted = withoutNulls(fixture()); restricted.receiptsVisible = false
assert.equal(validateTripFinance(restricted, { ...scope, receiptsVisible: false }), true)
const pending = fixture('BILLED')
pending.finance.documents = [{ billNo: 'BILL-MIXED', currency: 'USD', receivedAmount: null, allocationStatus: 'PENDING_ALLOCATION', reason: '归属未完成' }]
assert.equal(validateTripFinance(withoutNulls(pending), scope), true)
const missingPaid = fixture('BILLED', '10.00'); delete missingPaid.finance.documents[0].receivedAmount
assert.throws(() => validateTripFinance(missingPaid, scope))
const missingAmount = fixture(); delete missingAmount.finance.sources[0].amount
assert.throws(() => validateTripFinance(missingAmount, scope))
})
test('wrong customer, trip, duplicate sources and invented receipts all reject', () => {
const mutations = [v => { v.tripNo = 'TR-FOREIGN' }, v => { v.dispatchNos = ['DS-FOREIGN'] },
v => { v.finance.sources[0].customerId = '9007199254740994' },
v => { v.finance.sources[0].businessNo = 'DL-FOREIGN' },
v => { v.finance.sources.push({ ...v.finance.sources[0] }) },
v => { v.finance.sources[0].status = 'VOIDED' }, v => { v.finance.sources[0].sourceVersion = 0 },
v => { v.finance.totals[0].receivedAmount = 25 },
v => { v.finance.totals.push({ ...v.finance.totals[0] }) }]
for (const mutate of mutations) { const value = fixture(); mutate(value); assert.throws(() => validateTripFinance(value, scope)) }
})
test('read-only orchestration retains proof and detects execution state mutation or HTTP failure', async () => {
for (const fault of ['', 'mutation', 'http']) {
const calls = [], checks = []; let reads = 0
const context = { marker: 'CURRENT', createdResources: {},
assert: (_m, _n, _r, passed) => checks.push(passed), apiSuccess: (_m, _n, r) => r.body.data,
request: async (method, path) => {
calls.push({ method, path })
if (path.endsWith('/finance')) return { http: fault === 'http' ? 503 : 200, body: { code: '200', data: fixture() } }
return { http: 200, body: { code: '200', data: { trip: { tripNo: scope.tripNo,
departedAt: '2026-10-05T00:00:00Z', recordVersion: ++reads === 2 && fault === 'mutation' ? 2 : 1 } } } }
} }
if (fault) await assert.rejects(verifyFleetTripFinance(context, 'offline', scope))
else assert.equal((await verifyFleetTripFinance(context, 'offline', scope)).status, 'READ_ONLY_ATTRIBUTION_VERIFIED')
assert.ok(calls.every(row => row.method === 'GET'))
assert.equal(context.createdResources.fleetTripFinance[0].status,
fault ? 'CHECKING' : 'READ_ONLY_ATTRIBUTION_VERIFIED')
}
})