test(fleet): verify operations from immutable current-run evidence

This commit is contained in:
2026-10-03 15:00:12 +08:00
parent 780d37e1d2
commit dd52bfd043
4 changed files with 221 additions and 0 deletions
@@ -1,4 +1,66 @@
import { createHash } from 'node:crypto'
import { expectedAllocations } from './fleet-trip-cost-acceptance.mjs'
/** C's signed-by-hash handover artifact is the only producer accepted by the standalone reader. */
export function operationsFixtureFromHandover(bytes, { sha256, marker, runId }) {
const fail = () => { throw new Error('Current C fixture, original DEPART or verified independent cost shares do not match') }
if (!/^[a-f0-9]{64}$/.test(sha256 || '') || createHash('sha256').update(bytes).digest('hex') !== sha256 || !marker || !runId) fail()
const f = JSON.parse(bytes.toString('utf8')), history = f.immutableDeparture, snapshot = history?.snapshot, load = snapshot?.confirmedLoad, c = f.costs
if (f.marker !== marker || f.runId !== runId || f.status !== 'DEPART_AND_HANDOVER_VERIFIED' || !f.handoverNo
|| !f.tripNo || !f.warehouseId || history?.action !== 'DEPART' || snapshot?.trip?.tripNo !== f.tripNo
|| String(snapshot.trip.warehouseId) !== String(f.warehouseId) || !snapshot.trip.remark?.includes(marker)
|| f.departure?.tripNo !== f.tripNo || f.departure.status !== 'IN_TRANSIT' || !Number.isFinite(Date.parse(f.departure.departedAt))
|| snapshot.trip.departedAt !== f.departure.departedAt || !load?.manifestHash
|| c?.status !== 'VERIFIED' || c.marker !== marker || c.tripNo !== f.tripNo
|| c.expected?.finalCny !== '100.03' || c.expected?.usd !== '13.07' || c.expected?.allocationMethod !== 'SAME_ROUTE_PALLET_SPACES') fail()
const positive = value => Number.isSafeInteger(Number(value)) && Number(value) > 0
if (!Array.isArray(load.pallets) || !load.pallets.length || !Array.isArray(load.cargo) || !load.cargo.length || !Array.isArray(c.shares) || !c.shares.length) fail()
const pallets = new Map()
for (const p of load.pallets) { if (!p.warehousePalletId || pallets.has(String(p.warehousePalletId)) || !positive(p.palletSpaces)) fail(); pallets.set(String(p.warehousePalletId), Number(p.palletSpaces)) }
const seen = new Set(), shipped = new Map()
for (const row of load.cargo) {
const item = `${row.dispatchNo}:${row.dispatchItemId}`, key = `${row.dispatchNo}:${row.customerId}:${row.warehousePalletId}`
if (!row.dispatchNo || !row.dispatchItemId || !row.customerId || seen.has(item) || !pallets.has(String(row.warehousePalletId)) || !positive(row.packages)) fail()
seen.add(item); shipped.set(key, (shipped.get(key) || 0) + Number(row.packages))
}
const bookings = new Map(), external = new Map(), money = new Map()
for (const b of snapshot.externalBookings || []) {
if (['DRAFT', 'CANCELLED'].includes(b.status)) continue
if (b.status !== 'BOUND' || !b.bookingNo || bookings.has(b.bookingNo) || !b.dispatchNo || !b.receipt?.source?.pallets?.length || !['CNY', 'USD'].includes(b.cargo?.currency)) fail()
bookings.set(b.bookingNo, b)
let count = 0
for (const p of b.receipt.source.pallets) { if (!positive(p.packages)) fail(); const key = `${b.dispatchNo}:${b.customerId}:${p.warehousePalletId}`; external.set(key, (external.get(key) || 0) + Number(p.packages)); count += Number(p.packages) }
if (count !== Number(b.receipt.source.receivedPackages)) fail()
const amount = number(b.agreedAmount); money.set(b.cargo.currency, (money.get(b.cargo.currency) || 0) + Math.round(amount * 100))
}
for (const [key, value] of external) if (value > (shipped.get(key) || 0)) fail()
const sharePallets = new Set(), businesses = new Set()
for (const share of c.shares) {
if (!share.businessNo || businesses.has(share.businessNo) || !['OWN', 'EXTERNAL'].includes(share.sourceType) || !share.warehousePalletIds?.length) fail()
businesses.add(share.businessNo)
let spaces = 0
for (const id of share.warehousePalletIds) {
if (!pallets.has(String(id)) || sharePallets.has(String(id)) || !load.cargo.some(row => row.dispatchNo === share.dispatchNo && String(row.warehousePalletId) === String(id))) fail()
sharePallets.add(String(id)); spaces += pallets.get(String(id))
}
if (spaces !== share.palletSpaces) fail()
if (share.sourceType === 'EXTERNAL') {
const b = bookings.get(share.businessNo)
if (!b || b.dispatchNo !== share.dispatchNo || b.receipt.source.pallets.some(p => !share.warehousePalletIds.map(String).includes(String(p.warehousePalletId)))) fail()
} else if (share.businessNo !== share.dispatchNo || [...bookings.values()].some(b => b.dispatchNo === share.dispatchNo)) fail()
}
if (sharePallets.size !== pallets.size || !c.shares.some(s => s.sourceType === 'OWN') || !c.shares.some(s => s.sourceType === 'EXTERNAL')) fail()
const total = [...shipped.values()].reduce((a, b) => a + b, 0), ext = [...external.values()].reduce((a, b) => a + b, 0)
if (total !== Number(f.departure.shippedPackages) || ext <= 0 || ext >= total) fail()
const costs = [['CNY', '100.03'], ['USD', '13.07']].map(([currency, amount]) => {
const allocations = expectedAllocations(amount, c.shares)
const sum = type => Number(allocations.filter(row => row.sourceType === type).reduce((n, row) => n + BigInt(row.cents), 0n)) / 100
return { currency, directCost: Number(amount), ownCost: sum('OWN'), externalCost: sum('EXTERNAL') }
})
return { marker, stage: 'CURRENT_RUN_DEPARTED', tripNo: f.tripNo, warehouseId: String(f.warehouseId), departedAt: f.departure.departedAt, departureVerified: true,
immutableDeparture: history, expected: { physicalPallets: pallets.size, palletSpaces: [...pallets.values()].reduce((a, b) => a + b, 0), ownPackages: total - ext, externalPackages: ext,
externalAgreedAmounts: [...money].map(([currency, amount]) => ({ currency, amount: amount / 100 })), costs } }
}
const MODULE = 'fleet-quotes-operations'
const QUOTES = '/api/platform/v1/last-leg/fleet/quotes'
@@ -10,6 +72,7 @@ const encode = encodeURIComponent
const query = values => new URLSearchParams(Object.entries(values).filter(([, value]) => value != null).map(([key, value]) => [key, String(value)])).toString()
const version = value => BigInt(id(value))
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value
function number(value) {
if (!['number', 'string'].includes(typeof value) || !/^\d+(?:\.\d{1,2})?$/.test(String(value)) || !Number.isFinite(Number(value))) throw new Error('Missing or invalid numeric fact; never convert unknown to zero')
return Number(value)
@@ -133,6 +196,11 @@ async function execute(context, { scope, writeMeasured = false, idempotencyNames
let fixture, from, to, report
if (scope === 'OPERATIONS_ONLY') {
fixture = requireCurrentFixture(context)
if (fixture.immutableDeparture) {
const history = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}/history?page=1&size=200`, undefined, '核对输入报告的原DEPART不可变快照')
const departures = history.records?.filter(row => row.action === 'DEPART') || []
check('当前历史完整且原DEPART与SHA绑定报告一致', null, Number(history.total) === history.records?.length && departures.length === 1 && same(canonical(departures[0]), canonical(fixture.immutableDeparture)))
}
proof.tripNo = fixture.tripNo
const trip = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}`, undefined, '核对本轮真实发车车次')
check('车次属于当前场景并有真实发车时间', null, trip.trip?.tripNo === fixture.tripNo && id(trip.trip.warehouseId) === id(fixture.warehouseId)