2026-10-03 14:52:04 +08:00
import { mkdirSync , writeFileSync , openSync , closeSync , unlinkSync , readFileSync , fsyncSync } from 'node:fs'
2026-10-03 06:26:33 +08:00
import { resolve , dirname , basename , relative , isAbsolute , join } from 'node:path'
import { fileURLToPath , pathToFileURL } from 'node:url'
import { createHash , randomUUID } from 'node:crypto'
2026-10-03 19:19:16 +08:00
import { AsyncLocalStorage } from 'node:async_hooks'
2026-10-03 06:26:33 +08:00
import { AcceptanceRun , parseArguments , validateExecutionOptions , buildAcceptanceSummary } from './admin-test-env-acceptance.mjs'
import { redactEvidence } from './fba-core-regression-test-env.mjs'
2026-10-03 21:00:07 +08:00
import { createTestEvidenceReaders , buildEvidenceRemoteScript , validateEvidenceConfig } from './fba-core-regression-evidence.mjs'
2026-10-20 10:35:52 +08:00
import { prepareFleetTrip , finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
import { createBody , candidatePage , markerCandidates , dispatchIo , readDispatch , version as dispatchVersion , replay as replayDispatch } from './admin-test-env-modules/grouped-dispatch-acceptance.mjs'
2026-10-03 06:26:33 +08:00
import { verifyFleetBookingReservation } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs'
import { verifyFleetFeeCoverage } from './admin-test-env-modules/fleet-fee-coverage-acceptance.mjs'
import { verifyFleetExternalReceipt } from './admin-test-env-modules/fleet-external-receipt-acceptance.mjs'
import { verifyFleetExternalDispatch } from './admin-test-env-modules/fleet-external-dispatch-acceptance.mjs'
2026-10-03 14:52:04 +08:00
import { verifyFleetHandover , cleanupFleetHandoverResources } from './admin-test-env-modules/fleet-handover-acceptance.mjs'
import { verifyFleetTripCosts } from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs'
2026-10-03 22:13:15 +08:00
import { verifyFleetReturnRedispatch , waitForRedispatchSignatureAudit , selectReturnSources } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs'
2026-10-20 10:35:52 +08:00
import { resolvePickerAccess , createPicker , disablePicker , pickerFixtureSuffix } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
2026-10-03 06:26:33 +08:00
const ROOT = resolve ( dirname ( fileURLToPath ( import . meta . url )), '..' )
const MODULE = 'fleet-specialized-acceptance'
const OPTIONS = { scenario : MODULE , deploymentBlocker : true }
const id = value => value == null ? '' : String ( value )
const success = response => response ? . http === 200 && id ( response ? . body ? . code ) === '200'
const key = () => `fleet- ${ randomUUID () } `
2026-10-20 10:35:52 +08:00
const NATIVE_PROTOCOL = 'NATIVE_DISPATCH_V1'
2026-10-21 14:30:41 +08:00
const nativeDeparture = ( value , dispatchNo ) => value ? . action === 'LOADING_COMPLETE' && value . dispatchNo === dispatchNo
&& value . status === 'ACTIVE' && value . stage === 'DELIVERING' && value . result ? . loadingCompletionHash
&& Number . isFinite ( Date . parse ( value . result . loadingCompletedAt )) && value . result . confirmedLoad ? . manifestHash
2026-10-20 10:35:52 +08:00
const instant = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?$/ . test ( value ) ? ` ${ value } Z` : value
const sameInstant = ( a , b ) => Number . isFinite ( Date . parse ( instant ( a ))) && Date . parse ( instant ( a )) === Date . parse ( instant ( b ))
const deliveryFacts = rows => JSON . stringify ( rows . map ( row => [ id ( row . customerId ), row . deliveryNo , row . outboundNo , Number ( row . packageCount )]). sort ())
export function nativeFleetDeliveries ( detail ) {
if ( ! detail ? . dispatchNo || ! Array . isArray ( detail . documents ? . deliveries ) || ! detail . documents . deliveries . length ) throw Error ( 'Native delivery references missing' )
const rows = new Map (), owners = new Map ()
for ( const row of detail . documents . deliveries ) {
if ( ! row . deliveryNo || ! row . outboundNo || ! id ( row . customerId ) || ! Number . isSafeInteger ( Number ( row . packageCount )) || Number ( row . packageCount ) <= 0 )
throw Error ( 'Native delivery customer, business identity or quantity missing' )
if ( owners . has ( row . deliveryNo ) && owners . get ( row . deliveryNo ) !== id ( row . customerId )) throw Error ( 'Delivery cannot belong to two customers' )
const identity = ` ${ row . customerId } : ${ row . deliveryNo } ` , prior = rows . get ( identity )
if ( prior && ( prior . outboundNo !== row . outboundNo || Number ( prior . packageCount ) !== Number ( row . packageCount ) || prior . groupKey !== row . groupKey ))
throw Error ( 'Duplicate native delivery references disagree' )
owners . set ( row . deliveryNo , id ( row . customerId )); rows . set ( identity , row )
}
return [... rows . values ()]
}
export function selectNativeFleetFullSource ( context , original , candidates ) {
const source = context . createdResources ? . phase4OverseasDispatch
if ( source ? . marker !== context . marker || source . status !== 'VERIFIED' || original . data . dispatchNo !== source . dispatchNo
|| original . data . status !== 'ACTIVE' || original . data . stage !== 'BEFORE_LOADING' || Number ( original . data . counts . pickedPackageCount ) !== 0
|| Number ( original . data . counts . shippedPackageCount ) !== 0 || id ( original . data . recordVersion ) !== id ( source . recordVersion )
|| original . items . items . length < 2 || new Set ( original . items . items . map ( i => i . destination ? . key )). size !== 2 )
throw Error ( 'FULL/PARTIAL require the unchanged current-run two-destination native main' )
const history = source . routeLifecycle ? . stages ? . find ( s => s . label === '新增同目的地第二托' ) ? . snapshot ? . items
if ( ! history ? . length ) throw Error ( 'Current-run released duplicate source evidence missing' )
const own = original . items . items
const eligible = markerCandidates ( context , candidates ). filter ( c => id ( c . customerId ) === id ( source . customerId )
&& id ( c . warehouseId ) === id ( original . warehouseId ) && history . some ( i => i . sourceKey === c . sourceKey
&& id ( i . warehousePalletId ) === id ( c . selection . warehousePalletId ))
&& own . every ( i => i . sourceKey !== c . sourceKey && id ( i . warehousePalletId ) !== id ( c . selection . warehousePalletId )))
if ( eligible . length !== 1 ) throw Error ( 'Need exactly one current-run released, free, non-overlapping FULL pallet; never borrow historical inventory' )
return eligible [ 0 ]
}
2026-10-03 06:26:33 +08:00
2026-10-03 19:07:58 +08:00
/** Install before login/prerequisites. A caught unknown write must not permit another request. */
2026-10-03 19:19:16 +08:00
export function guardFleetWrites ( context , checkpoint , { guardFetch = false } = {}) {
2026-10-03 19:07:58 +08:00
let stopped = null , sequence = 0
2026-10-20 10:35:52 +08:00
const stop = entry => {
context . createdResources ??= {}
context . createdResources . fleetWriteBoundary = { status : 'STOPPED' , method : entry . method , path : entry . path ,
outcome : entry . status , requestId : entry . requestId , traceId : entry . traceId }
}
2026-10-03 19:19:16 +08:00
const scope = new AsyncLocalStorage (), originalFetch = globalThis . fetch
const direct = { async requestWithBody ( method , path , body , token , headers , request ) {
const nativeResponse = await originalFetch ( request ), copy = nativeResponse . clone ()
2026-10-03 19:27:03 +08:00
const bytes = new Uint8Array ( await copy . arrayBuffer ()), contentType = nativeResponse . headers . get ( 'content-type' ) || ''
const content = /json|text|xml|html/i . test ( contentType ) ? new TextDecoder (). decode ( bytes ) : ''
2026-10-03 19:19:16 +08:00
let payload = null
try { payload = content ? JSON . parse ( content ) : null } catch { /* Keep bounded diagnostic only. */ }
2026-10-03 19:27:03 +08:00
return { nativeResponse , http : nativeResponse . status , body : payload , bytes , contentType , raw : payload || ! content ? null : content . slice ( 0 , 500 ),
2026-10-03 19:19:16 +08:00
requestId : payload ? . requestId || nativeResponse . headers . get ( 'x-request-id' ),
traceId : payload ? . traceId || nativeResponse . headers . get ( 'x-trace-id' ) }
} }
const targets = [ 'request' , 'requestWithBody' , 'requestBinary' ]. map ( name => [ context , name ])
if ( guardFetch ) targets . push ([ direct , 'requestWithBody' ])
for ( const [ owner , name ] of targets ) {
const original = owner [ name ]
2026-10-03 19:07:58 +08:00
if ( typeof original !== 'function' ) continue
2026-10-03 19:19:16 +08:00
owner [ name ] = async function ( method , path , body , token , headers ) {
2026-10-03 19:07:58 +08:00
if ( stopped ) throw stopped
if ([ 'GET' , 'HEAD' , 'OPTIONS' ]. includes ( String ( method ). toUpperCase ()))
2026-10-03 19:19:16 +08:00
return scope . run ( true , () => original . apply ( this , arguments ))
2026-10-03 19:07:58 +08:00
const entry = { sequence : ++ sequence , method , path , status : 'SENDING' , startedAt : new Date (). toISOString (),
headers , body }
2026-10-03 19:19:16 +08:00
let session = owner === direct ? String ( headers ? . authorization || '' ). replace ( /^Bearer\s+/i , '' )
: token === undefined ? this . token : token
if ( owner !== direct && this . pdaToken && /\/pda-loading\/tasks\/\d+/ . test ( String ( path ))) session = this . pdaToken
entry . actor = session && session === context . pdaToken ? { kind : 'PDA' , source : 'CONTEXT_PDA_SESSION' }
: session && session === context . token && context . options ? . username ? { kind : 'PLATFORM' , source : 'CONTEXT_ADMIN_SESSION' }
: { kind : 'UNKNOWN' , source : session ? 'UNRESOLVED_SESSION' : 'NO_SESSION' }
2026-10-03 19:07:58 +08:00
// Multipart delegates to requestWithBody, so it is journaled exactly once, after construction.
if ( body instanceof FormData ) {
entry . body = { multipart : [] }
for ( const [ field , value ] of body . entries ()) entry . body . multipart . push ( typeof value === 'string'
? { field , value } : { field , name : value . name , type : value . type ,
base64 : Buffer . from ( await value . arrayBuffer ()). toString ( 'base64' ) })
}
2026-10-20 10:35:52 +08:00
try { await checkpoint ( entry ) } catch ( error ) { entry . status = 'NOT_SENT' ; stopped = error ; stop ( entry ); throw error }
2026-10-03 19:07:58 +08:00
try {
2026-10-03 19:19:16 +08:00
const response = await scope . run ( true , () => original . apply ( this , arguments ))
2026-10-03 19:07:58 +08:00
const http = response ? . http , payload = response ? . body , code = id ( payload ? . code )
2026-10-03 19:27:03 +08:00
const expectsPdf = name === 'requestBinary' || owner === direct && /(?:^|,)\s*application\/pdf(?:\s*[;,]|$)/i . test ( headers ? . accept || '' )
const binary = ( name === 'requestBinary' || owner === direct ) && response ? . bytes instanceof Uint8Array
const pdf = expectsPdf && binary && http === 200 && /^application\/pdf(?:\s*;|$)/i . test ( response . contentType || '' )
2026-10-03 19:19:16 +08:00
&& response . bytes . length > 4 && Buffer . from ( response . bytes . subarray ( 0 , 5 )). toString ( 'ascii' ) === '%PDF-'
2026-10-03 19:07:58 +08:00
const definite = Number . isInteger ( http ) && http >= 200 && http < 500
2026-10-03 19:19:16 +08:00
&& response . requestId && response . traceId
&& ( pdf || payload && typeof payload === 'object' && ! Array . isArray ( payload )
2026-10-03 19:27:03 +08:00
&& ( ! expectsPdf && http === 200 && code === '200' || /^[A-Z]\d{4}$/ . test ( code ) && ( http === 200 || http >= 400 )))
2026-10-03 19:07:58 +08:00
Object . assign ( entry , { status : definite ? 'RESPONDED' : 'UNKNOWN_RESULT' , http , code ,
2026-10-03 19:19:16 +08:00
requestId : response ? . requestId , traceId : response ? . traceId , raw : boundedFleetDiagnostic ( response ? . raw ),
responseBody : redactFleetWrite ( response ? . body ), finishedAt : new Date (). toISOString () })
if ( binary ) entry . binary = { contentType : response . contentType , bytes : response . bytes . length ,
sha256 : createHash ( 'sha256' ). update ( response . bytes ). digest ( 'hex' ), validPdf : Boolean ( pdf ) }
2026-10-20 10:35:52 +08:00
if ( ! definite ) { stopped = Error ( `Unknown write outcome: ${ method } ${ path } ; preserve original request, do not resend` ); stop ( entry ) }
2026-10-03 19:07:58 +08:00
await checkpoint ( entry )
if ( stopped ) throw stopped
return response
} catch ( error ) {
2026-10-03 19:19:16 +08:00
stopped ||= Error ( boundedFleetDiagnostic ( error instanceof Error ? error . message : String ( error )) || 'Fleet write failed' )
2026-10-03 19:07:58 +08:00
if ( entry . status === 'SENDING' ) {
entry . status = 'UNKNOWN_RESULT' ; entry . finishedAt = new Date (). toISOString ()
2026-10-03 19:19:16 +08:00
entry . raw = boundedFleetDiagnostic ( error instanceof Error ? error . message : String ( error ))
2026-10-20 10:35:52 +08:00
stop ( entry ); await checkpoint ( entry )
2026-10-03 19:07:58 +08:00
}
2026-10-20 10:35:52 +08:00
stop ( entry )
2026-10-03 19:07:58 +08:00
throw stopped
}
}
}
2026-10-03 19:19:16 +08:00
if ( guardFetch ) globalThis . fetch = async ( input , init ) => {
if ( scope . getStore ()) return originalFetch ( input , init )
if ( stopped ) throw stopped
const request = new Request ( input , init ), url = new URL ( request . url )
if ( url . origin !== new URL ( context . options . gateway ). origin ) return originalFetch ( input , init )
if ([ 'GET' , 'HEAD' , 'OPTIONS' ]. includes ( request . method )) return originalFetch ( input , init )
const type = request . headers . get ( 'content-type' ) || ''
let body
if ( request . body ) body = type . includes ( 'multipart/form-data' ) ? await request . clone (). formData ()
: type . includes ( 'application/json' ) ? await request . clone (). json () : await request . clone (). text ()
const result = await direct . requestWithBody ( request . method , url . pathname + url . search , body , undefined ,
Object . fromEntries ( request . headers ), request )
return result . nativeResponse
}
return () => { if ( guardFetch ) globalThis . fetch = originalFetch }
}
function boundedFleetDiagnostic ( value ) {
return typeof value === 'string' ? value . replace ( /Bearer\s+[^\s"'<>]+/gi , 'Bearer [REDACTED]' )
. replace ( /((?:password|accessToken|refreshToken|authorization)["']?\s*[=:]\s*["']?)[^\s,;"'<>]+/gi , '$1[REDACTED]' )
. replace ( /https?:\/\/[^\s"'<>]+/gi , '[REDACTED_URL]' ). slice ( 0 , 500 ) : null
2026-10-03 19:07:58 +08:00
}
export function redactFleetWrite ( value , secrets = []) {
2026-10-03 19:19:16 +08:00
const sensitive = name => name !== 'draftToken' && /password|authorization|cookie|privateKey|api.?key|secret|token$|^(?:loginId|username)$/i . test ( name )
2026-10-03 19:07:58 +08:00
if ( Array . isArray ( value )) return value . map ( item => redactFleetWrite ( item , secrets ))
if ( value && typeof value === 'object' && typeof value . field === 'string' && sensitive ( value . field ))
return { field : value . field , value : '[REDACTED]' }
if ( value && typeof value === 'object' ) return Object . fromEntries ( Object . entries ( value ). map (([ name , item ]) => [ name ,
sensitive ( name )
? '[REDACTED]' : redactFleetWrite ( item , secrets )]))
if ( typeof value === 'string' ) return secrets . filter ( secret => typeof secret === 'string' && secret . length )
. reduce (( text , secret ) => text . split ( secret ). join ( '[REDACTED]' ), value )
return value
}
2026-10-03 06:26:33 +08:00
export function parseFleetArguments ( args ) {
2026-10-03 16:11:52 +08:00
const config = { execute : false , environment : 'TEST' , runId : 'plan' , scenario : 'handover' }
2026-10-03 06:26:33 +08:00
let action = null
const seen = new Set ()
for ( let index = 0 ; index < args . length ; index ++ ) {
const name = args [ index ]
if ([ '--plan' , '--execute' ]. includes ( name )) {
if ( action ) throw Error ( 'Choose exactly one plan or execute action' )
action = name ; config . execute = name === '--execute' ; continue
}
2026-10-03 20:06:02 +08:00
if ( ! [ '--environment' , '--run-id' , '--output' , '--resume-report' , '--resume-stage' , '--resume-sha256' , '--scenario' ]. includes ( name ) || ! args [ index + 1 ] || args [ index + 1 ]. startsWith ( '--' )) throw Error ( 'Unsupported or missing argument' )
2026-10-03 06:26:33 +08:00
if ( seen . has ( name )) throw Error ( 'Duplicate argument' )
seen . add ( name )
const value = args [ ++ index ]
if ( name === '--environment' ) config . environment = value
else if ( name === '--run-id' ) config . runId = value
2026-10-03 14:52:04 +08:00
else if ( name === '--output' ) config . outputDir = value
2026-10-03 06:26:33 +08:00
else if ( name === '--resume-report' ) config . resumeReport = value
2026-10-03 20:06:02 +08:00
else if ( name === '--resume-sha256' ) config . resumeSha256 = value
2026-10-03 16:11:52 +08:00
else if ( name === '--scenario' ) config . scenario = value
2026-10-03 06:26:33 +08:00
else config . resumeStage = value
}
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' )
2026-10-03 22:13:15 +08:00
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 )
2026-10-03 20:06:02 +08:00
throw Error ( 'Departed recovery requires the exact reviewed report SHA256' )
2026-10-03 14:52:04 +08:00
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' )
2026-10-03 22:13:15 +08:00
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' ))
2026-10-03 16:11:52 +08:00
throw Error ( 'Return redispatch only accepts a fresh run, never a reviewed old checkpoint' )
2026-10-03 06:26:33 +08:00
return config
}
export function fleetExecutionPlan ( config ) {
2026-10-03 21:00:07 +08:00
const afterCosts = [ 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER' , 'DEPARTED_VEHICLE_CREATED' ]. includes ( config . resumeStage )
2026-10-03 16:11:52 +08:00
return { environment : 'TEST' , execution : config . execute ? 'EXECUTE' : 'PLAN' , runId : config . runId , scenario : config . scenario || 'handover' ,
returnRedispatch : config . scenario === 'return-redispatch' ? { required : 'Two current-run domestic sources; A >=3 boxes; current departure covers A and B in full' ,
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 ,
2026-10-03 20:06:02 +08:00
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 ,
2026-10-03 22:13:15 +08:00
stages : config . resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN'
2026-10-20 10:35:52 +08:00
? [ 'Verify exact source-selection-failure report SHA256 and three definitive native delivery reads' , 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions' ,
2026-10-03 22:13:15 +08:00
'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_' )
2026-10-03 21:00:07 +08:00
? [ 'Verify reviewed report SHA256' , afterCosts
2026-10-03 20:24:56 +08:00
? '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' ,
2026-10-03 21:00:07 +08:00
...( config . resumeStage === 'DEPARTED_VEHICLE_CREATED' ? [ 'Verify exact successful vehicle creation, complete current vehicle snapshot and zero resource assignments' , 'Explicitly reuse the created vehicle; skip its creation POST and continue from new driver' ] : []),
afterCosts ? 'Create fresh PDA session; retain verified costs' : 'Create fresh bounded cost/PDA sessions' ,
afterCosts ? 'Match original verified cost snapshot; no cost commands' : 'Original trip operating costs with actual allocation availability' ,
2026-10-03 20:24:56 +08:00
'Retain original DEPART; no picking or departure commands' ,
2026-10-03 20:06:02 +08:00
'In-transit handover and PDA readback' , 'Original delivery signatures and resource release' , 'Close newly created test sessions' ]
2026-10-20 10:35:52 +08:00
: [ 'two disjoint current-run native sources' , 'independent FULL loading/coverage/departure/POD/resource closure' , 'PARTIAL original main and reservation lifecycle' ,
'external actual receipt' , 'external dispatch and agreed fee' , 'Finance consumer persistence' , 'one native main PDA picking and explicit actual loading' ,
'native current-version whole-main departure and cross-end exact replay' , 'actual in-transit handover and PDA readback' , 'same-fixture optional H5 checkpoint' ,
2026-10-03 14:52:04 +08:00
'per-destination receipt and replacement-resource release' , 'explicit handover-fixture cleanup' ],
2026-10-03 06:26:33 +08:00
boundaries : 'TEST only; serial fresh fixtures; no SQL writes, deployment or configuration changes. Failure retains original requests/resources. No automatic rerun/resume.' ,
limits : 'Finance pool proves consumer persistence; producer ACK requires separate event evidence. UI and remaining fleet product scope are separate.' }
}
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 )
2026-10-20 10:35:52 +08:00
if ( proof ? . protocol !== NATIVE_PROTOCOL || ! proof . dispatchNo || ! trip || trip . dispatchNo !== proof . dispatchNo
|| JSON . stringify ( proof . dispatchNos ) !== JSON . stringify ([ proof . dispatchNo ]) || resources . dispatchWriteBoundary ? . outcomeUnknown
|| resources . fleetWriteBoundary ? . status === 'STOPPED'
|| ( proof . departure && ! nativeDeparture ( proof . departure , proof . dispatchNo )))
throw Error ( 'Only current native single-dispatch reports may resume; legacy or unknown outcomes require explicit reconciliation' )
2026-10-03 22:13:15 +08:00
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'
2026-10-20 10:35:52 +08:00
|| ! nativeDeparture ( proof . departure , proof . dispatchNo )
2026-10-03 22:13:15 +08:00
|| 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
2026-10-20 10:35:52 +08:00
|| originals ? . length !== 3 || new Set ( originals . map ( row => row . deliveryNo )). size !== 3 || writes ? . length !== 0
2026-10-03 22:13:15 +08:00
|| originals . some ( row => id ( row . customerId ) !== id ( proof . customerId ) || id ( row . overseasWarehouseId ) !== id ( proof . warehouseId )
2026-10-20 10:35:52 +08:00
|| row . dispatchNo !== proof . dispatchNo || row . status !== 'DELIVERING' || row . signedAt || Number ( row . signedPackages || 0 ))
2026-10-03 22:13:15 +08:00
|| attempt . requests . some ( row => row . status !== 'RESPONDED' || row . http !== 200 || id ( row . code ) !== '200' || ! row . requestId || ! row . traceId )
2026-10-20 10:35:52 +08:00
|| attempt . requests . length !== 3 || attempt . requests . some ( row => {
const original = originals . find ( item => item . deliveryNo === row . result ? . order ? . deliveryNo )
return row . method !== 'GET' || Object . hasOwn ( row . result || {}, 'replayed' ) || ! original
|| row . path !== `/api/platform/v1/last-leg/delivery-orders/ ${ original . deliveryNo } ?customerId= ${ proof . customerId } `
|| JSON . stringify ( row . result . order ) !== JSON . stringify ( original )
}) || new Set ( attempt . requests . map ( row => row . result ? . order ? . deliveryNo )). size !== 3
2026-10-03 22:13:15 +08:00
|| resources . fleetCostActors ? . status !== 'DISABLED' || failures ? . length !== 1 || failures [ 0 ]. name !== '模块执行'
|| failures [ 0 ]. evidence !== attempt . failure )
2026-10-20 10:35:52 +08:00
throw Error ( 'Return source recovery requires the exact native candidate failure, three already-delivering documents and no return/signature writes' )
2026-10-03 22:13:15 +08:00
return { resources , marker : proof . marker , returnSourceSelection : true }
}
2026-10-03 21:00:07 +08:00
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'
2026-10-03 20:06:02 +08:00
const costs = trip ? . costAcceptance , failures = report ? . steps ? . filter ( row => row . passed === false )
2026-10-03 21:00:07 +08:00
const failureName = vehicleCreated ? '接替车辆返回真实身份及车牌' : afterCosts ? '每张排车已发车且货量与客户起运仓一致' : '独立预期覆盖全部实物且没有重叠托盘'
2026-10-03 20:24:56 +08:00
const handover = trip ? . handover
2026-10-03 21:00:07 +08:00
const creation = vehicleCreated ? handover ? . requests ? . filter ( row => row . method !== 'GET' ) : null
if ( vehicleCreated && ( creation ? . length !== 1 || creation [ 0 ]. method !== 'POST'
|| creation [ 0 ]. path !== '/api/platform/v1/last-leg/master-data/vehicles' || creation [ 0 ]. status !== 'RESPONDED'
|| creation [ 0 ]. http !== 200 || id ( creation [ 0 ]. code ) !== '200' || ! creation [ 0 ]. requestId || ! creation [ 0 ]. traceId
|| ! /^[1-9]\d*$/ . test ( id ( creation [ 0 ]. result ? . id )) || id ( creation [ 0 ]. result . recordVersion ) !== '0'
|| creation [ 0 ]. result . plateNumber !== creation [ 0 ]. body ? . plateNumber ? . toUpperCase ()
|| creation [ 0 ]. result . status !== 'IN_SERVICE' || handover . replacement ? . driverId ))
throw Error ( 'Created vehicle recovery requires the exact definitive successful creation and no later handover write' )
2026-10-03 20:24:56 +08:00
if ( report ? . success !== false || proof ? . stage !== ( afterCosts ? 'IN_TRANSIT_HANDOVER' : 'TRIP_OPERATING_COSTS' ) || proof . status !== 'CHECKING'
2026-10-20 10:35:52 +08:00
|| proof . marker !== costs ? . marker || ! nativeDeparture ( proof . departure , proof . dispatchNo )
2026-10-03 20:06:02 +08:00
|| ! proof . departureRequest ? . idempotencyKey || ! costs ? . departure ? . snapshot ? . confirmedLoad ? . manifestHash
2026-10-03 20:24:56 +08:00
|| costs . status !== ( afterCosts ? 'VERIFIED' : 'FAILED' ) || ! Array . isArray ( costs . costs ) || costs . costs . length !== ( afterCosts ? 2 : 0 ) || ! costs . requests ? . length
|| costs . requests . some ( row => row . status !== 'RESPONDED' || ! row . requestId || ! row . traceId
|| ( afterCosts ? ! [ 200 , 403 ]. includes ( row . http ) || ! [ '200' , 'A0403' , 'A0409' ]. includes ( id ( row . code )) : row . method !== 'GET' || row . http !== 200 || id ( row . code ) !== '200' ))
|| ( afterCosts ? handover ? . status !== 'FAILED' || ! handover . requests ? . length || handover . confirmRequest || handover . selection
2026-10-03 21:00:07 +08:00
|| handover . requests . some ( row => ( row . method !== 'GET' && row !== creation ? .[ 0 ]) || row . status !== 'RESPONDED' || row . http !== 200 || id ( row . code ) !== '200' )
2026-10-03 20:24:56 +08:00
|| ! costs . final ? . page || costs . final . page . records ? . length !== 2 : trip . handover )
|| trip . closure || proof . receipts || proof . closure || trip . returnRedispatch
2026-10-03 20:06:02 +08:00
|| resources . fleetCostActors ? . status !== 'DISABLED' || ! proof . picker ? . membershipId
2026-10-03 20:24:56 +08:00
|| failures ? . length !== 2 || ! failures . some ( row => row . name === failureName )
|| failures . some ( row => ! [ failureName , '模块执行' ]. includes ( row . name )))
2026-10-03 20:06:02 +08:00
throw Error ( 'Departed checkpoint must be the reviewed read-only cost assertion failure, with no later business writes' )
2026-10-03 21:00:07 +08:00
return { resources , marker : proof . marker , departed : true , costsAlreadyVerified : afterCosts ,
createdVehicle : vehicleCreated ? { snapshot : creation [ 0 ]. result , creationRequest : creation [ 0 ] } : null }
2026-10-03 20:06:02 +08:00
}
2026-10-03 06:26:33 +08:00
if ( stage === 'TERMINAL_READONLY' ) {
const failures = report ? . steps ? . filter ( row => row . passed === false )
if ( report ? . success !== false || proof ? . stage !== 'RECEIPTS_AND_RESOURCE_RELEASE'
|| trip ? . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'
|| proof . closure ? . status !== trip . closure . status || trip . externalReceipt ? . status !== 'VERIFIED'
2026-10-20 10:35:52 +08:00
|| trip . externalDispatch ? . status !== 'VERIFIED' || trip . externalDispatch . dispatchNo !== proof . dispatchNo || trip . reassignment ? . status !== 'REJECTED_WITHOUT_MUTATION'
2026-10-03 06:26:33 +08:00
|| ! proof . receipts ? . length || proof . finance ? . status !== 'CONSUMER_PERSISTED'
|| ! failures ? . length || failures . some ( row => ! [ '本次车次货物结案及车辆司机释放完成' , '模块执行' ]. includes ( row . name )))
throw Error ( 'Terminal reconciliation requires completed original business proofs and only the known aggregate assertion failure' )
return { resources , marker : proof . marker , terminal : true }
}
if ( stage === 'EXTERNAL_PALLETIZED' || stage === 'EXTERNAL_RECEIPT_CONFIRMED' ) {
const confirmed = stage === 'EXTERNAL_RECEIPT_CONFIRMED'
const partial = resources ? . fleetFeeCoverageCases ? . find ( row => row . mode === 'PARTIAL' ), receipt = trip ? . externalReceipt
if ( report ? . success !== false || proof ? . stage !== 'EXTERNAL_RECEIPT' || proof . status !== 'CHECKING'
2026-10-20 10:35:52 +08:00
|| full ? . proof ? . status !== 'VERIFIED' || proof . fullExecution ? . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial ? . proof || partial ? . dispatchNo !== trip ? . dispatchNo
2026-10-03 06:26:33 +08:00
|| trip . reservation ? . status !== 'VERIFIED' || receipt ? . status !== 'CHECKING' || trip . externalDispatch
|| ( confirmed ? ( ! receipt . receiptRequest ? . idempotencyKey || ! receipt . requests ? . some ( row => row . label === '运营按实际1托位和新单价30确认实收' && row . method === 'POST' && row . http === 200 && String ( row . code ) === '200' )) : receipt . receiptRequest )
|| ! receipt . palletizeRequest ? . idempotencyKey || ! receipt . warehousePalletId || ! proof . picker ? . membershipId || ! proof . pickerMarker
|| ! receipt . requests ? . some ( row => row . label === 'PDA正式完成外接收货及库存入账' && row . method === 'POST' && row . http === 200 && String ( row . code ) === '200' ))
throw Error ( 'Palletized checkpoint requires the exact successful PDA command and no subsequent receipt confirmation' )
return { resources , marker : proof . marker , receiptReady : true , palletized : ! confirmed , receiptConfirmed : confirmed }
}
if ( stage === 'EXTERNAL_RECEIPT_READY' ) {
const partial = resources ? . fleetFeeCoverageCases ? . find ( row => row . mode === 'PARTIAL' )
if ( report ? . success !== false || proof ? . stage !== 'RESERVATIONS' || proof . status !== 'CHECKING'
2026-10-20 10:35:52 +08:00
|| full ? . proof ? . status !== 'VERIFIED' || proof . fullExecution ? . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial ? . proof || partial ? . dispatchNo !== trip ? . dispatchNo
2026-10-03 06:26:33 +08:00
|| trip . reservation ? . status !== 'VERIFIED' || trip . externalReceipt || trip . externalDispatch || proof . picker
|| ! report . steps ? . some ( row => row . name === '创建海外PDA仓库操作员71' && row . code === 'A0409' && row . message === '手机号已存在' ))
throw Error ( 'Receipt-ready checkpoint requires verified coverage/reservations and an explicitly rejected operator creation with no receipt writes' )
return { resources , marker : proof . marker , receiptReady : true }
}
if ( report ? . success !== false || proof ? . stage !== 'FULL_COVERAGE' || proof . status !== 'CHECKING' || full ? . mode !== 'FULL'
2026-10-20 10:35:52 +08:00
|| full . marker !== proof . marker || full . dispatchNo === trip ? . dispatchNo || ! resources . fleetTrips . some ( t => t . dispatchNo === full . dispatchNo && t . tripNo === proof . fullTripNo )
|| ! [ 'CHECKING' , 'UNPROVEN' ]. includes ( full . proof ? . status ) || proof . fullExecution ? . departureRequest
2026-10-03 06:26:33 +08:00
|| ! full . proof . requests ? . length
2026-10-20 10:35:52 +08:00
|| full . proof . requests . some ( row => row . status !== 'RESPONDED' || ! row . requestId || ! row . traceId )
|| resources . fleetFeeCoverageCases . length !== 2 || ! proof . originalDispatch ? . detail || proof . originalDispatch . detail . dispatchNo !== proof . dispatchNo )
2026-10-03 06:26:33 +08:00
throw Error ( 'Only a reviewed read-only FULL_COVERAGE failure can resume; mutated/unknown outcomes require case-specific reconciliation' )
const persisted = stage === 'FULL_COVERAGE_PERSISTED'
2026-10-21 14:30:41 +08:00
const fullTrip = resources . fleetTrips . find ( row => row . dispatchNo === full . dispatchNo )
if ( persisted ? ( ! full . proof . outboundNo || ! full . proof . deliveryNos ? . length || ! fullTrip ? . loadingCommand ? . body ? . idempotencyKey
|| ! fullTrip . loadingCommand . body . measurements ? . length || fullTrip . loadingCommand . body . recordVersion == null
|| ! nativeDeparture ( fullTrip . loadingCommand . result , full . dispatchNo )
|| ! fullTrip . loadingCommand . afterReplay ? . main ? . documents ? . outbounds ? . some ( o => o . outboundNo === full . proof . outboundNo )
|| JSON . stringify ( fullTrip . loadingCommand . afterReplay . main . documents . deliveries . map ( d => d . deliveryNo ). sort ()) !== JSON . stringify ([... full . proof . deliveryNos ]. sort ()))
2026-10-03 06:26:33 +08:00
: ( full . proof . requests . some ( row => row . method !== 'GET' ) || full . proof . outboundNo ))
throw Error ( 'Mutated/unknown outcomes require the exact successful command and persisted document reconciliation' )
return { resources , marker : proof . marker , persisted }
}
export async function verifyFleetTerminalState ( context ) {
const proof = context . createdResources . fleetAcceptance
const trip = context . createdResources . fleetTrips . find ( row => row . tripNo === proof . tripNo )
const evidence = proof . terminalReadback = { status : 'CHECKING' , requests : [], deliveries : [], dispatches : [] }
2026-10-20 10:35:52 +08:00
const check = ( label , passed ) => { context . assert ( MODULE , label , null , Boolean ( passed ), '' , OPTIONS ); if ( ! passed ) throw Error ( label ) }
async function get ( path ) { const r = await context . request ( 'GET' , path ); evidence . requests . push ({ path , http : r . http , code : r . body ? . code , requestId : r . requestId , traceId : r . traceId }); check ( '终态只读回查成功' , success ( r ) && r . requestId && r . traceId ); return r . body . data }
check ( '原生结案助手已完成货物与资源核对' , proof . protocol === NATIVE_PROTOCOL && trip ? . dispatchNo === proof . dispatchNo && trip . closure ? . status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' )
const current = await get ( `/api/platform/v1/last-leg/fleet/trips/ ${ encodeURIComponent ( trip . tripNo ) } ` )
check ( '当前车次已完成且两项资源结束事实与原证明一致' , current . trip ? . status === 'COMPLETED'
&& current . trip . completedAt === trip . closure . finalTrip . completedAt && id ( current . trip . recordVersion ) === id ( trip . closure . finalTrip . recordVersion )
&& JSON . stringify ( current . trip . vehicleRelease ) === JSON . stringify ( trip . closure . finalTrip . vehicleRelease )
&& JSON . stringify ( current . trip . driverRelease ) === JSON . stringify ( trip . closure . finalTrip . driverRelease )
&& current . trip . vehicleRelease && current . trip . driverRelease && current . dispatches ? . length === 1 && current . dispatches [ 0 ]. dispatchNo === proof . dispatchNo )
evidence . trip = current . trip
const detail = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( proof . dispatchNo ) } ` )
check ( '原生主单全部正常派送完成且数量守恒' , detail . dispatchNo === proof . dispatchNo && detail . status === 'ACTIVE' && detail . stage === 'DELIVERED'
&& Number ( detail . counts ? . packageCount ) === Number ( detail . counts ? . shippedPackageCount ) && Number ( detail . counts ? . pickedPackageCount ) === Number ( detail . counts ? . shippedPackageCount )
2026-10-21 14:30:41 +08:00
&& Number ( detail . counts ? . shippedPackageCount ) === Number ( proof . departureSnapshot . detail . counts . shippedPackageCount ))
2026-10-20 10:35:52 +08:00
evidence . dispatches . push ( detail )
const refs = nativeFleetDeliveries ( detail )
check ( '签收证明恰好覆盖主单每客户派送一次' , JSON . stringify ( proof . receipts . map ( r => ` ${ r . customerId } : ${ r . deliveryNo } ` ). sort ()) === JSON . stringify ( refs . map ( r => ` ${ r . customerId } : ${ r . deliveryNo } ` ). sort ()))
for ( const ref of refs ){ const delivery = await get ( `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( ref . deliveryNo ) } ?customerId= ${ encodeURIComponent ( id ( ref . customerId )) } ` )
check ( '每张原派送保留客户和真实签收时间' , delivery . order ? . deliveryNo === ref . deliveryNo && id ( delivery . order . customerId ) === id ( ref . customerId ) && delivery . order . status === 'DELIVERED'
&& delivery . order . signedAt === proof . receipts . find ( r => r . deliveryNo === ref . deliveryNo && r . customerId === id ( ref . customerId )). signedAt ); evidence . deliveries . push ( delivery . order )}
const probe = trip . closure . nextDispatch
check ( '原生空排车占用保护证明完整' , trip . closure . subsequentTripClaimProtection === 'VERIFIED_NATIVE_EMPTY_DISPATCH' && probe ? . dispatchNo && probe . finalStatus === 'CANCELLED' )
const cancelled = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( probe . dispatchNo ) } ` )
check ( '本轮资源核对原生空排车已取消' , cancelled . dispatchNo === probe . dispatchNo && cancelled . status === 'CANCELLED' && Number ( cancelled . counts ? . packageCount ) === 0 && Number ( cancelled . counts ? . pickedPackageCount ) === 0 )
const customerId = id ( proof . finance . customerId ), pool = await get ( `/api/platform/finance/v1/fee-pool?customerId= ${ encodeURIComponent ( customerId ) } &direction=RECEIVABLE` )
const fees = pool . filter ( row => id ( row . sourceId ) === id ( proof . finance . source . sourceFeeId ) && id ( row . sourceVersion ) === id ( proof . finance . source . sourceVersion ))
check ( '原外接应收仍唯一且金额正确' , fees . length === 1 && Number ( fees [ 0 ]. amount ) === 30 && fees [ 0 ]. currency === 'CNY' && id ( fees [ 0 ]. customerId ) === customerId && fees [ 0 ]. sourceType === 'OVERSEAS_DELIVERY_FEE' )
evidence . fees = fees ; evidence . status = 'VERIFIED' ; proof . status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' ; proof . stage = 'FINISHED' ; return proof
2026-10-03 06:26:33 +08:00
}
2026-10-03 20:06:02 +08:00
/** All business checks happen before creating replacement test sessions or issuing a business command. */
2026-10-03 22:13:15 +08:00
export async function verifyDepartedRecovery ( context , { costsAlreadyVerified = false , createdVehicle , returnSourceReady = false } = {}) {
2026-10-03 20:06:02 +08:00
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
2026-10-03 22:13:15 +08:00
const expectedTrip = returnSourceReady ? trip . handover . finalTrip : snapshot . trip
2026-10-03 20:06:02 +08:00
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
const check = ( ok , message ) => { if ( ! ok ) throw Error ( `Departed recovery refused: ${ message } ` ) }
const same = ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b )
async function get ( path ) {
const r = await context . request ( 'GET' , path )
evidence . requests . push ({ path , http : r . http , code : r . body ? . code , requestId : r . requestId , traceId : r . traceId })
check ( success ( r ) && r . requestId && r . traceId , 'read-only response not definitive' )
return r . body . data
}
const current = await get ( base )
2026-10-03 22:13:15 +08:00
check ( current . trip ? . status === 'IN_TRANSIT' && id ( current . trip . recordVersion ) === id ( expectedTrip . recordVersion )
2026-10-21 14:30:41 +08:00
&& sameInstant ( current . trip . departedAt , proof . departureSnapshot . detail . dispatchInfo . departedAt ) && id ( current . trip . warehouseId ) === id ( proof . warehouseId )
2026-10-03 22:13:15 +08:00
&& id ( current . trip . vehicleId ) === id ( expectedTrip . vehicleId ) && id ( current . trip . driverId ) === id ( expectedTrip . driverId )
2026-10-03 20:06:02 +08:00
&& ! 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 )
2026-10-03 22:13:15 +08:00
&& 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
}
2026-10-03 20:06:02 +08:00
const costs = await get ( ` ${ base } /costs?page=1&size=100` ), handovers = await get ( ` ${ base } /handovers` )
2026-10-03 20:24:56 +08:00
check (( costsAlreadyVerified ? same ( costs , trip . costAcceptance . final )
: costs . page ? . records ? . length === 0 && Number ( costs . page . total ) === 0 && costs . summary ? . totals ? . length === 0 )
2026-10-03 20:06:02 +08:00
&& costs . summary . manifestHash === load . manifestHash && id ( costs . summary . departureVersion ) === id ( original . recordVersion ), 'costs already written or cargo basis changed' )
2026-10-03 22:13:15 +08:00
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' )
2026-10-20 10:35:52 +08:00
check ( proof . protocol === NATIVE_PROTOCOL && nativeDeparture ( proof . departure , proof . dispatchNo )
&& trip . dispatchNo === proof . dispatchNo && current . dispatches ? . length === 1 && current . dispatches [ 0 ]. dispatchNo === proof . dispatchNo
&& JSON . stringify ( proof . dispatchNos ) === JSON . stringify ([ proof . dispatchNo ]), 'trip dispatch binding changed' )
const detail = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( proof . dispatchNo ) } ` )
const items = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( proof . dispatchNo ) } /items` )
const cargo = load . cargo
const facts = rows =>{
const totals = new Map ()
for ( const row of rows ){ const identity = JSON . stringify ([ id ( row . dispatchItemId ), id ( row . warehousePalletId ), id ( row . customerId )]); totals . set ( identity ,( totals . get ( identity ) || 0 ) + Number ( row . scheduledPackages ?? row . packages ))}
return [... totals ]. sort (([ a ],[ b ])=> a . localeCompare ( b ))
2026-10-03 20:06:02 +08:00
}
2026-10-20 10:35:52 +08:00
check ( detail . dispatchNo === proof . dispatchNo && detail . status === 'ACTIVE' && detail . stage === 'DELIVERING' && items . dispatchNo === proof . dispatchNo
&& id ( items . recordVersion ) === id ( detail . recordVersion ) && Number ( detail . counts ? . packageCount ) === Number ( detail . counts ? . shippedPackageCount )
&& Number ( detail . counts ? . pickedPackageCount ) === Number ( detail . counts ? . shippedPackageCount ) && cargo . every ( row => row . dispatchNo === proof . dispatchNo )
&& same ( facts ( items . items ), facts ( cargo )) && proof . departureSnapshot ? . detail ? . dispatchNo === proof . dispatchNo
&& same ( items , proof . departureSnapshot . items ), 'dispatch shipped cargo changed' )
evidence . dispatchDetails . push ({ detail , items })
const refs = nativeFleetDeliveries ( detail )
check ( new Set ( refs . map ( d => d . deliveryNo )). size === refs . length
&& deliveryFacts ( refs ) === deliveryFacts ( nativeFleetDeliveries ( proof . departureSnapshot . detail )), 'duplicate or changed delivery identities' )
for ( const ref of refs ){
const response = await get ( `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( ref . deliveryNo ) } ?customerId= ${ encodeURIComponent ( id ( ref . customerId )) } ` ), order = response . order
const expected = ( returnSourceReady ? trip . returnRedispatch . originalDeliveries : proof . departureDeliveries ) ? . find ( d => d . deliveryNo === ref . deliveryNo && id ( d . customerId ) === id ( ref . customerId ))
check ( expected && order ? . deliveryNo === ref . deliveryNo && id ( order . customerId ) === id ( ref . customerId ) && order . dispatchNo === proof . dispatchNo
&& order . outboundNo === ref . outboundNo && order . status === 'DELIVERING' && id ( order . version ) === id ( expected . version )
&& order . outboundNo === expected . outboundNo && Number ( order . packageCount ) === Number ( ref . packageCount ) && Number ( order . packageCount ) === Number ( expected . packageCount )
&& ( ! 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 ( r => Number ( r . fileCount ) === 0 ), 'delivery has started, changed or been signed' )
2026-10-03 20:06:02 +08:00
evidence . deliveries . push ( order )
}
2026-10-21 14:30:41 +08:00
check ( evidence . deliveries . reduce (( n , row )=> n + Number ( row . packageCount ), 0 ) === Number ( proof . departureSnapshot . detail . counts . shippedPackageCount ), 'delivery quantity mismatch' )
2026-10-03 21:00:07 +08:00
if ( createdVehicle ) {
const vehicle = await get ( `/api/platform/v1/last-leg/master-data/vehicles/ ${ encodeURIComponent ( createdVehicle . snapshot . id ) } ` )
const canonical = value => value && typeof value === 'object' && ! Array . isArray ( value )
? Object . fromEntries ( Object . keys ( value ). sort (). map ( key => [ key , canonical ( value [ key ])])) : value
check ( same ( canonical ( vehicle ), canonical ( createdVehicle . snapshot )), 'created vehicle snapshot changed' )
check ( typeof context . context ? . readReviewedVehicleOccupation === 'function' , 'vehicle occupancy reader missing' )
const occupation = await context . context . readReviewedVehicleOccupation ( vehicle . id )
check ( id ( occupation ? . vehicleId ) === id ( vehicle . id ) && occupation . assignmentCount === 0 , 'created vehicle is occupied' )
evidence . reviewedVehicle = { ... createdVehicle , snapshot : vehicle , occupation }
}
2026-10-03 20:06:02 +08:00
Object . assign ( evidence , { status : 'VERIFIED' , tripVersion : current . trip . recordVersion , manifestHash : load . manifestHash ,
departure : departures [ 0 ], costSummary : costs . summary })
await context . checkpoint ()
2026-10-03 21:00:07 +08:00
return { trip , dispatchDetails : evidence . dispatchDetails , reviewedVehicle : evidence . reviewedVehicle }
2026-10-03 20:06:02 +08:00
}
2026-10-03 21:00:07 +08:00
export async function readReviewedVehicleOccupation ( env , vehicleId , fetcher = globalThis . fetch ) {
validateEvidenceConfig ( env )
if ( ! /^[1-9]\d*$/ . test ( id ( vehicleId ))) throw Error ( 'Exact numeric reviewed vehicle ID 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('vehicleId',' ${ vehicleId } ','assignmentCount',COUNT(*)) FROM ll_fleet_resource_assignment WHERE resource_kind='VEHICLE' AND resource_id= ${ vehicleId } ;` , '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 ( 'Reviewed vehicle occupancy HTTP failure' )
const lines = ( await response . text ()). split ( /\r?\n/ ). filter ( line => line . startsWith ( 'FBA_CORE_JSON=' ))
if ( lines . length !== 1 ) throw Error ( 'Reviewed vehicle occupancy response ambiguous' )
const result = JSON . parse ( lines [ 0 ]. slice ( 'FBA_CORE_JSON=' . length ))
if ( result . ok !== true || result . rows ? . length !== 1 ) throw Error ( 'Reviewed vehicle occupancy query failed' )
return result . rows [ 0 ]
}
2026-10-03 22:13:15 +08:00
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 ,
2026-10-20 10:35:52 +08:00
pickerToken : picker . token , dispatchNos : run . dispatchNos , deliveryNos : nativeFleetDeliveries ( verified . dispatchDetails [ 0 ]. detail ). map ( ref => ref . deliveryNo ), sourceSelectionRecovery : true })
run . receipts = result . originalSignatures . map ( row => ({ customerId : id ( row . customerId || run . customerId ), deliveryNo : row . deliveryNo , signedAt : row . signedAt }))
2026-10-03 22:13:15 +08:00
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 ) }
}
2026-10-03 21:00:07 +08:00
export async function runFleetHandoverStage ( context , trip , pickerToken , dispatchDetails , { verify = verifyFleetHandover , onReady , reviewedVehicle } = {}) {
const state = await verify ( context , MODULE , trip , pickerToken , dispatchDetails , { reviewedVehicle })
2026-10-03 14:52:04 +08:00
const run = context . createdResources . fleetAcceptance
2026-10-20 10:35:52 +08:00
if ( state ? . status !== 'VERIFIED' || state . marker !== context . marker || run ? . tripNo !== trip . tripNo
|| ! nativeDeparture ( run . departure , trip . dispatchNo ) || ! state . originalDeparture ? . snapshot ? . confirmedLoad || ! state . browser
2026-10-03 14:52:04 +08:00
|| trip . costAcceptance ? . status !== 'VERIFIED' || trip . costAcceptance . marker !== context . marker )
throw Error ( 'Only the current verified DEPART and handover can publish a shared fixture' )
const fixture = { marker : context . marker , runId : context . context ? . fleetRunId , tripNo : trip . tripNo , warehouseId : trip . warehouseId , dispatchNos : state . dispatchNos ,
departure : run . departure , immutableDeparture : state . originalDeparture , handoverNo : state . handoverNo ,
originalResources : state . previous , replacementResources : state . replacement , browser : state . browser ,
costs : trip . costAcceptance ,
2026-10-03 20:06:02 +08:00
allocationStatus : trip . costAcceptance . expected ? . allocationStatus ,
allocationBlockReason : trip . costAcceptance . expected ? . allocationBlockReason ,
2026-10-03 14:52:04 +08:00
status : 'DEPART_AND_HANDOVER_VERIFIED' , requestCount : state . requestCount , pdaReadCount : state . pdaReadCount }
context . context ? . saveFleetHandoverFixture ? .( fixture )
if ( onReady ) await onReady ( fixture )
return fixture
}
export function fleetCostShares ( trip , dispatchDetails ) {
const external = trip . externalReceipt , externalDispatch = trip . externalDispatch
2026-10-20 10:35:52 +08:00
if ( external ? . status !== 'VERIFIED' || ! external . bookingNo || ! external . warehousePalletId
|| ! trip . dispatchNo || externalDispatch ? . dispatchNo !== trip . dispatchNo )
2026-10-03 14:52:04 +08:00
throw Error ( 'Cost expectations require this run\'s verified external receipt' )
2026-10-20 10:35:52 +08:00
const bundle = dispatchDetails ? . length === 1 ? dispatchDetails [ 0 ] : null
const detail = bundle ? . detail , items = bundle ? . items
if ( detail ? . dispatchNo !== trip . dispatchNo || items ? . dispatchNo !== trip . dispatchNo
|| id ( detail . recordVersion ) !== id ( items . recordVersion ) || ! items . items ? . length )
throw Error ( 'Cost expectations require the same native dispatch Detail and Items version' )
const pallets = [... new Set ( items . items . map ( item => id ( item . warehousePalletId )))]. sort ()
const measured = trip . confirmedLoad ? . pallets
if ( pallets . some ( value => ! value ) || ! trip . confirmedLoad ? . manifestHash || ! Array . isArray ( measured )
|| measured . length !== pallets . length || new Set ( measured . map ( row => id ( row . warehousePalletId ))). size !== measured . length
|| measured . some ( row => ! pallets . includes ( id ( row . warehousePalletId )) || ! Number . isSafeInteger ( row . palletSpaces ) || row . palletSpaces < 1 ))
throw Error ( 'Cost expectations require exact actual loading measurements for every distinct pallet' )
const externalId = id ( external . warehousePalletId ), own = pallets . filter ( value => value !== externalId )
if ( ! pallets . includes ( externalId ) || ! own . length ) throw Error ( 'External receipt and own cargo require distinct actual pallets' )
const share = ( sourceType , businessNo , ids ) => ({ businessNo , sourceType , dispatchNo : trip . dispatchNo ,
warehousePalletIds : ids , palletSpaces : measured . filter ( row => ids . includes ( id ( row . warehousePalletId )))
. reduce (( sum , row ) => sum + row . palletSpaces , 0 ) })
return [ share ( 'OWN' , trip . dispatchNo , own ), share ( 'EXTERNAL' , external . bookingNo , [ externalId ])]
2026-10-03 14:52:04 +08:00
}
2026-10-03 23:58:47 +08:00
export function fleetCostActorJournalData ( value ) {
const clean = redactEvidence ( value )
function removeCredentials ( item ) {
if ( Array . isArray ( item )) return item . map ( removeCredentials )
if ( ! item || typeof item !== 'object' ) return item
const result = {}, forbidden = /^(?:password|newPassword|temporaryPassword|accessToken|refreshToken|authorization)$/i
let removed = false
for ( const [ name , field ] of Object . entries ( item )) {
if ( forbidden . test ( name )) removed = true
else result [ name ] = removeCredentials ( field )
}
if ( removed ) result . credentialFieldsRemoved = true
return result
}
return removeCredentials ( clean )
}
2026-10-03 14:52:04 +08:00
export async function prepareFleetCostActors ( context ) {
if ( ! context . marker || context . createdResources . fleetCostActors || typeof context . checkpoint !== 'function' )
throw Error ( 'Cost actors require fresh current-run ownership and durable checkpoints' )
const proof = context . createdResources . fleetCostActors = { marker : context . marker , status : 'CREATING' , actors : [], requests : [] }
const base = '/api/platform/v1/iam' , sessions = []
async function request ( method , path , body , token ) {
2026-10-03 23:58:47 +08:00
const entry = { method , path , body : fleetCostActorJournalData ( body ), status : 'SENDING' }
2026-10-03 14:52:04 +08:00
proof . requests . push ( entry ); await context . checkpoint ()
try {
const response = await context . request ( method , path , body , token )
Object . assign ( entry , { status : 'RESPONDED' , http : response ? . http , code : response ? . body ? . code ,
2026-10-03 23:58:47 +08:00
requestId : response ? . requestId , traceId : response ? . traceId , result : fleetCostActorJournalData ( response ? . body ? . data ) })
2026-10-03 14:52:04 +08:00
await context . checkpoint ()
if ( ! success ( response ) || ! response . requestId || ! response . traceId ) throw Error ( `Cost actor request failed: ${ method } ${ path } ` )
return response . body . data
} catch ( error ) {
if ( entry . status === 'SENDING' ) { entry . status = 'UNKNOWN_RESULT' ; await context . checkpoint () }
throw error
}
}
async function close () {
const errors = []
for ( const actor of proof . actors ) {
try {
if ( actor . membershipId && ! actor . userDisabled ) {
const result = await request ( 'PUT' , ` ${ base } /users/ ${ actor . membershipId } /status` , { status : 'DISABLED' })
if ( result ? . status !== 'DISABLED' ) throw Error ( 'Cost test account was not disabled' )
actor . userDisabled = true
}
if ( actor . roleId && ! actor . roleDisabled ) {
const result = await request ( 'PUT' , ` ${ base } /roles/ ${ actor . roleId } /status` , { status : 'DISABLED' })
if ( result ? . status !== 'DISABLED' ) throw Error ( 'Cost test role was not disabled' )
actor . roleDisabled = true
}
} catch ( error ) { errors . push ( error ) }
}
proof . status = errors . length ? 'CLEANUP_FAILED' : 'DISABLED' ; await context . checkpoint ()
if ( errors . length ) throw new AggregateError ( errors , 'Cost actor cleanup requires review; do not recreate accounts' )
}
try {
const permissions = await request ( 'GET' , ` ${ base } /permissions` )
const list = permissions . filter ( row => row . permissionCode === 'platform:fleet:trip:list' && row . status === 'ACTIVE' )
if ( list . length !== 1 || ! list [ 0 ]. id ) throw Error ( 'Active trip list permission missing or ambiguous' )
for ( const kind of [ 'reader' , 'denied' ]) {
const suffix = randomUUID (). replaceAll ( '-' , '' ). slice ( 0 , 16 ), username = `fleet_ ${ kind } _ ${ suffix } `
const actor = { kind , username }; proof . actors . push ( actor )
const roleCode = `FLEET_ ${ kind . toUpperCase () } _ ${ suffix . toUpperCase () } `
const role = await request ( 'POST' , ` ${ base } /roles` , { roleCode , roleName : ` ${ context . marker } - ${ kind } ` , description : context . marker })
if ( ! id ( role ? . id ) || role . roleCode !== roleCode || role . roleType !== 'CUSTOM' ) throw Error ( 'Fresh cost role ownership mismatch' )
actor . roleId = id ( role . id )
await request ( 'PUT' , ` ${ base } /roles/permissions` , { roleId : actor . roleId , permissionIds : kind === 'reader' ? [ id ( list [ 0 ]. id )] : [] })
const password = `Fleet ${ randomUUID (). replaceAll ( '-' , '' ) } A9`
const user = await request ( 'POST' , ` ${ base } /users` , { username , displayName : ` ${ context . marker } - ${ kind } ` ,
2026-10-03 20:13:50 +08:00
phone : await findFleetActorPhone ( context , suffix ), email : ` ${ username } @example.com` , tenantId : '0' ,
2026-10-03 14:52:04 +08:00
roleIds : [ actor . roleId ], password , status : 'ACTIVE' })
if ( ! id ( user ? . membershipId ) || user . username !== username || id ( user . tenantId ) !== '0'
|| user . roleIds ? . length !== 1 || id ( user . roleIds [ 0 ]) !== actor . roleId ) throw Error ( 'Fresh cost account ownership mismatch' )
actor . membershipId = id ( user . membershipId )
const login = await request ( 'POST' , '/api/auth/login' , { loginId : username , password }, '' )
if ( ! login ? . accessToken || login . accessToken === context . token || sessions . includes ( login . accessToken )) throw Error ( 'Independent cost session missing' )
sessions . push ( login . accessToken )
const actual = await request ( 'GET' , ` ${ base } /authorization/permissions` , undefined , login . accessToken )
const expected = kind === 'reader' ? [ 'platform:fleet:trip:list' ] : []
if ( ! Array . isArray ( actual ) || JSON . stringify ([... actual ]. sort ()) !== JSON . stringify ( expected )) throw Error ( 'Cost actor effective permissions differ from exact scope' )
}
proof . status = 'READY' ; await context . checkpoint ()
return { marker : context . marker , listOnlyToken : sessions [ 0 ], deniedToken : sessions [ 1 ], close }
} catch ( error ) {
proof . status = 'FAILED' ; await context . checkpoint ()
try { await close () } catch ( cleanupError ) { throw new AggregateError ([ error , cleanupError ], 'Cost actor creation and cleanup failed' ) }
throw error
}
}
2026-10-03 20:13:50 +08:00
export async function findFleetActorPhone ( context , actorSuffix ) {
if ( ! context . marker || ! /^[a-f0-9]{16}$/ . test ( actorSuffix )) throw Error ( 'Fresh actor phone scope required' )
for ( let attempt = 0 ; attempt < 100 ; attempt ++ ) {
const number = createHash ( 'sha256' ). update ( ` ${ context . marker } : ${ actorSuffix } : ${ attempt } ` ). digest (). readUInt32BE ( 0 )
const phone = `18 ${ String ( number ). padStart ( 9 , '0' ). slice ( - 9 ) } `
const users = await context . request ( 'GET' , `/api/platform/v1/iam/users? ${ new URLSearchParams ({ page : '1' , size : '20' , keyword : phone } )}` )
if ( ! success ( users ) || ! users . requestId || ! users . traceId || ! Array . isArray ( users . body . data ? . records )
|| ! Number . isSafeInteger ( Number ( users . body . data . total )) || Number ( users . body . data . total ) !== users . body . data . records . length )
throw Error ( 'Cannot verify platform actor phone availability' )
if ( users . body . data . records . some ( row => row . phone === phone )) continue
const customer = await context . request ( 'GET' , `/api/platform/v1/iam/tenants/administrator-phone-availability?phone= ${ encodeURIComponent ( phone ) } ` )
if ( ! success ( customer ) || ! customer . requestId || ! customer . traceId ) throw Error ( 'Cannot verify customer administrator phone availability' )
if ( customer . body . data === true ) return phone
}
throw Error ( 'No available phone in the fresh actor scope; do not replay user creation' )
}
2026-10-20 10:35:52 +08:00
async function nativeRequest ( context , method , path , body , token ) {
const result = await context . request ( method , path , body , token )
if ( ! success ( result ) || ! result . requestId || ! result . traceId ) throw Error ( `Native fleet request failed: ${ method } ${ path } ` )
return result . body . data
}
export async function readNativeFleetBundle ( context , dispatchNo ) {
const base = `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( dispatchNo ) } `
const detail = await nativeRequest ( context , 'GET' , base )
const items = await nativeRequest ( context , 'GET' , ` ${ base } /items` )
if ( detail . dispatchNo !== dispatchNo || items . dispatchNo !== dispatchNo || id ( dispatchVersion ( detail . recordVersion )) !== id ( dispatchVersion ( items . recordVersion ))) throw Error ( 'Native Detail/Items version mismatch' )
return { detail , items }
}
// 只读取本轮生产者和运营已确认实测,不从计划托位、计费重量或常数生成实际量。
export async function currentFleetMeasurements ( context , trip ) {
const path = `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( trip . dispatchNo ) } /loading`
const preview = await nativeRequest ( context , 'GET' , path )
if ( preview . dispatchNo !== trip . dispatchNo || preview . status !== 'ACTIVE' || ! preview . pallets ? . length ) throw Error ( 'Current native physical loading preview missing' )
const producer = context . createdResources . phase4OverseasDispatch ? . loadingMeasurements
if ( producer && ( producer . marker !== context . marker || producer . environment !== 'TEST' || producer . dispatchNo !== trip . dispatchNo || ! Array . isArray ( producer . pallets )))
throw Error ( 'Physical measurements must be the exact current-run producer receipt for this dispatch' )
const actual = ( producer ? . pallets || []). map ( row => structuredClone ( row ))
const receipt = trip . externalReceipt ? . finalBooking ? . receipt
for ( const measured of receipt ? . pallets || []) {
const source = receipt . source ? . pallets ? . find ( p => id ( p . warehousePalletId ) === id ( measured . warehousePalletId ))
if ( ! source || ! Number . isFinite ( Date . parse ( receipt . confirmedAt )) || ! receipt . operatorId || ! receipt . source ? . incomingNo ) throw Error ( 'External actual measurement ownership missing' )
actual . push ({ warehousePalletId : id ( measured . warehousePalletId ), palletSpaces : measured . palletSpaces ,
weightKg : measured . weightKg ?? source . weightKg , volumeCbm : measured . volumeCbm ?? source . volumeCbm ,
deliveryIndex : trip . stops . findIndex ( s => s . destinationKey === trip . externalReceipt . finalBooking . cargo . destinationKey ),
evidence : [ 'palletSpaces' , 'weightKg' , 'volumeCbm' ]. map ( field => ({ field , value : field === 'palletSpaces' ? measured [ field ] : measured [ field ] ?? source [ field ],
actual : true , sourceType : 'FLEET_RECEIPT_CONFIRMATION' , sourceBusinessNo : receipt . source . incomingNo , observedAt : receipt . confirmedAt })) })
}
if ( new Set ( actual . map ( p => id ( p . warehousePalletId ))). size !== actual . length ) throw Error ( 'Duplicate current-run physical measurement receipts' )
for ( const pallet of preview . pallets ) {
const supplied = actual . find ( p => id ( p . warehousePalletId ) === id ( pallet . warehousePalletId ))
for ( const field of [ 'palletSpaces' , 'weightKg' , 'volumeCbm' ]) {
const source = pallet [ field ] == null ? supplied : pallet , value = source ? .[ field ]
if ( ! ( Number ( value ) > 0 ) || ! source ? . evidence ? . some ( e => e . field === field && e . actual === true && e . sourceBusinessNo && e . sourceType
&& Number . isFinite ( Date . parse ( e . observedAt )) && Number ( e . value ) === Number ( value )))
throw Error ( `Missing current-run actual ${ field } for ${ pallet . palletNo || 'unassociated pallet' } ; no planning constants or external receipt injection` )
}
const index = pallet . deliveryIndex ?? supplied ? . deliveryIndex
if ( ! Number . isSafeInteger ( index ) || index < 1 || ! trip . stops ? .[ index ] ? . destinationKey )
throw Error ( 'Current-run physical pallet has no verified unloading stop' )
}
if ( actual . some ( p => ! preview . pallets . some ( row => id ( row . warehousePalletId ) === id ( p . warehousePalletId ))
&& ! receipt ? . source ? . pallets ? . some ( row => id ( row . warehousePalletId ) === id ( p . warehousePalletId )))) throw Error ( 'Physical receipts contain a pallet outside this dispatch and its confirmed incoming' )
return { environment : 'TEST' , marker : context . marker , dispatchNo : trip . dispatchNo , pallets : actual }
}
export async function departNativeFleet ( context , trip , pickerToken , run ) {
if ( ! pickerToken || ! run || run . dispatchNo !== trip . dispatchNo || run . tripNo !== trip . tripNo || run . departureRequest )
throw Error ( 'Native departure requires owned main and a new original command' )
const path = `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( trip . dispatchNo ) } `
const after = await readNativeFleetBundle ( context , trip . dispatchNo )
2026-10-21 14:30:41 +08:00
const refs = nativeFleetDeliveries ( after . detail ), original = trip . loadingCommand
if ( ! nativeDeparture ( original ? . result , trip . dispatchNo ) || ! original . body ? . idempotencyKey
|| after . detail . status !== 'ACTIVE' || after . detail . stage !== 'DELIVERING'
|| id ( after . detail . recordVersion ) !== id ( original . result . recordVersion )
|| Number ( after . detail . counts ? . shippedPackageCount ) !== Number ( after . detail . counts ? . packageCount )
|| refs . reduce (( sum , d ) => sum + Number ( d . packageCount ), 0 ) !== Number ( after . detail . counts . packageCount ))
throw Error ( 'Automatic loading completion must have an original success receipt and complete real delivery documents' )
const departed = await nativeRequest ( context , 'GET' , `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( trip . dispatchNo ) } /commands/ ${ encodeURIComponent ( original . body . idempotencyKey ) } ` , undefined , pickerToken )
if ( ! nativeDeparture ( departed , trip . dispatchNo ) || id ( departed . recordVersion ) !== id ( original . result . recordVersion )
|| JSON . stringify ( departed . result ) !== JSON . stringify ( original . result . result )) throw Error ( 'Original loading command readback changed its outcome' )
run . departureRequest = original . body ; run . departure = departed ; run . departureReplay = original . replay
2026-10-20 10:35:52 +08:00
run . departureSnapshot = after ; run . dispatchNos = [ trip . dispatchNo ]
run . departureDeliveries = []
for ( const ref of nativeFleetDeliveries ( after . detail )) {
const value = await nativeRequest ( context , 'GET' , `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( ref . deliveryNo ) } ?customerId= ${ encodeURIComponent ( id ( ref . customerId )) } ` )
if ( value . order ? . deliveryNo !== ref . deliveryNo || id ( value . order . customerId ) !== id ( ref . customerId ) || value . order . dispatchNo !== trip . dispatchNo
|| value . order . status !== 'DELIVERING' || value . order . version == null || Number ( value . order . packageCount ) !== Number ( ref . packageCount )) throw Error ( 'Native departure delivery versions or owners changed' )
run . departureDeliveries . push ( value . order )
}
await context . checkpoint ? .(); return { departed , dispatchDetails : [ after ] }
}
2026-10-03 14:52:04 +08:00
export async function runFleetScenario ( context , { wait = ms => new Promise ( resolve => setTimeout ( resolve , ms )), onHandoverReady ,
costActors , resumeFull = false , resumePersisted = false , resumeReceiptReady = false , resumePalletized = false , resumeReceiptConfirmed = false , terminalReadOnly = false } = {}) {
2026-10-03 06:26:33 +08:00
if ( terminalReadOnly ) return verifyFleetTerminalState ( context )
2026-10-03 14:52:04 +08:00
if ( costActors ? . marker !== context . marker || ! costActors . listOnlyToken || ! costActors . deniedToken
|| costActors . listOnlyToken === costActors . deniedToken || [ costActors . listOnlyToken , costActors . deniedToken ]. includes ( context . token ))
throw Error ( 'Prepare two independent current-run platform cost permission sessions before starting the fleet scenario' )
2026-10-20 10:35:52 +08:00
const resumed = resumeFull || resumeReceiptReady
const proof = resumed ? context . createdResources . fleetAcceptance : context . createdResources . fleetAcceptance = {
protocol : NATIVE_PROTOCOL , marker : context . marker , status : 'CHECKING' , stage : 'SOURCES' , checkpoints : [] }
if ( proof . protocol !== NATIVE_PROTOCOL ) throw Error ( 'Legacy multi-dispatch reports cannot enter the native main chain' )
2026-10-03 06:26:33 +08:00
function stage ( value ) { proof . stage = value ; proof . checkpoints . push ({ stage : value , at : new Date (). toISOString () }); console . log ( `FLEET_STAGE= ${ value } ` ) }
const source = context . createdResources . phase4OverseasDispatch , receiving = context . createdResources . phase4OverseasPda
2026-10-20 10:35:52 +08:00
if ( source ? . marker !== context . marker || source . status !== 'VERIFIED' || ! source . dispatchNo || receiving ? . marker !== context . marker
|| receiving . finalStatus !== 'COMPLETED' || ! receiving . customerId || ! receiving . overseasWarehouseId ) throw Error ( 'Current-run native dispatch and real received warehouse evidence required' )
2026-10-03 06:26:33 +08:00
const customerId = id ( receiving . customerId ), warehouseId = id ( receiving . overseasWarehouseId )
2026-10-20 10:35:52 +08:00
Object . assign ( proof , { customerId , warehouseId , dispatchNo : source . dispatchNo , dispatchNos : [ source . dispatchNo ] })
const journal = dispatchIo ( context , MODULE , proof , 'FLEET-NATIVE' )
let full , partial , trip
const template = { plannedDeliveryAt : new Date ( Date . now () + 21600000 ). toISOString (), deliveryMethod : 'TRUCK_PALLET' , pickupMethod : 'WAREHOUSE_PICKUP' ,
loadingType : 'PALLET' , vehicleType : 'TRUCK' , vehicleSize : '本轮实际装载' , appointmentAccount : 'SELF' , remark : ` ${ context . marker } TEST原费用覆盖` ,
fees : [{ feeType : 'DELIVERY_FEE' , feeName : '未覆盖原入仓派送费' , direction : 'RECEIVABLE' , currency : 'CNY' , amount : 10 }] }
if ( ! resumed ) {
const original = await readDispatch ( journal , source . dispatchNo )
const candidate = selectNativeFleetFullSource ( context , original , await candidatePage ( journal , warehouseId ))
proof . originalDispatch = { detail : original . data , items : original . items , route : original . route }
proof . fullCreateRequest = createBody ( context . marker , 'FLEET-FULL-INDEPENDENT' , [ candidate ])
const created = await journal . command ( 'POST' , '/api/platform/v1/last-leg/dispatches' , proof . fullCreateRequest , '释放的本轮真实第三托创建独立FULL原生排车' )
await replayDispatch ( journal , '/api/platform/v1/last-leg/dispatches' , proof . fullCreateRequest , created , '独立FULL创建原键恢复' )
if ( ! created . dispatchNo || created . dispatchNo === source . dispatchNo ) throw Error ( 'FULL must be a distinct newly created native dispatch' )
const fullTrip = await prepareFleetTrip ( context , MODULE , created . dispatchNo , warehouseId )
trip = await prepareFleetTrip ( context , MODULE , source . dispatchNo , warehouseId )
Object . assign ( proof , { tripNo : trip . tripNo , fullTripNo : fullTrip . tripNo })
full = { marker : context . marker , mode : 'FULL' , dispatchNo : created . dispatchNo , customerId , transferBody : structuredClone ( template ) }
partial = { marker : context . marker , mode : 'PARTIAL' , dispatchNo : source . dispatchNo , customerId , transferBody : structuredClone ( template ) }
context . createdResources . fleetFeeCoverageCases = [ full , partial ]
proof . fullExecution = { protocol : NATIVE_PROTOCOL , marker : context . marker , dispatchNo : full . dispatchNo , tripNo : fullTrip . tripNo }
2026-10-03 06:26:33 +08:00
} else {
2026-10-20 10:35:52 +08:00
full = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'FULL' )
partial = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'PARTIAL' )
trip = context . createdResources . fleetTrips . find ( row => row . dispatchNo === proof . dispatchNo )
if ( ! full || ! partial || partial . dispatchNo !== trip ? . dispatchNo || full . dispatchNo === partial . dispatchNo ) throw Error ( 'Reviewed native FULL/PARTIAL original identities missing' )
if ( resumeFull && ! resumePersisted && JSON . stringify ( await readNativeFleetBundle ( context , full . dispatchNo )) !== JSON . stringify ( proof . fullBeforeAttempt ))
throw Error ( 'Read-only FULL checkpoint changed physical state; review the original commands before any new session or write' )
2026-10-03 06:26:33 +08:00
}
const access = await resolvePickerAccess ( context )
2026-10-20 10:35:52 +08:00
let picker
2026-10-03 06:26:33 +08:00
try {
2026-10-20 10:35:52 +08:00
if ( ! resumeReceiptReady ) {
const fullTrip = context . createdResources . fleetTrips . find ( t => t . dispatchNo === full . dispatchNo )
stage ( 'FULL_COVERAGE' )
if ( ! resumed ) proof . fullBeforeAttempt = await readNativeFleetBundle ( context , full . dispatchNo )
let fullPicker
try {
const child = Object . create ( context )
proof . fullExecution . pickerMarker = ` ${ context . marker } -FULL-PDA- ${ randomUUID (). slice ( 0 , 8 ) } `
child . marker = proof . fullExecution . pickerMarker
fullPicker = access ? await createPicker ( child , 161 , warehouseId , access ) : null
if ( ! fullPicker ? . token || ! fullPicker . membershipId ) throw Error ( 'Independent FULL requires its own bound-warehouse PDA' )
proof . fullExecution . picker = { membershipId : fullPicker . membershipId , username : fullPicker . username }
await context . checkpoint ? .()
if ( ! resumePersisted && full . proof ) { full . previousReadOnlyAttempts ??= []; full . previousReadOnlyAttempts . push ( full . proof ); delete full . proof }
await verifyFleetFeeCoverage ( context , MODULE , full , { reconcilePersisted : resumePersisted , pickerToken : fullPicker . token ,
actualMeasurements : resumePersisted ? undefined : await currentFleetMeasurements ( context , fullTrip ) })
const fullDeparted = await departNativeFleet ( context , fullTrip , fullPicker . token , proof . fullExecution )
proof . fullExecution . receipts = []
for ( const [ index , ref ] of nativeFleetDeliveries ( fullDeparted . dispatchDetails [ 0 ]. detail ). entries ()) {
const detail = await nativeRequest ( context , 'GET' , `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( ref . deliveryNo ) } ?customerId= ${ encodeURIComponent ( id ( ref . customerId )) } ` )
const signed = await saveReceiptAndComplete ( context , detail . order , id ( ref . customerId ), 161 + index )
if ( signed ? . order ? . status !== 'DELIVERED' || ! signed . order . signedAt ) throw Error ( 'FULL source requires actual customer POD completion' )
proof . fullExecution . receipts . push ({ customerId : id ( ref . customerId ), deliveryNo : ref . deliveryNo , signedAt : signed . order . signedAt })
}
proof . fullExecution . closure = await finishFleetTrip ( context , MODULE , fullTrip )
if ( proof . fullExecution . closure . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' ) throw Error ( 'FULL independent resources did not finish' )
} finally { await disablePicker ( context , fullPicker , 161 ) }
stage ( 'RESERVATIONS' ); await verifyFleetBookingReservation ( context , MODULE , trip )
2026-10-03 06:26:33 +08:00
}
2026-10-20 10:35:52 +08:00
if ( full . proof ? . status !== 'VERIFIED' || proof . fullExecution ? . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'
|| trip . reservation ? . status !== 'VERIFIED' || partial . proof ) throw Error ( 'PARTIAL must retain its unpicked real main after independent FULL closure and reservation' )
if ( resumePalletized || resumeReceiptConfirmed ) {
const child = Object . create ( context ); child . marker = proof . pickerMarker
const suffix = pickerFixtureSuffix ( child ), username = `pda_overseas_ ${ suffix . toLowerCase () } _71` . slice ( 0 , 64 )
const users = await nativeRequest ( context , 'GET' , `/api/platform/v1/iam/users? ${ new URLSearchParams ({ page : '1' , size : '200' , keyword : username } )}` )
const user = users . records ? . find ( row => id ( row . membershipId ) === id ( proof . picker ? . membershipId ) && row . username === username )
if ( ! user || user . protectedAccount || id ( user . tenantId ) !== '0' || id ( user . overseasWarehouseId ) !== warehouseId
|| user . roleIds ? . length !== 1 || id ( user . roleIds [ 0 ]) !== id ( access ? . role ? . id )) throw Error ( 'Reviewed PDA identity or warehouse changed' )
picker = { membershipId : user . membershipId , username , token : null }
const active = await nativeRequest ( context , 'PUT' , `/api/platform/v1/iam/users/ ${ encodeURIComponent ( id ( user . membershipId )) } /status` , { status : 'ACTIVE' })
if ( active . status !== 'ACTIVE' ) throw Error ( 'Original owned PDA not active' )
const login = await nativeRequest ( context , 'POST' , '/api/auth/login' , { loginId : username , password : `Pda ${ suffix } A719` }, '' )
picker . token = login . accessToken
} else {
const child = Object . create ( context ); proof . pickerMarker = ` ${ context . marker } -FPDA- ${ randomUUID (). slice ( 0 , 8 ) } ` ; child . marker = proof . pickerMarker
picker = access ? await createPicker ( child , 71 , warehouseId , access ) : null
}
if ( ! picker ? . token || ! picker . membershipId ) throw Error ( 'Fresh bound-warehouse fleet PDA required' )
proof . picker = { membershipId : picker . membershipId , username : picker . username }; await context . checkpoint ? .()
stage ( 'EXTERNAL_RECEIPT' )
await verifyFleetExternalReceipt ( context , MODULE , trip , picker . token , { reconcilePalletized : resumePalletized , reconcileConfirmed : resumeReceiptConfirmed })
stage ( 'EXTERNAL_DISPATCH' )
await verifyFleetExternalDispatch ( context , MODULE , trip , { pickerToken : picker . token ,
actualMeasurements : await currentFleetMeasurements ( context , trip ), coverageCase : partial })
2026-10-03 06:26:33 +08:00
const external = trip . externalDispatch
stage ( 'FINANCE_CONSUMER' )
2026-10-20 10:35:52 +08:00
const externalCustomer = id ( external . customerId )
const sources = await nativeRequest ( context , 'GET' , `/api/platform/v1/last-leg/finance-fee-sources? ${ new URLSearchParams ({ customerId : externalCustomer , businessType : 'DELIVERY_ORDER' , businessNo : external . deliveryNo } )}` )
const target = sources . filter ( row => row . feeItemCode === 'DELIVERY_FEE' && row . direction === 'RECEIVABLE' && row . sourceStatus === 'ACTIVE'
&& row . inboundCode === trip . externalReceipt . incomingNo && id ( row . customerId ) === externalCustomer )
if ( target . length !== 1 || Number ( target [ 0 ]. originalAmount ) !== 30 || target [ 0 ]. originalCurrency !== 'CNY' ) throw Error ( 'Exact current-run external fee source missing' )
2026-10-03 06:26:33 +08:00
let matched = []
2026-10-20 10:35:52 +08:00
for ( let attempt = 0 ; attempt < 30 ; attempt ++ ) {
const pool = await nativeRequest ( context , 'GET' , `/api/platform/finance/v1/fee-pool?customerId= ${ encodeURIComponent ( externalCustomer ) } &direction=RECEIVABLE` )
if ( ! Array . isArray ( pool ) || pool . some ( row => id ( row . customerId ) !== externalCustomer || row . direction !== 'RECEIVABLE' )) throw Error ( 'Finance consumer scope mismatch' )
matched = pool . filter ( row => id ( row . sourceId ) === id ( target [ 0 ]. sourceFeeId ) && id ( row . sourceVersion ) === id ( target [ 0 ]. sourceVersion ))
2026-10-03 06:26:33 +08:00
if ( matched . length ) break
2026-10-20 10:35:52 +08:00
if ( attempt < 29 ) await wait ( 1000 )
2026-10-03 06:26:33 +08:00
}
2026-10-20 10:35:52 +08:00
if ( matched . length !== 1 || matched [ 0 ]. sourceType !== 'OVERSEAS_DELIVERY_FEE' || Number ( matched [ 0 ]. amount ) !== 30 || matched [ 0 ]. currency !== 'CNY' ) throw Error ( 'Finance did not persist exactly this external fee' )
proof . finance = { status : 'CONSUMER_PERSISTED' , customerId : externalCustomer , source : target [ 0 ], pool : matched , producerAck : 'NOT_VERIFIED' }
2026-10-03 06:26:33 +08:00
external . financeAcknowledgement = 'CONSUMER_PERSISTED_PRODUCER_ACK_NOT_VERIFIED'
stage ( 'WHOLE_TRIP_DEPARTURE' )
2026-10-20 10:35:52 +08:00
const departure = await departNativeFleet ( context , trip , picker . token , proof )
await completeDepartedFleetScenario ( context , trip , picker , departure . dispatchDetails , { costActors , onHandoverReady })
2026-10-03 20:06:02 +08:00
} finally { await disablePicker ( context , picker , 71 ) }
return proof
}
export async function completeDepartedFleetScenario ( context , trip , picker , dispatchDetails , { costActors , onHandoverReady ,
2026-10-03 21:00:07 +08:00
costsAlreadyVerified = false , reviewedVehicle ,
2026-10-03 20:06:02 +08:00
verifyCosts = verifyFleetTripCosts , handover = runFleetHandoverStage , completeDelivery = saveReceiptAndComplete ,
closeTrip = finishFleetTrip , cleanup = cleanupFleetHandoverResources } = {}) {
2026-10-20 10:35:52 +08:00
const proof = context . createdResources . fleetAcceptance , customerId = proof . customerId , warehouseId = proof . warehouseId
if ( proof . protocol !== NATIVE_PROTOCOL ||! nativeDeparture ( proof . departure , trip . dispatchNo ) || proof . dispatchNo !== trip . dispatchNo
|| dispatchDetails ? . length !== 1 || dispatchDetails [ 0 ]. detail ? . dispatchNo !== trip . dispatchNo ) throw Error ( 'Post-departure continuation requires one native main bundle and its actual DEPART command' )
const stage = value =>{ proof . stage = value ; proof . checkpoints . push ({ stage : value , at : new Date (). toISOString ()}); console . log ( 'FLEET_STAGE=' + value )}
if ( ! costsAlreadyVerified ){ stage ( 'TRIP_OPERATING_COSTS' ); await verifyCosts ( context , MODULE , trip ,{... costActors , shares : fleetCostShares ( trip , dispatchDetails )})}
else if ( trip . costAcceptance ? . status !== 'VERIFIED' ) throw Error ( 'Recovery cost proof must remain verified' )
stage ( 'IN_TRANSIT_HANDOVER' )
await handover ( context , trip , picker . token , dispatchDetails . map ( bundle => bundle . detail ),{ reviewedVehicle ,
onReady : onHandoverReady ? async fixture =>{ stage ( 'PDA_UI_CHECKPOINT' ); await onHandoverReady ({ fixture , picker })} : undefined })
stage ( 'RECEIPTS_AND_RESOURCE_RELEASE' ); proof . receipts = []
const current = await readNativeFleetBundle ( context , trip . dispatchNo ), refs = nativeFleetDeliveries ( current . detail )
if ( context . context . fleetScenario === 'return-redispatch' ){
stage ( 'RETURN_REDISPATCH' )
if ( refs . some ( ref => id ( ref . customerId ) !== id ( customerId ))) throw Error ( 'Explicit return-redispatch source selection requires the one actual source customer' )
const returned = await verifyFleetReturnRedispatch ( context , MODULE , trip ,{ customerId , warehouseId , pickerToken : picker . token ,
dispatchNos : [ trip . dispatchNo ], deliveryNos : refs . map ( ref => ref . deliveryNo )})
proof . receipts = returned . originalSignatures . map ( row =>({ customerId : id ( row . customerId || customerId ), deliveryNo : row . deliveryNo , signedAt : row . signedAt }))
context . context . saveFleetReturnRedispatchFixture ( returned )
} else {
for ( const [ index , ref ] of refs . entries ()){
const delivery = await nativeRequest ( context , 'GET' , `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( ref . deliveryNo ) } ?customerId= ${ encodeURIComponent ( id ( ref . customerId )) } ` )
if ( delivery . order ? . deliveryNo !== ref . deliveryNo || id ( delivery . order . customerId ) !== id ( ref . customerId ) || delivery . order . dispatchNo !== trip . dispatchNo ) throw Error ( 'POD source customer or dispatch drift' )
const completed = await completeDelivery ( context , delivery . order , id ( ref . customerId ), index + 71 )
if ( completed ? . order ? . status !== 'DELIVERED' ||! completed . order . signedAt ) throw Error ( '每个目的地真实保存签收资料后完成' )
proof . receipts . push ({ customerId : id ( ref . customerId ), deliveryNo : ref . deliveryNo , signedAt : completed . order . signedAt })
2026-10-03 06:26:33 +08:00
}
2026-10-20 10:35:52 +08:00
}
proof . closure = await closeTrip ( context , MODULE , trip )
if ( proof . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' ) throw Error ( '本次车次货物结案及车辆司机释放完成' )
stage ( 'HANDOVER_RESOURCE_CLEANUP' ); await cleanup ( context , MODULE , trip )
proof . status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' ; stage ( 'FINISHED' ); return proof
2026-10-03 06:26:33 +08:00
}
2026-10-03 20:06:02 +08:00
export async function continueReviewedDeparture ( context , { verify = verifyDepartedRecovery , prepareActors = prepareFleetCostActors ,
2026-10-03 21:00:07 +08:00
costsAlreadyVerified = false , createdVehicle ,
2026-10-03 20:06:02 +08:00
resolveAccess = resolvePickerAccess , makePicker = createPicker , closePicker = disablePicker ,
complete = completeDepartedFleetScenario } = {}) {
2026-10-03 21:00:07 +08:00
const verified = await verify ( context , { costsAlreadyVerified , createdVehicle })
2026-10-03 20:06:02 +08:00
const proof = context . createdResources . fleetAcceptance , trip = verified . trip
2026-10-03 20:24:56 +08:00
if ( ! costsAlreadyVerified ) {
context . createdResources . previousFleetCostActors = context . createdResources . fleetCostActors
trip . previousReadOnlyCostAttempts = [ trip . costAcceptance ]
delete context . createdResources . fleetCostActors
delete trip . costAcceptance
} else {
trip . previousReadOnlyHandoverAttempts = [ trip . handover ]
delete trip . handover
}
2026-10-03 20:06:02 +08:00
proof . previousPicker = proof . picker
delete proof . picker
proof . pickerMarker = ` ${ context . marker } -RECOVERY-PDA- ${ randomUUID (). slice ( 0 , 8 ) } `
await context . checkpoint ()
let actors , picker
try {
2026-10-03 20:24:56 +08:00
if ( ! costsAlreadyVerified ) {
actors = await prepareActors ( context )
if ( actors ? . marker !== context . marker || ! actors . listOnlyToken || ! actors . deniedToken
2026-10-03 20:06:02 +08:00
|| actors . listOnlyToken === actors . deniedToken || [ actors . listOnlyToken , actors . deniedToken ]. includes ( context . token ))
2026-10-03 20:24:56 +08:00
throw Error ( 'Recovery requires fresh independent cost sessions' )
}
2026-10-03 20:06:02 +08:00
const pickerContext = Object . create ( context ); pickerContext . marker = proof . pickerMarker
const access = await resolveAccess ( context )
picker = access ? await makePicker ( pickerContext , 71 , proof . warehouseId , access ) : null
if ( ! picker ? . token || ! picker . membershipId ) throw Error ( 'Recovery requires a new bound PDA session' )
proof . picker = { membershipId : picker . membershipId , username : picker . username }
await context . checkpoint ()
2026-10-03 21:00:07 +08:00
return await complete ( context , trip , picker , verified . dispatchDetails , { costActors : actors , costsAlreadyVerified , reviewedVehicle : verified . reviewedVehicle })
2026-10-03 20:06:02 +08:00
} finally {
try { await closePicker ( context , picker , 71 ) } finally { await actors ? . close () }
}
}
2026-10-03 14:52:04 +08:00
export async function runFleetAcceptance ( config , { env = process . env , readers , createContext , scenario = runFleetScenario , prepareCostActors = prepareFleetCostActors , root = ROOT } = {}) {
2026-10-03 06:26:33 +08:00
if ( ! config . execute || config . environment !== 'TEST' || env . CHONGCHONG_ENV ? . toLowerCase () !== 'test'
|| basename ( root ) !== 'baiyue-firstleg' || basename ( dirname ( root )) !== 'chongchong-global' ) throw Error ( 'Explicit TEST workspace and configuration required' )
2026-10-03 14:52:04 +08:00
if ( scenario === runFleetScenario && config . resumeStage !== 'TERMINAL_READONLY' && typeof prepareCostActors !== 'function' )
throw Error ( 'Current-run cost actor preparation and lifecycle must be provided before any environment requests' )
2026-10-03 06:26:33 +08:00
const base = new URL ( env . CHONGCHONG_ADMIN_API_BASE_URL )
if ( ! [ 'http:' , 'https:' ]. includes ( base . protocol ) || base . username || base . password || base . search || base . hash
|| /(?:^|[.-])(?:dev|prod|production)(?:[.-]|$)/i . test ( base . hostname )) throw Error ( 'Invalid TEST gateway' )
for ( const name of [ 'CHONGCHONG_ADMIN_USERNAME' , 'CHONGCHONG_ADMIN_PASSWORD' , 'ADMIN_PDA_USERNAME' , 'ADMIN_PDA_PASSWORD' ]) if ( ! env [ name ]) throw Error ( `Missing ${ name } ` )
2026-10-03 14:52:04 +08:00
if ( config . outputDir && ! /^reports\/[A-Za-z0-9][A-Za-z0-9_-]{0,95}$/ . test ( config . outputDir )) throw Error ( 'Report scope invalid' )
const output = resolve ( root , config . outputDir || `reports/fleet- ${ config . runId } ` ), rel = relative ( resolve ( root , 'reports' ), output )
2026-10-03 06:26:33 +08:00
if ( ! rel || rel . startsWith ( '..' ) || isAbsolute ( rel )) throw Error ( 'Report scope invalid' )
mkdirSync ( resolve ( root , 'reports' ), { recursive : true })
const lockPath = resolve ( root , 'reports/.fleet-acceptance.lock' ), lock = openSync ( lockPath , 'wx' )
2026-10-03 19:19:16 +08:00
let context , initialized = false , report , restoreFetch
2026-10-03 06:26:33 +08:00
const secrets = Object . entries ( env ). filter (([ name ]) => / PASSWORD | TOKEN | USERNAME | JENKINS_USER / . test ( name )). map (([, value ]) => value )
const save = ( name , value ) => writeFileSync ( join ( output , name ), JSON . stringify ( redactEvidence ( value , [... secrets ,
context ? . token , context ? . pdaToken , context ? . refreshToken , context ? . context ? . merchantQualificationFixture ? . password ]), null , 2 ) + '\n' )
try {
writeFileSync ( lock , JSON . stringify ({ pid : process . pid , runId : config . runId , startedAt : new Date (). toISOString () }))
mkdirSync ( output , { recursive : false }); initialized = true ; mkdirSync ( join ( output , 'evidence' ))
save ( 'manifest.json' , fleetExecutionPlan ( config ))
let resumed
if ( config . resumeReport ) {
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 )
2026-10-03 22:13:15 +08:00
if (( config . resumeStage ? . startsWith ( 'DEPARTED_' ) || config . resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN' ) && createHash ( 'sha256' ). update ( bytes ). digest ( 'hex' ) !== config . resumeSha256 )
2026-10-03 20:06:02 +08:00
throw Error ( 'Reviewed departure report SHA256 mismatch; no environment request sent' )
2026-10-03 06:26:33 +08:00
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 })
} else {
readers ||= createTestEvidenceReaders ( env )
await readers . preflight ()
}
const options = validateExecutionOptions ({ ... parseArguments ([ '--modules' , 'phase4-overseas-dispatch' , '--quiet' , 'true' ]),
gateway : base . origin , username : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD ,
pdaUsername : env . ADMIN_PDA_USERNAME , pdaPassword : env . ADMIN_PDA_PASSWORD , merchantUsername : '' , merchantPassword : '' ,
markerNamespace : createHash ( 'sha256' ). update ( config . runId ). digest ( 'hex' ). slice ( 0 , 12 ) })
context = createContext ? createContext ( options ) : new AcceptanceRun ( options )
2026-10-03 19:07:58 +08:00
let writeCheckpoint = 0
2026-10-03 19:19:16 +08:00
restoreFetch = guardFleetWrites ( context , entry => {
2026-10-03 19:07:58 +08:00
const payload = JSON . stringify ( redactFleetWrite ({ marker : context . marker , runId : config . runId , request : entry },
[... secrets , context . token , context . pdaToken , context . refreshToken ]), null , 2 ) + '\n'
const descriptor = openSync ( join ( output , 'evidence' , `write- ${ String ( ++ writeCheckpoint ). padStart ( 6 , '0' ) } .json` ), 'wx' )
try { writeFileSync ( descriptor , payload ); fsyncSync ( descriptor ) }
finally { closeSync ( descriptor ) }
2026-10-03 19:19:16 +08:00
}, { guardFetch : true })
2026-10-03 14:52:04 +08:00
context . context . fleetRunId = config . runId
2026-10-03 16:11:52 +08:00
context . context . fleetScenario = config . scenario || 'handover'
2026-10-03 21:00:07 +08:00
context . context . readReviewedVehicleOccupation = vehicleId => readReviewedVehicleOccupation ( env , vehicleId )
2026-10-03 22:13:15 +08:00
context . context . readRecoveryTripResources = tripNo => readRecoveryTripResources ( env , tripNo )
context . context . onBeforeRedispatchSignature = proof => waitForRedispatchSignatureAudit ( context , proof ,
{ root , output , secrets : [... secrets , context . token , context . pdaToken , context . refreshToken ] })
2026-10-03 14:52:04 +08:00
context . context . saveFleetHandoverFixture = fixture => save ( 'handover-fixture.json' , fixture )
2026-10-03 16:11:52 +08:00
context . context . saveFleetReturnRedispatchFixture = fixture => save ( 'return-redispatch-fixture.json' , fixture )
2026-10-03 14:52:04 +08:00
let handoverCheckpoint = 0
context . context . checkpointFleetHandover = proof => {
const payload = JSON . stringify ( redactEvidence ({ marker : context . marker , runId : config . runId ,
proof , createdResources : context . createdResources }, [... secrets , context . token , context . pdaToken , context . refreshToken ]), null , 2 ) + '\n'
// Never truncate the prior checkpoint: interruption or disk failure leaves the earlier request recoverable.
const descriptor = openSync ( join ( output , 'evidence' , `handover- ${ String ( ++ handoverCheckpoint ). padStart ( 6 , '0' ) } .json` ), 'wx' )
try { writeFileSync ( descriptor , payload ); fsyncSync ( descriptor ) }
finally { closeSync ( descriptor ) }
}
context . checkpoint = () => context . context . checkpointFleetHandover ( context . createdResources . fleetAcceptance )
const fleetScenario = async ( ctx , options = {}) => {
2026-10-03 22:13:15 +08:00
if ( options . returnSourceSelection ) return continueReviewedReturnSources ( ctx )
2026-10-03 21:00:07 +08:00
if ( options . resumeDeparted ) return continueReviewedDeparture ( ctx , { prepareActors : prepareCostActors ,
costsAlreadyVerified : options . costsAlreadyVerified , createdVehicle : options . createdVehicle })
2026-10-03 14:52:04 +08:00
const costActors = options . terminalReadOnly || scenario !== runFleetScenario ? undefined : await prepareCostActors ? .( ctx )
try { return await scenario ( ctx , { ... options , costActors }) }
finally { await costActors ? . close () }
}
2026-10-03 06:26:33 +08:00
let evidenceNumber = 0
context . context . phase4ReceivingEvidenceReader = async scope => {
const evidence = await readers . readReceiving ( scope )
save ( `evidence/receiving- ${ ++ evidenceNumber } .json` , { scope , evidence }); return evidence
}
const executeModule = context . executeModule . bind ( context )
context . executeModule = async ( name , handler ) => {
await executeModule ( name , handler )
save ( 'progress.json' , { stage : name , steps : context . steps , modules : context . modules , createdResources : context . createdResources })
if ( context . steps . some ( step => step . passed === false ) || context . modules . at ( - 1 ) ? . success === false ) throw Error ( `Producer stopped: ${ name } ` )
if ( name === 'phase4-overseas-dispatch' ) {
2026-10-03 14:52:04 +08:00
await executeModule ( MODULE , () => fleetScenario ( context ))
2026-10-03 06:26:33 +08:00
if ( context . modules . at ( - 1 ) ? . success === false ) throw Error ( 'Fleet case stopped at original fixture' )
}
}
if ( resumed ) {
context . createdResources = resumed . resources ; context . marker = resumed . marker
const login = await context . request ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password }, '' )
if ( ! success ( login ) || ! login . body ? . data ? . accessToken ) throw Error ( 'Resume administrator login failed' )
context . token = login . body . data . accessToken ; context . refreshToken = login . body . data . refreshToken || ''
2026-10-03 14:52:04 +08:00
await context . executeModule ( MODULE , () => fleetScenario ( context , { resumeFull : ! resumed . receiptReady ,
2026-10-03 20:06:02 +08:00
resumeDeparted : resumed . departed ,
2026-10-03 22:13:15 +08:00
returnSourceSelection : resumed . returnSourceSelection ,
2026-10-03 20:24:56 +08:00
costsAlreadyVerified : resumed . costsAlreadyVerified ,
2026-10-03 21:00:07 +08:00
createdVehicle : resumed . createdVehicle ,
2026-10-03 06:26:33 +08:00
resumePersisted : resumed . persisted , resumeReceiptReady : resumed . receiptReady , resumePalletized : resumed . palletized , resumeReceiptConfirmed : resumed . receiptConfirmed , terminalReadOnly : resumed . terminal }))
const summary = buildAcceptanceSummary ( context . steps , context . modules , context . coverageGaps )
report = { success : summary . failed === 0 , marker : context . marker , summary , steps : context . steps , modules : context . modules ,
createdResources : context . createdResources , resumedFrom : config . resumeReport }
} else report = await context . run ()
report . fleetExecution = fleetExecutionPlan ( config )
// Keep the unresolved producer ACK explicit; a green consumer row cannot close the complete fleet goal.
report . fleetGoalComplete = false
} catch ( error ) {
report = { success : false , fatal : error instanceof Error ? error . message : 'FLEET_ACCEPTANCE_FAILED' ,
summary : context ? buildAcceptanceSummary ( context . steps , context . modules , context . coverageGaps ) : { failed : 1 },
steps : context ? . steps || [], modules : context ? . modules || [], createdResources : context ? . createdResources || {} }
} finally {
2026-10-03 19:19:16 +08:00
restoreFetch ? .()
2026-10-03 06:26:33 +08:00
if ( initialized ) save ( 'report.json' , report || { success : false , fatal : 'RUN_INTERRUPTED' })
closeSync ( lock ); unlinkSync ( lockPath )
}
return { success : report ? . success === true , report : join ( output , 'report.json' ), summary : report ? . summary ,
fleet : report ? . createdResources ? . fleetAcceptance ? . status , fatal : report ? . fatal }
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( resolve ( process . argv [ 1 ])). href ) {
try { const config = parseFleetArguments ( process . argv . slice ( 2 )); const result = config . execute ? await runFleetAcceptance ( config ) : fleetExecutionPlan ( config )
console . log ( JSON . stringify ( result , null , 2 )); if ( config . execute && ! result . success ) process . exitCode = 1
} catch ( error ) { console . error ( error instanceof Error ? error . message : 'Fleet acceptance failed' ); process . exitCode = 1 }
}