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') } })