Files
baiyue-firstleg/scripts/admin-test-env-modules/fleet-trip-cost-acceptance-contract.test.mjs
T

136 lines
9.7 KiB
JavaScript

import assert from 'node:assert/strict'
import test from 'node:test'
import { verifyFleetTripCosts, expectedAllocations, TRIP_COST_EXPECTATIONS } from './fleet-trip-cost-acceptance.mjs'
const copy = value => value === undefined ? undefined : structuredClone(value)
const prefix = 'platform:fleet:trip:'
function fixture(fault) {
const trip = { marker: 'fixture-vehicle', tripNo: 'FT-CURRENT' }, calls = [], rows = [], histories = new Map(), keys = new Map()
const shares = [
{ businessNo: 'DP-OWN', dispatchNo: 'DP-OWN', sourceType: 'OWN', palletSpaces: 2, warehousePalletIds: ['P1'] },
{ businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', sourceType: 'EXTERNAL', palletSpaces: 1, warehousePalletIds: ['P2'] },
]
const inputs = { shares, listOnlyToken: 'offline-reader', deniedToken: 'offline-denied' }
const depart = { recordVersion: 7, action: 'DEPART', snapshot: {
trip: { tripNo: trip.tripNo, departedAt: '2026-10-03T01:00:00Z' },
confirmedLoad: { manifestHash: 'original-manifest', pallets: [
{ warehousePalletId: 'P1', palletSpaces: 2, deliveryIndex: 1 }, { warehousePalletId: 'P2', palletSpaces: 1, deliveryIndex: 1 }],
cargo: [{ warehousePalletId: 'P1', dispatchNo: 'DP-OWN', customerId: 'C1', packages: 5 },
{ warehousePalletId: 'P2', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', packages: 9 }] },
externalBookings: [{ bookingNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', status: 'BOUND',
receipt: { source: { receivedPackages: 9, pallets: [{ warehousePalletId: 'P2', packages: 9 }] } } }] } }
if (fault === 'unbound external') depart.snapshot.externalBookings[0].status = 'CONFIRMED'
if (fault === 'wrong stop') depart.snapshot.confirmedLoad.pallets[1].deliveryIndex = 2
if (fault === 'wrong receipt') depart.snapshot.externalBookings[0].receipt.source.pallets[0].packages = 8
if (fault === 'wrong weights') shares[0].palletSpaces = 3
function summary() {
return { departureVersion: fault === 'wrong departure' ? 8 : 7, manifestHash: 'original-manifest',
allocationMethod: 'SAME_ROUTE_PALLET_SPACES', allocationBlockReason: null,
totals: ['CNY', 'USD'].filter(currency => rows.some(row => row.details.currency === currency)).map(currency => {
const amount = rows.filter(row => row.details.currency === currency).reduce((sum, row) => sum + Math.round(Number(row.details.amount) * 100), 0)
// Hard-coded independently calculated expected cent splits for 2:1 physical spaces.
const own = currency === 'USD' ? 871 : amount === 10003 ? 6669 : 6667
const external = currency === 'USD' ? 436 : 3334
return { currency, directCost: amount / 100, ownCost: own / 100, externalCost: external / 100, allocations: [
{ businessNo: 'DP-OWN', dispatchNo: 'DP-OWN', sourceType: 'OWN', palletSpaces: 2, amount: own / 100 },
{ businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', sourceType: fault === 'external counted own' ? 'OWN' : 'EXTERNAL',
palletSpaces: 1, amount: (external + (fault === 'lost cent' ? 1 : 0)) / 100 }] }
}) }
}
const response = (data, code = '200', http = 200) => ({ http, body: { code, data: copy(data) }, requestId: 'request', traceId: 'trace' })
const listing = () => ({ page: { records: copy(rows), total: rows.length, page: 1, size: 100 }, summary: summary() })
function audit(row, action) {
const history = histories.get(row.costNo) || []
history.push({ recordVersion: row.recordVersion, action, reason: 'controlled audit', occurredAt: new Date().toISOString(),
snapshot: { cost: copy(row), summary: summary() } })
histories.set(row.costNo, history)
}
const context = { marker: 'CURRENT-RUN', token: 'offline-writer', createdResources: { fleetTrips: [trip],
fleetAcceptance: { marker: 'CURRENT-RUN', tripNo: trip.tripNo,
departure: { tripNo: trip.tripNo, status: 'IN_TRANSIT', departedAt: depart.snapshot.trip.departedAt } } },
assert(_module, label, _response, condition) { assert.ok(condition, label) }, async checkpoint() {},
async request(method, path, body, token) {
calls.push({ method, path, body: copy(body), actor: token })
if (path.endsWith('/authorization/permissions')) return response((token === 'offline-writer' || fault === 'reader is admin'
? ['list', 'cost-create', 'cost-edit'] : token === 'offline-reader' ? ['list'] : []).map(name => prefix + name))
const denied = token === 'offline-denied' || (token === 'offline-reader' && method !== 'GET')
if (denied && fault !== 'permission bypass') return fault === 'denial is outage' ? response(null, 'S0503', 503) : response(null, 'A0403', 403)
if (method === 'GET' && path.includes('/history') && !path.includes('/costs')) return response({ records: [depart], total: 1 })
if (method === 'GET' && path.includes('/history')) {
const costNo = path.split('/costs/')[1].split('/')[0], history = copy(histories.get(costNo))
if (fault === 'history overwritten') history[0].snapshot.cost.details.amount = '100.03'
if (fault === 'history summary drift') history[0].snapshot.summary.totals[0].directCost = 999
return response({ records: history, total: history.length })
}
if (method === 'GET') return response(listing())
if (method === 'POST') {
const saved = keys.get(body.idempotencyKey)
if (saved) {
if (JSON.stringify(saved.details) !== JSON.stringify(body.details)) return response(null, 'A0409')
if (fault === 'replay duplicate') { const duplicate = { ...copy(saved.row), costNo: `TC-${rows.length + 1}` }; rows.push(duplicate); return response(duplicate) }
if (fault === 'creation overwrites correction') saved.row.details = copy(body.details)
return response(saved.row)
}
const row = { costNo: `TC-${rows.length + 1}`, tripNo: trip.tripNo, recordVersion: 0, details: copy(body.details) }
rows.push(row); keys.set(body.idempotencyKey, { details: copy(body.details), row }); audit(row, 'CREATE'); return response(row)
}
assert.equal(method, 'PUT')
const row = rows.find(item => path.endsWith(item.costNo)); assert.ok(row)
if (Number(body.recordVersion) !== row.recordVersion && fault !== 'stale accepted') return response(null, 'A0409')
if (JSON.stringify(row.details) === JSON.stringify(body.details) && fault !== 'noop audit') return response(row)
row.details = copy(body.details); row.recordVersion++; audit(row, 'UPDATE'); return response(row)
} }
return { context, trip, inputs, calls, rows }
}
test('physical DEPART costs include original currencies, correction, immutable history and three permission actors', async () => {
const f = fixture(), proof = await verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)
assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.expected.finalCny, '100.03')
assert.equal(proof.final.summary.totals.find(row => row.currency === 'CNY').ownCost, 66.69)
assert.deepEqual(proof.coverage.sourceTypes, ['OWN', 'EXTERNAL'])
assert.ok(f.calls.every(call => call.path.includes('/fleet/trips/FT-CURRENT') || call.path.endsWith('/authorization/permissions')))
assert.ok(!JSON.stringify(proof).includes('offline-reader')); assert.ok(!JSON.stringify(proof).includes('offline-denied'))
assert.ok(proof.requests.filter(row => row.method !== 'GET').every(row => row.body))
const count = f.calls.length
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs), /重复开始/)
assert.equal(f.calls.length, count)
})
test('cost expectation uses stable largest remainder and conserves every cent including tie and zero', () => {
const shares = ['B', 'A', 'C'].map(businessNo => ({ businessNo, sourceType: 'OWN', dispatchNo: businessNo, palletSpaces: 1 }))
assert.deepEqual(expectedAllocations('0.02', shares).map(row => [row.businessNo, row.cents]), [['A', '1'], ['B', '1'], ['C', '0']])
assert.deepEqual(expectedAllocations('0', shares).map(row => row.cents), ['0', '0', '0'])
assert.equal(TRIP_COST_EXPECTATIONS.finalCny, '100.03')
})
for (const fault of ['unbound external', 'wrong stop', 'wrong receipt', 'wrong weights', 'wrong departure', 'external counted own',
'lost cent', 'reader is admin', 'denial is outage', 'permission bypass', 'history overwritten', 'history summary drift',
'replay duplicate', 'creation overwrites correction', 'stale accepted', 'noop audit']) {
test(`rejects ${fault} and preserves evidence without cleanup`, async () => {
const f = fixture(fault)
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs))
assert.equal(f.trip.costAcceptance.status, 'FAILED')
assert.ok(f.calls.every(call => call.method !== 'DELETE'))
})
}
test('missing current-run membership or permission session fails before HTTP', async () => {
for (const mutate of [f => { f.inputs.deniedToken = '' }, f => { f.context.createdResources.fleetTrips = [] },
f => { f.context.createdResources.fleetAcceptance.marker = 'OLD' }, f => { f.inputs.shares = [] }]) {
const f = fixture(); mutate(f)
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)); assert.equal(f.calls.length, 0)
}
})
test('unknown HTTP outcome retains original key and does not retry or hide failure', async () => {
const f = fixture(), original = f.context.request
f.context.request = async (...args) => {
if (args[0] === 'POST' && args[3] === f.context.token) throw new Error('controlled network loss')
return original(...args)
}
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs), /network loss/)
const entry = f.trip.costAcceptance.requests.at(-1)
assert.equal(entry.status, 'UNKNOWN_RESULT'); assert.ok(entry.body.idempotencyKey)
assert.equal(f.rows.length, 0)
})