test(fleet): preserve unavailable allocations for multi-stop trips
This commit is contained in:
@@ -3,6 +3,7 @@ import { randomUUID } from 'node:crypto'
|
||||
const OPTIONS = { scenario: 'fleet-trip-operating-cost', deploymentBlocker: true }
|
||||
const PERM = 'platform:fleet:trip:'
|
||||
const AUTH = '/api/platform/v1/iam/authorization/permissions'
|
||||
const MULTI_STOP_REASON = '各票卸货站不同,暂不可按同路线托位自动分摊'
|
||||
const clone = value => value === undefined ? undefined : structuredClone(value)
|
||||
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
||||
const sorted = rows => [...rows].sort((a, b) => a.businessNo < b.businessNo ? -1 : a.businessNo > b.businessNo ? 1 : 0)
|
||||
@@ -30,7 +31,7 @@ export function expectedAllocations(amount, shares) {
|
||||
* C calls after real DEPART + replay, before releasing the fixture. No entry registration or IAM mutation.
|
||||
* tripProof must be the object in createdResources.fleetTrips and fleetAcceptance.departure must match.
|
||||
* inputs.shares is the caller's independently known OWN dispatch / EXTERNAL booking mapping:
|
||||
* {businessNo,sourceType,dispatchNo,palletSpaces,warehousePalletIds[]} (same unloading stop).
|
||||
* {businessNo,sourceType,dispatchNo,palletSpaces,warehousePalletIds[]} (may span unloading stops).
|
||||
* inputs.listOnlyToken/deniedToken are current-run platform sessions owned and cleaned up by the caller.
|
||||
* Neither token is persisted. Missing actors/facts fail before cost writes. No automatic restart/cleanup.
|
||||
*/
|
||||
@@ -69,15 +70,22 @@ export async function verifyFleetTripCosts(context, module, tripProof, inputs =
|
||||
} catch (error) { if (entry.status === 'SENDING') entry.status = 'UNKNOWN_RESULT'; throw error }
|
||||
}
|
||||
const list = () => request('GET', `${path}?page=1&size=100`, undefined, '读取本趟成本及分摊')
|
||||
let departure
|
||||
let departure, sameRoute
|
||||
function verifySummary(summary, amounts) {
|
||||
check('分摊锁定原始DEPART版本和装载摘要', String(summary?.departureVersion) === String(departure.recordVersion)
|
||||
&& summary.manifestHash === departure.snapshot.confirmedLoad.manifestHash
|
||||
&& summary.allocationMethod === TRIP_COST_EXPECTATIONS.allocationMethod && summary.allocationBlockReason == null)
|
||||
&& (sameRoute
|
||||
? summary.allocationMethod === TRIP_COST_EXPECTATIONS.allocationMethod && summary.allocationBlockReason == null
|
||||
: summary.allocationMethod == null && summary.allocationBlockReason === MULTI_STOP_REASON))
|
||||
check('原币分别汇总,未混币', Array.isArray(summary.totals) && summary.totals.length === Object.keys(amounts).length
|
||||
&& new Set(summary.totals.map(row => row.currency)).size === summary.totals.length)
|
||||
for (const row of summary.totals) {
|
||||
check('整趟成本与确定预期一致', amounts[row.currency] != null && cents(row.directCost) === cents(amounts[row.currency]))
|
||||
if (!sameRoute) {
|
||||
check('多目的地保留整趟原币成本,分票及自有外接成本未分摊', Array.isArray(row.allocations)
|
||||
&& row.allocations.length === 0 && row.ownCost == null && row.externalCost == null)
|
||||
continue
|
||||
}
|
||||
const expected = expectedAllocations(amounts[row.currency], shares)
|
||||
const actual = sorted(row.allocations).map(part => ({ businessNo: part.businessNo, sourceType: part.sourceType,
|
||||
dispatchNo: part.dispatchNo, palletSpaces: Number(part.palletSpaces), cents: String(cents(part.amount)) }))
|
||||
@@ -110,8 +118,18 @@ export async function verifyFleetTripCosts(context, module, tripProof, inputs =
|
||||
const ids = shares.flatMap(row => row.warehousePalletIds).map(String)
|
||||
check('独立预期覆盖全部实物且没有重叠托盘', new Set(ids).size === ids.length
|
||||
&& same([...ids].sort(), load.pallets.map(row => String(row.warehousePalletId)).sort())
|
||||
&& new Set(load.pallets.map(row => row.deliveryIndex)).size === 1
|
||||
&& load.cargo.every(row => ids.includes(String(row.warehousePalletId))))
|
||||
const stops = snapshot.trip.stops
|
||||
check('逐托目的站索引与DEPART货物目的地一致', Array.isArray(stops) && load.pallets.every(pallet => {
|
||||
const index = pallet.deliveryIndex
|
||||
const cargo = load.cargo.filter(row => String(row.warehousePalletId) === String(pallet.warehousePalletId))
|
||||
return Number.isSafeInteger(index) && index > 0 && index < stops.length && cargo.length > 0
|
||||
&& Boolean(stops[index]?.destinationKey)
|
||||
&& cargo.every(row => row.destinationKey === stops[index].destinationKey)
|
||||
}))
|
||||
sameRoute = new Set(load.pallets.map(row => row.deliveryIndex)).size === 1
|
||||
Object.assign(proof.expected, { allocationMethod: sameRoute ? TRIP_COST_EXPECTATIONS.allocationMethod : null,
|
||||
allocationStatus: sameRoute ? 'AVAILABLE' : 'UNAVAILABLE', allocationBlockReason: sameRoute ? null : MULTI_STOP_REASON })
|
||||
const external = (snapshot.externalBookings || []).filter(row => !['DRAFT', 'CANCELLED'].includes(row.status))
|
||||
check('有效外接预订全为BOUND且预期完整', external.every(row => row.status === 'BOUND')
|
||||
&& same(external.map(row => row.bookingNo).sort(), shares.filter(row => row.sourceType === 'EXTERNAL').map(row => row.businessNo).sort()))
|
||||
@@ -179,6 +197,8 @@ export async function verifyFleetTripCosts(context, module, tripProof, inputs =
|
||||
(proof.histories ??= []).push({ costNo: row.costNo, records: clone(rows) })
|
||||
}
|
||||
proof.coverage = { currencies: ['CNY', 'USD'], sourceTypes: [...new Set(shares.map(row => row.sourceType))],
|
||||
allocation: sameRoute ? 'SAME_ROUTE_CONSERVATION_VERIFIED' : 'MULTI_STOP_UNAVAILABLE_VERIFIED',
|
||||
convertedCnyStatistics: 'NOT_TESTED_BY_THIS_MODULE',
|
||||
permissions: 'WRITER_READER_DENIED', customerFinance: 'NOT_TESTED_BY_THIS_MODULE' }
|
||||
proof.status = 'VERIFIED'; await context.checkpoint?.(); return proof
|
||||
} catch (error) { proof.status = 'FAILED'; await context.checkpoint?.(); throw error }
|
||||
|
||||
Reference in New Issue
Block a user