Files

210 lines
14 KiB
JavaScript
Raw Permalink Normal View History

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, multiDestination = false) {
2026-10-20 10:35:52 +08:00
const trip = { marker: 'fixture-vehicle', tripNo: 'FT-CURRENT', dispatchNo: 'DP-OWN' }, calls = [], rows = [], histories = new Map(), keys = new Map()
const shares = [
{ businessNo: 'DP-OWN', dispatchNo: 'DP-OWN', sourceType: 'OWN', palletSpaces: 2, warehousePalletIds: ['P1'] },
2026-10-20 10:35:52 +08:00
{ businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', 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',
stops: [{ name: 'Origin' }, { destinationKey: 'FBA:A' }, { destinationKey: 'FBA:B' }] },
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, destinationKey: 'FBA:A' },
2026-10-20 10:35:52 +08:00
{ warehousePalletId: 'P2', dispatchNo: 'DP-OWN', customerId: 'C2', packages: 9, destinationKey: 'FBA:A' }] },
externalBookings: [{ bookingNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', customerId: 'C2', status: 'BOUND',
receipt: { source: { receivedPackages: 9, pallets: [{ warehousePalletId: 'P2', packages: 9 }] } } }] } }
const load = depart.snapshot.confirmedLoad
if (multiDestination) {
shares[0].warehousePalletIds.push('P3')
load.pallets[0].palletSpaces = 1
load.pallets.push({ warehousePalletId: 'P3', palletSpaces: 1, deliveryIndex: 2 })
load.cargo[0].packages = 1
load.cargo.push({ warehousePalletId: 'P3', dispatchNo: 'DP-OWN', customerId: 'C1', packages: 4, destinationKey: 'FBA:B' })
}
if (fault === 'duplicate pallet') load.pallets.push(copy(load.pallets[0]))
if (fault === 'overlapping shares') shares[1].warehousePalletIds.push('P1')
if (fault === 'missing pallet') load.pallets.pop()
if (fault === 'uncovered cargo') load.cargo.push({ ...load.cargo[0], warehousePalletId: 'UNKNOWN' })
if (fault === 'mixed pallet destinations') load.cargo.push({ ...load.cargo[0], destinationKey: 'FBA:B' })
if (fault === 'origin stop') load.pallets[0].deliveryIndex = 0
if (fault === 'out of range stop') load.pallets[0].deliveryIndex = 3
if (fault === 'fractional stop') load.pallets[0].deliveryIndex = 1.5
if (fault === 'missing destination') delete load.cargo[0].destinationKey
if (fault === 'missing stops') delete depart.snapshot.trip.stops
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() {
const unavailable = multiDestination && fault !== 'forced allocation'
return { departureVersion: fault === 'wrong departure' ? 8 : 7, manifestHash: 'original-manifest',
allocationMethod: unavailable ? null : 'SAME_ROUTE_PALLET_SPACES',
allocationBlockReason: unavailable ? fault === 'wrong block reason' ? 'other' : '各票卸货站不同,暂不可按同路线托位自动分摊' : 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
if (unavailable) return { currency, directCost: (amount + (fault === 'wrong direct total' ? 1 : 0)) / 100,
ownCost: fault === 'zero instead of unavailable' ? 0 : null, externalCost: null,
allocations: fault === 'invented allocation' ? [{ businessNo: 'DP-OWN', amount: amount / 100 }] : [] }
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 },
2026-10-20 10:35:52 +08:00
{ businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', 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,
2026-10-20 10:35:52 +08:00
departure: { dispatchNo: trip.dispatchNo, status: 'ACTIVE', stage: 'DELIVERING', action: 'DEPART',
result: { 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')
})
test('multi-destination records and corrects whole-trip costs while allocations remain unavailable', async () => {
const f = fixture(undefined, true), proof = await verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)
assert.equal(proof.status, 'VERIFIED')
assert.deepEqual(proof.departure.snapshot.confirmedLoad.pallets.map(row => row.deliveryIndex), [1, 1, 2])
assert.deepEqual(proof.final.summary.totals.map(row => [row.currency, row.directCost, row.ownCost, row.externalCost]),
[['CNY', 100.03, null, null], ['USD', 13.07, null, null]])
assert.ok(proof.final.summary.totals.every(row => row.allocations.length === 0))
assert.equal(proof.expected.allocationStatus, 'UNAVAILABLE')
assert.equal(proof.expected.allocationMethod, null)
assert.equal(proof.coverage.allocation, 'MULTI_STOP_UNAVAILABLE_VERIFIED')
assert.equal(proof.coverage.convertedCnyStatistics, 'NOT_TESTED_BY_THIS_MODULE')
})
for (const fault of ['forced allocation', 'wrong block reason', 'wrong direct total', 'zero instead of unavailable', 'invented allocation']) {
test(`multi-destination rejects ${fault}`, async () => {
const f = fixture(fault, true)
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs))
assert.equal(f.trip.costAcceptance.status, 'FAILED')
})
}
for (const fault of ['duplicate pallet', 'overlapping shares', 'missing pallet', 'uncovered cargo',
'mixed pallet destinations', 'origin stop', 'out of range stop', 'fractional stop', 'missing destination', 'missing stops', 'wrong stop']) {
test(`multi-destination ${fault} fails before any cost write`, async () => {
const f = fixture(fault, true)
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 === 'GET'))
assert.equal(f.rows.length, 0)
})
}
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)
}
})
2026-10-20 10:35:52 +08:00
test('cost execution requires the current single-dispatch native departure, without a legacy Fleet result alias', async () => {
for (const mutate of [
f => { f.context.createdResources.fleetAcceptance.departure = { tripNo: f.trip.tripNo, status: 'IN_TRANSIT', departedAt: '2026-10-03T01:00:00Z' } },
f => { f.context.createdResources.fleetAcceptance.departure.dispatchNo = 'OTHER' },
f => { f.context.createdResources.fleetAcceptance.departure.stage = 'AFTER_LOADING' },
f => { f.context.createdResources.fleetAcceptance.departure.result.departedAt = null },
f => { f.inputs.shares[1].dispatchNo = 'SECOND-DISPATCH' },
]) {
const f = fixture(); mutate(f)
await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs))
assert.equal(f.calls.length, 0)
}
const f = fixture()
f.context.createdResources.fleetAcceptance.departure.result.departedAt = '2026-10-03T01:00:00'
assert.equal((await verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)).status, 'VERIFIED')
})
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)
})