test(fleet): preserve unavailable allocations for multi-stop trips

This commit is contained in:
2026-10-03 19:51:29 +08:00
parent e5af1ba758
commit bab967acce
2 changed files with 85 additions and 9 deletions
@@ -4,7 +4,7 @@ import { verifyFleetTripCosts, expectedAllocations, TRIP_COST_EXPECTATIONS } fro
const copy = value => value === undefined ? undefined : structuredClone(value)
const prefix = 'platform:fleet:trip:'
function fixture(fault) {
function fixture(fault, multiDestination = false) {
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'] },
@@ -12,25 +12,49 @@ function fixture(fault) {
]
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' },
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 },
{ warehousePalletId: 'P2', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', packages: 9 }] },
cargo: [{ warehousePalletId: 'P1', dispatchNo: 'DP-OWN', customerId: 'C1', packages: 5, destinationKey: 'FBA:A' },
{ warehousePalletId: 'P2', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', packages: 9, destinationKey: 'FBA:A' }] },
externalBookings: [{ bookingNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', 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: 'SAME_ROUTE_PALLET_SPACES', allocationBlockReason: null,
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 },
{ businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', sourceType: fault === 'external counted own' ? 'OWN' : 'EXTERNAL',
@@ -103,6 +127,38 @@ test('cost expectation uses stable largest remainder and conserves every cent in
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']) {