feat(test): continue reviewed return source sample with pre-signature audit

This commit is contained in:
2026-10-03 22:13:15 +08:00
parent 6dbc899626
commit 0c8ede3889
5 changed files with 346 additions and 17 deletions
+143 -12
View File
@@ -13,7 +13,7 @@ import { verifyFleetExternalReceipt } from './admin-test-env-modules/fleet-exter
import { verifyFleetExternalDispatch } from './admin-test-env-modules/fleet-external-dispatch-acceptance.mjs'
import { verifyFleetHandover, cleanupFleetHandoverResources } from './admin-test-env-modules/fleet-handover-acceptance.mjs'
import { verifyFleetTripCosts } from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs'
import { verifyFleetReturnRedispatch } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs'
import { verifyFleetReturnRedispatch, waitForRedispatchSignatureAudit, selectReturnSources } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs'
import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
@@ -154,11 +154,12 @@ export function parseFleetArguments(args) {
}
if (config.environment !== 'TEST' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(config.runId)
|| (config.execute && (!args.includes('--environment') || config.runId === 'plan'))) throw Error('Explicit TEST and fresh run-id required')
if (Boolean(config.resumeReport) !== Boolean(config.resumeStage) || (config.resumeStage && !['FULL_COVERAGE', 'FULL_COVERAGE_PERSISTED', 'EXTERNAL_RECEIPT_READY', 'EXTERNAL_PALLETIZED', 'EXTERNAL_RECEIPT_CONFIRMED', 'TERMINAL_READONLY', 'DEPARTED_BEFORE_COSTS', 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER', 'DEPARTED_VEHICLE_CREATED'].includes(config.resumeStage))) throw Error('Explicit reviewed checkpoint required')
if (config.resumeStage?.startsWith('DEPARTED_') ? !/^[a-f0-9]{64}$/.test(config.resumeSha256 || '') : config.resumeSha256)
if (Boolean(config.resumeReport) !== Boolean(config.resumeStage) || (config.resumeStage && !['FULL_COVERAGE', 'FULL_COVERAGE_PERSISTED', 'EXTERNAL_RECEIPT_READY', 'EXTERNAL_PALLETIZED', 'EXTERNAL_RECEIPT_CONFIRMED', 'TERMINAL_READONLY', 'DEPARTED_BEFORE_COSTS', 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER', 'DEPARTED_VEHICLE_CREATED', 'RETURN_SOURCE_SELECTION_BEFORE_RETURN'].includes(config.resumeStage))) throw Error('Explicit reviewed checkpoint required')
if (config.resumeStage?.startsWith('DEPARTED_') || config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN' ? !/^[a-f0-9]{64}$/.test(config.resumeSha256 || '') : config.resumeSha256)
throw Error('Departed recovery requires the exact reviewed report SHA256')
if (config.outputDir && !/^reports\/[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/.test(config.outputDir)) throw Error('Output must be a fresh named directory directly under reports')
if (!['handover', 'return-redispatch'].includes(config.scenario) || (config.scenario === 'return-redispatch' && config.resumeReport))
if (!['handover', 'return-redispatch'].includes(config.scenario) || (config.scenario === 'return-redispatch' && config.resumeReport && config.resumeStage !== 'RETURN_SOURCE_SELECTION_BEFORE_RETURN')
|| (config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN' && config.scenario !== 'return-redispatch'))
throw Error('Return redispatch only accepts a fresh run, never a reviewed old checkpoint')
return config
}
@@ -169,7 +170,12 @@ export function fleetExecutionPlan(config) {
stages: ['A actual return 2', 'original remaining signatures; A incomplete and B complete', 'RETURN_RECEIPT draft lock and pre-arrange', 'new PDA picks and DEPART 2', 'redispatch signature; A complete', 'source event/ACK read-only evidence remains required'] } : null,
outputDir: config.outputDir || `reports/fleet-${config.runId}`, producer: config.resumeReport ? `Reviewed ${config.resumeStage} checkpoint in the same original fixture` : 'phase4-overseas-dispatch with its normal fresh prerequisites',
resumeReport: config.resumeReport || null, resumeStage: config.resumeStage || null, resumeSha256: config.resumeSha256 || null,
stages: config.resumeStage?.startsWith('DEPARTED_')
stages: config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN'
? ['Verify exact source-selection-failure report SHA256 and three definitive delivery starts', 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions',
'Recheck repaired A/B return candidates and complete source quantities before any write', 'Create fresh bound PDA session',
'Original sample actual return and redispatch; no repeated original departure/handover/cost/start-delivery',
'Wait for coordinator pre-signature A0/B1 event evidence gate', 'New signature and source ACK evidence; close new and original resources']
: config.resumeStage?.startsWith('DEPARTED_')
? ['Verify reviewed report SHA256', afterCosts
? 'Read-only exact trip/version/DEPART/cargo, unchanged verified costs, empty handovers and unsigned deliveries'
: 'Read-only exact original trip/version/DEPART/cargo and empty costs/handovers/unsigned deliveries',
@@ -189,6 +195,30 @@ export function fleetExecutionPlan(config) {
export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') {
const resources = report?.createdResources, proof = resources?.fleetAcceptance, full = resources?.fleetFeeCoverageCases?.[0]
const trip = resources?.fleetTrips?.find(row => row.tripNo === proof?.tripNo)
if (stage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN') {
const attempt = trip?.returnRedispatch, originals = attempt?.originalDeliveries, writes = attempt?.requests?.filter(row => row.method !== 'GET')
const failures = report?.steps?.filter(row => row.passed === false)
if (report?.success !== false || proof?.stage !== 'RETURN_REDISPATCH' || proof.status !== 'CHECKING'
|| proof.marker !== attempt?.marker || trip?.costAcceptance?.status !== 'VERIFIED' || trip.handover?.status !== 'VERIFIED'
|| proof.departure?.status !== 'IN_TRANSIT' || proof.departure.tripNo !== trip.tripNo
|| trip.closure || proof.closure || proof.receipts?.length || attempt.status !== 'FAILED'
|| attempt.failure !== 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B'
|| attempt.exceptionNo || attempt.sources || attempt.newTripNo || attempt.originalSignatures
|| originals?.length !== 3 || new Set(originals.map(row => row.deliveryNo)).size !== 3 || writes?.length !== 3
|| originals.some(row => id(row.customerId) !== id(proof.customerId) || id(row.overseasWarehouseId) !== id(proof.warehouseId)
|| !proof.dispatchNos.includes(row.dispatchNo) || row.status !== 'WAIT_DELIVERY' || row.signedAt || Number(row.signedPackages || 0))
|| attempt.requests.some(row => row.status !== 'RESPONDED' || row.http !== 200 || id(row.code) !== '200' || !row.requestId || !row.traceId)
|| writes.some(row => {
const original = originals.find(item => item.deliveryNo === row.result?.deliveryNo)
return !original || row.method !== 'PUT' || row.path !== `/api/platform/v1/last-leg/delivery-orders/${original.deliveryNo}/status?customerId=${proof.customerId}`
|| row.body?.status !== 'DELIVERING' || id(row.body.expectedVersion) !== id(original.version)
|| row.result.deliveryStatus !== 'DELIVERING' || row.result.replayed !== false
}) || new Set(writes.map(row => row.result?.deliveryNo)).size !== 3
|| resources.fleetCostActors?.status !== 'DISABLED' || failures?.length !== 1 || failures[0].name !== '模块执行'
|| failures[0].evidence !== attempt.failure)
throw Error('Return source recovery requires the exact known candidate failure and three definitive delivery starts, with no return/signature writes')
return { resources, marker: proof.marker, returnSourceSelection: true }
}
if (stage === 'DEPARTED_BEFORE_COSTS' || stage === 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER' || stage === 'DEPARTED_VEHICLE_CREATED') {
const vehicleCreated = stage === 'DEPARTED_VEHICLE_CREATED'
const afterCosts = stage !== 'DEPARTED_BEFORE_COSTS'
@@ -321,10 +351,11 @@ export async function verifyFleetTerminalState(context) {
}
/** All business checks happen before creating replacement test sessions or issuing a business command. */
export async function verifyDepartedRecovery(context, { costsAlreadyVerified = false, createdVehicle } = {}) {
export async function verifyDepartedRecovery(context, { costsAlreadyVerified = false, createdVehicle, returnSourceReady = false } = {}) {
const proof = context.createdResources.fleetAcceptance
const trip = context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo)
const original = trip.costAcceptance.departure, snapshot = original.snapshot, load = snapshot.confirmedLoad
const expectedTrip = returnSourceReady ? trip.handover.finalTrip : snapshot.trip
const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`
const evidence = { status: 'CHECKING', tripNo: trip.tripNo, requests: [], dispatchDetails: [], deliveries: [] }
context.createdResources.fleetDepartureRecoveryVerification = evidence
@@ -337,20 +368,43 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f
return r.body.data
}
const current = await get(base)
check(current.trip?.status === 'IN_TRANSIT' && id(current.trip.recordVersion) === id(snapshot.trip.recordVersion)
check(current.trip?.status === 'IN_TRANSIT' && id(current.trip.recordVersion) === id(expectedTrip.recordVersion)
&& current.trip.departedAt === proof.departure.departedAt && id(current.trip.warehouseId) === id(proof.warehouseId)
&& id(current.trip.vehicleId) === id(snapshot.trip.vehicleId) && id(current.trip.driverId) === id(snapshot.trip.driverId)
&& id(current.trip.vehicleId) === id(expectedTrip.vehicleId) && id(current.trip.driverId) === id(expectedTrip.driverId)
&& !current.trip.vehicleRelease && !current.trip.driverRelease, 'trip version, resources or departure changed')
const history = await get(`${base}/history?page=1&size=100`)
const departures = history.records?.filter(row => row.action === 'DEPART')
check(Array.isArray(history.records) && Number(history.total) === history.records.length
&& departures?.length === 1 && same(departures[0], original)
&& history.records.every(row => BigInt(row.recordVersion) <= BigInt(original.recordVersion)), 'immutable DEPART/history changed')
&& history.records.every(row => BigInt(row.recordVersion) <= BigInt(expectedTrip.recordVersion)), 'immutable DEPART/history changed')
if (returnSourceReady) {
const expectedHistory = trip.handover.requests.filter(row => row.method === 'GET' && row.path.startsWith(`${base}/history?`)).at(-1)?.result
check(expectedHistory && same(history.records, expectedHistory.records), 'verified handover history changed')
check(typeof context.context?.readRecoveryTripResources === 'function', 'current resource occupancy reader missing')
const bindings = await context.context.readRecoveryTripResources(trip.tripNo)
check(bindings.length === 2 && bindings.every(row => row.tripNo === trip.tripNo)
&& bindings.some(row => row.resourceKind === 'VEHICLE' && id(row.resourceId) === id(expectedTrip.vehicleId))
&& bindings.some(row => row.resourceKind === 'DRIVER' && id(row.resourceId) === id(expectedTrip.driverId)), 'current resources no longer belong to the original trip')
evidence.resourceBindings = bindings
const vehicleSnapshot = trip.handover.requests.find(row => row.method === 'POST'
&& row.path === '/api/platform/v1/last-leg/master-data/vehicles' && id(row.result?.id) === id(expectedTrip.vehicleId))?.result
const driverSnapshot = trip.handover.requests.find(row => row.method === 'POST'
&& row.path === `/api/platform/v1/last-leg/fleet/drivers/${expectedTrip.driverId}/review`)?.result
check(vehicleSnapshot && driverSnapshot, 'verified current resource snapshots missing')
const vehicle = await get(`/api/platform/v1/last-leg/master-data/vehicles/${expectedTrip.vehicleId}`)
const driver = await get(`/api/platform/v1/last-leg/fleet/drivers/${expectedTrip.driverId}`)
check(same(vehicle, vehicleSnapshot), 'current vehicle snapshot changed')
check(['id', 'recordVersion', 'active', 'reviewStatus', 'licenseFileId', 'details'].every(field => same(driver[field], driverSnapshot[field])), 'current driver identity/version/qualification facts changed')
evidence.currentVehicle = vehicle; evidence.currentDriver = driver
}
const costs = await get(`${base}/costs?page=1&size=100`), handovers = await get(`${base}/handovers`)
check((costsAlreadyVerified ? same(costs, trip.costAcceptance.final)
: costs.page?.records?.length === 0 && Number(costs.page.total) === 0 && costs.summary?.totals?.length === 0)
&& costs.summary.manifestHash === load.manifestHash && id(costs.summary.departureVersion) === id(original.recordVersion), 'costs already written or cargo basis changed')
check(handovers.records?.length === 0 && Number(handovers.total) === 0, 'handover already exists')
if (returnSourceReady) {
const expected = trip.handover.requests.filter(row => row.method === 'GET' && row.path.startsWith(`${base}/handovers?`)).at(-1)?.result
check(expected && Number(handovers.total) === 1 && same(handovers.records, expected.records), 'original verified handover changed')
} else check(handovers.records?.length === 0 && Number(handovers.total) === 0, 'handover already exists')
check(same(current.dispatches.map(row => row.dispatchNo).sort(), [...proof.dispatchNos].sort())
&& current.dispatches.every(row => id(row.customerId) === id(proof.customerId)), 'trip dispatch binding changed')
for (const number of proof.dispatchNos) {
@@ -369,8 +423,12 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f
for (const number of deliveryNos) {
const detail = await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(proof.customerId)}`)
const order = detail.order
const started = returnSourceReady ? trip.returnRedispatch.requests.find(row => row.method === 'PUT' && row.result?.deliveryNo === number) : null
check(order?.deliveryNo === number && id(order.customerId) === id(proof.customerId) && proof.dispatchNos.includes(order.dispatchNo)
&& order.status === 'WAIT_DELIVERY' && id(order.version) === '1' && !order.signedAt && !Number(order.signedPackages || 0)
&& order.status === (returnSourceReady ? 'DELIVERING' : 'WAIT_DELIVERY')
&& id(order.version) === (returnSourceReady ? String(Number(started?.body.expectedVersion) + 1) : '1')
&& (!returnSourceReady || order.vehiclePlate === expectedTrip.vehiclePlate && order.driverName === expectedTrip.driverName && order.driverPhone === expectedTrip.driverPhone)
&& !order.signedAt && !Number(order.signedPackages || 0)
&& !order.completedAt && order.documentSummary?.categories?.every(row => Number(row.fileCount) === 0), 'delivery has started, changed or been signed')
evidence.deliveries.push(order)
}
@@ -409,6 +467,74 @@ export async function readReviewedVehicleOccupation(env, vehicleId, fetcher = gl
return result.rows[0]
}
export async function readRecoveryTripResources(env, tripNo, fetcher = globalThis.fetch) {
validateEvidenceConfig(env)
if (!/^[A-Za-z0-9_-]{1,128}$/.test(tripNo)) throw Error('Exact recovery trip number required')
const sql = ['SET NAMES utf8mb4 COLLATE utf8mb4_bin;', 'SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;',
'START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;',
`SELECT JSON_OBJECT('resourceKind',resource_kind,'resourceId',CAST(resource_id AS CHAR),'tripNo',trip_no) FROM ll_fleet_resource_assignment WHERE trip_no='${tripNo}' ORDER BY resource_kind;`, 'COMMIT;'].join('\n')
const script = buildEvidenceRemoteScript('lastleg', sql, env.FBA_CORE_JENKINS_AGENT)
const response = await fetcher(new URL('scriptText', env.JENKINS_URL.replace(/\/?$/, '/')), { method: 'POST',
headers: { Authorization: 'Basic ' + Buffer.from(env.JENKINS_USER + ':' + env.JENKINS_API_TOKEN).toString('base64'), 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ script }), signal: AbortSignal.timeout(55000) })
if (!response.ok) throw Error('Recovery resources readonly HTTP failure')
const lines = (await response.text()).split(/\r?\n/).filter(line => line.startsWith('FBA_CORE_JSON='))
if (lines.length !== 1) throw Error('Recovery resource evidence ambiguous')
const result = JSON.parse(lines[0].slice('FBA_CORE_JSON='.length))
if (result.ok !== true || !Array.isArray(result.rows)) throw Error('Recovery resource query failed')
return result.rows
}
export async function verifyReturnSourceRecovery(context, { verifyOriginal = verifyDepartedRecovery } = {}) {
const original = await verifyOriginal(context, { costsAlreadyVerified: true, returnSourceReady: true })
const run = context.createdResources.fleetAcceptance, rows = []
const previous = original.trip.returnRedispatch
for (const delivery of previous.originalDeliveries) {
const response = await context.request('GET', `/api/platform/v1/last-leg/delivery-orders/${delivery.deliveryNo}/return-inbound-options?customerId=${run.customerId}`)
if (!success(response) || !response.requestId || !response.traceId || !Array.isArray(response.body.data)) throw Error('Repaired return candidates must be definitive')
for (const relation of response.body.data) {
const tail = await context.request('GET', `/api/platform/v1/last-leg/orders/${relation.lastMileOrderId}?customerId=${run.customerId}`)
if (!success(tail) || !tail.requestId || !tail.traceId || relation.deliveryNo !== delivery.deliveryNo
|| relation.outboundNo !== delivery.outboundNo || id(tail.body.data?.order?.customerId) !== id(run.customerId)
|| id(tail.body.data.order.overseasWarehouseId) !== id(run.warehouseId)
|| id(tail.body.data.order.id) !== id(relation.lastMileOrderId)) throw Error('Repaired source owner or delivery identity mismatch')
rows.push({ relation, tail: tail.body.data.order, delivery })
}
}
const selected = selectReturnSources(rows)
context.createdResources.fleetDepartureRecoveryVerification.returnSourceSelection = selected
await context.checkpoint()
return original
}
export async function continueReviewedReturnSources(context, { verify = verifyReturnSourceRecovery,
resolveAccess = resolvePickerAccess, makePicker = createPicker, closePicker = disablePicker,
redispatch = verifyFleetReturnRedispatch, closeOriginal = finishFleetTrip, cleanup = cleanupFleetHandoverResources } = {}) {
const verified = await verify(context), trip = verified.trip, run = context.createdResources.fleetAcceptance
trip.previousSourceSelectionAttempts = [trip.returnRedispatch]; delete trip.returnRedispatch
run.previousPicker = run.picker; delete run.picker
run.pickerMarker = `${context.marker}-RETURN-RECOVERY-${randomUUID().slice(0, 8)}`
await context.checkpoint()
let picker
try {
const child = Object.create(context); child.marker = run.pickerMarker
const access = await resolveAccess(context)
picker = access ? await makePicker(child, 71, run.warehouseId, access) : null
if (!picker?.token || !picker.membershipId) throw Error('Source recovery requires a fresh bound PDA session')
run.picker = { membershipId: picker.membershipId, username: picker.username }; await context.checkpoint()
const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL')
const result = await redispatch(context, MODULE, trip, { customerId: run.customerId, warehouseId: run.warehouseId,
pickerToken: picker.token, dispatchNos: run.dispatchNos, deliveryNos: [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo], sourceSelectionRecovery: true })
run.receipts = result.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt }))
context.context.saveFleetReturnRedispatchFixture(result)
run.closure = await closeOriginal(context, MODULE, trip)
if (run.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('Recovered original trip did not close')
await cleanup(context, MODULE, trip)
run.stage = 'FINISHED'; run.status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP'; await context.checkpoint()
return run
} finally { await closePicker(context, picker, 71) }
}
export async function runFleetHandoverStage(context, trip, pickerToken, dispatchDetails, { verify = verifyFleetHandover, onReady, reviewedVehicle } = {}) {
const state = await verify(context, MODULE, trip, pickerToken, dispatchDetails, { reviewedVehicle })
const run = context.createdResources.fleetAcceptance
@@ -802,7 +928,7 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
const previous = resolve(root, config.resumeReport), previousRelative = relative(resolve(root, 'reports'), previous)
if (!previousRelative || previousRelative.startsWith('..') || isAbsolute(previousRelative)) throw Error('Resume report must be inside this TEST repository reports')
const bytes = readFileSync(previous)
if (config.resumeStage?.startsWith('DEPARTED_') && createHash('sha256').update(bytes).digest('hex') !== config.resumeSha256)
if ((config.resumeStage?.startsWith('DEPARTED_') || config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN') && createHash('sha256').update(bytes).digest('hex') !== config.resumeSha256)
throw Error('Reviewed departure report SHA256 mismatch; no environment request sent')
resumed = reviewedFullCheckpoint(JSON.parse(bytes.toString('utf8')), config.resumeStage)
save('reviewed-checkpoint.json', { source: config.resumeReport, sha256: createHash('sha256').update(bytes).digest('hex'), stage: config.resumeStage })
@@ -826,6 +952,9 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
context.context.fleetRunId = config.runId
context.context.fleetScenario = config.scenario || 'handover'
context.context.readReviewedVehicleOccupation = vehicleId => readReviewedVehicleOccupation(env, vehicleId)
context.context.readRecoveryTripResources = tripNo => readRecoveryTripResources(env, tripNo)
context.context.onBeforeRedispatchSignature = proof => waitForRedispatchSignatureAudit(context, proof,
{ root, output, secrets: [...secrets, context.token, context.pdaToken, context.refreshToken] })
context.context.saveFleetHandoverFixture = fixture => save('handover-fixture.json', fixture)
context.context.saveFleetReturnRedispatchFixture = fixture => save('return-redispatch-fixture.json', fixture)
let handoverCheckpoint = 0
@@ -839,6 +968,7 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
}
context.checkpoint = () => context.context.checkpointFleetHandover(context.createdResources.fleetAcceptance)
const fleetScenario = async (ctx, options = {}) => {
if (options.returnSourceSelection) return continueReviewedReturnSources(ctx)
if (options.resumeDeparted) return continueReviewedDeparture(ctx, { prepareActors: prepareCostActors,
costsAlreadyVerified: options.costsAlreadyVerified, createdVehicle: options.createdVehicle })
const costActors = options.terminalReadOnly || scenario !== runFleetScenario ? undefined : await prepareCostActors?.(ctx)
@@ -867,6 +997,7 @@ export async function runFleetAcceptance(config, { env = process.env, readers, c
context.token = login.body.data.accessToken; context.refreshToken = login.body.data.refreshToken || ''
await context.executeModule(MODULE, () => fleetScenario(context, { resumeFull: !resumed.receiptReady,
resumeDeparted: resumed.departed,
returnSourceSelection: resumed.returnSourceSelection,
costsAlreadyVerified: resumed.costsAlreadyVerified,
createdVehicle: resumed.createdVehicle,
resumePersisted: resumed.persisted, resumeReceiptReady: resumed.receiptReady, resumePalletized: resumed.palletized, resumeReceiptConfirmed: resumed.receiptConfirmed, terminalReadOnly: resumed.terminal }))