test(fleet): preserve unavailable allocations for multi-stop trips
This commit is contained in:
@@ -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']) {
|
||||
|
||||
Reference in New Issue
Block a user