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-03 06:26:33 +08:00
import { prepareFleetTrip , confirmFleetLoading , finishFleetTrip , controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
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-03 06:26:33 +08:00
import { resolvePickerAccess , createPicker , disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs'
import { pickAll , saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
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-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-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' ) })
}
try { await checkpoint ( entry ) } catch ( error ) { stopped = error ; throw error }
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-03 19:07:58 +08:00
if ( ! definite ) stopped = Error ( `Unknown write outcome: ${ method } ${ path } ; preserve original request, do not resend` )
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-03 19:07:58 +08:00
await checkpoint ( entry )
}
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'
? [ 'Verify exact source-selection-failure report SHA256 and three definitive delivery starts' , 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions' ,
'Recheck repaired A/B return candidates and complete source quantities before any write' , 'Create fresh bound PDA session' ,
'Original sample actual return and redispatch; no repeated original departure/handover/cost/start-delivery' ,
'Wait for coordinator pre-signature A0/B1 event evidence gate' , 'New signature and source ACK evidence; close new and original resources' ]
: config . resumeStage ? . startsWith ( 'DEPARTED_' )
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' ]
: [ 'FULL original coverage' , 'formal cancel/unbind/re-arrange' , 'PARTIAL original coverage' , 'reservation lifecycle' ,
2026-10-03 06:26:33 +08:00
'external actual receipt' , 'external dispatch and agreed fee' , 'Finance consumer persistence' , 'each dispatch PDA picking' ,
2026-10-03 14:52:04 +08:00
'atomic whole-trip departure' , 'actual in-transit handover and PDA readback' , 'same-fixture optional H5 checkpoint' ,
'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-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'
|| proof . departure ? . status !== 'IN_TRANSIT' || proof . departure . tripNo !== trip . tripNo
|| trip . closure || proof . closure || proof . receipts ? . length || attempt . status !== 'FAILED'
|| attempt . failure !== 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B'
|| attempt . exceptionNo || attempt . sources || attempt . newTripNo || attempt . originalSignatures
|| originals ? . length !== 3 || new Set ( originals . map ( row => row . deliveryNo )). size !== 3 || writes ? . length !== 3
|| originals . some ( row => id ( row . customerId ) !== id ( proof . customerId ) || id ( row . overseasWarehouseId ) !== id ( proof . warehouseId )
|| ! proof . dispatchNos . includes ( row . dispatchNo ) || row . status !== 'WAIT_DELIVERY' || row . signedAt || Number ( row . signedPackages || 0 ))
|| attempt . requests . some ( row => row . status !== 'RESPONDED' || row . http !== 200 || id ( row . code ) !== '200' || ! row . requestId || ! row . traceId )
|| writes . some ( row => {
const original = originals . find ( item => item . deliveryNo === row . result ? . deliveryNo )
return ! original || row . method !== 'PUT' || row . path !== `/api/platform/v1/last-leg/delivery-orders/ ${ original . deliveryNo } /status?customerId= ${ proof . customerId } `
|| row . body ? . status !== 'DELIVERING' || id ( row . body . expectedVersion ) !== id ( original . version )
|| row . result . deliveryStatus !== 'DELIVERING' || row . result . replayed !== false
}) || new Set ( writes . map ( row => row . result ? . deliveryNo )). size !== 3
|| resources . fleetCostActors ? . status !== 'DISABLED' || failures ? . length !== 1 || failures [ 0 ]. name !== '模块执行'
|| failures [ 0 ]. evidence !== attempt . failure )
throw Error ( 'Return source recovery requires the exact known candidate failure and three definitive delivery starts, with no return/signature writes' )
return { resources , marker : proof . marker , returnSourceSelection : true }
}
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-03 20:06:02 +08:00
|| proof . marker !== costs ? . marker || proof . departure ? . status !== 'IN_TRANSIT' || proof . departure . tripNo !== trip ? . tripNo
|| ! 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'
|| trip . externalDispatch ? . status !== 'VERIFIED' || trip . reassignment ? . status !== 'VERIFIED'
|| ! 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'
|| full ? . proof ? . status !== 'VERIFIED' || partial ? . proof ? . status !== 'VERIFIED' || partial . dispatchNo !== trip ? . dispatchNo
|| 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'
|| full ? . proof ? . status !== 'VERIFIED' || partial ? . proof ? . status !== 'VERIFIED' || partial . dispatchNo !== trip ? . dispatchNo
|| 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'
|| full . marker !== proof . marker || full . dispatchNo !== trip ? . dispatchNo || full . proof ? . status !== 'CHECKING'
|| ! full . proof . requests ? . length
|| resources . fleetFeeCoverageCases . length !== 1 || ! proof . originalDispatch || proof . originalDispatch . dispatchNo !== full . dispatchNo )
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'
if ( persisted ? ( ! full . proof . outboundNo || ! full . proof . deliveryNos ? . length || ! full . proof . confirmationRequest ? . idempotencyKey
|| ! full . proof . requests . some ( row => row . label === '正式保存本次原费用服务覆盖并转派送' && row . method === 'POST' && row . http === 200 && String ( row . code ) === '200' ))
: ( 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 partial = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'PARTIAL' )
const evidence = proof . terminalReadback = { status : 'CHECKING' , requests : [], deliveries : [], dispatches : [] }
function check ( label , passed , response ) {
context . assert ( MODULE , label , response , Boolean ( passed ), '' , OPTIONS )
if ( ! passed ) throw Error ( label )
}
async function get ( path ) {
const result = await context . request ( 'GET' , path )
evidence . requests . push ({ path , http : result . http , code : result . body ? . code , requestId : result . requestId , traceId : result . traceId })
check ( '终态只读回查成功' , success ( result ) && result . requestId && result . traceId , result )
return result . body . data
}
check ( '结案助手已完成货物与资源核对' , 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 )
evidence . trip = current . trip
const expectedDispatches = [ partial . dispatchNo , trip . externalDispatch . dispatchNo ]. sort ()
check ( '终态车次仍关联全部原执行单' , JSON . stringify ( current . dispatches . map ( row => row . dispatchNo ). sort ()) === JSON . stringify ( expectedDispatches ))
for ( const number of expectedDispatches ) {
const dispatch = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( number ) } ` )
check ( '原执行单已发车且拣货实发数量守恒' , dispatch . dispatchNo === number && dispatch . status === 'DEPARTED'
&& Number ( dispatch . packageCount ) === Number ( dispatch . shippedPackageCount )
&& Number ( dispatch . pickedPackageCount ) === Number ( dispatch . shippedPackageCount ))
evidence . dispatches . push ( dispatch )
}
check ( '整车最终箱数与原一次发车事实一致' , evidence . dispatches . reduce (( n , row ) => n + Number ( row . shippedPackageCount ), 0 ) === Number ( proof . departure . shippedPackages ))
const expectedDeliveries = [... partial . proof . deliveryNos , trip . externalDispatch . deliveryNo ]. sort ()
check ( '签收证明覆盖全部目的地派送' , JSON . stringify ( proof . receipts . map ( row => row . deliveryNo ). sort ()) === JSON . stringify ( expectedDeliveries ))
for ( const number of expectedDeliveries ) {
const delivery = await get ( `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( number ) } ?customerId= ${ encodeURIComponent ( proof . customerId ) } ` )
check ( '每张原派送保留完成及签收时间' , delivery . order ? . deliveryNo === number && delivery . order . status === 'DELIVERED'
&& delivery . order . signedAt === proof . receipts . find ( row => row . deliveryNo === number ). signedAt )
evidence . deliveries . push ( delivery . order )
}
for ( const probe of trip . closure . probes ) {
const detail = await get ( `/api/platform/v1/last-leg/fleet/trips/ ${ encodeURIComponent ( probe . tripNo ) } ` )
check ( '资源占用测试计划均已正式取消' , detail . trip ? . status === 'CANCELLED' )
}
const pool = await get ( `/api/platform/finance/v1/fee-pool?customerId= ${ encodeURIComponent ( proof . customerId ) } &direction=RECEIVABLE` )
const fees = pool . filter ( row => id ( row . sourceId ) === id ( proof . finance . source . sourceFeeId ) && Number ( row . sourceVersion ) === Number ( proof . finance . source . sourceVersion ))
check ( '原外接应收仍唯一且金额正确' , fees . length === 1 && fees [ 0 ]. amount === 30 && fees [ 0 ]. currency === 'CNY'
&& id ( fees [ 0 ]. customerId ) === id ( proof . 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 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-03 20:06:02 +08:00
&& current . trip . departedAt === proof . departure . 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-03 20:06:02 +08:00
check ( same ( current . dispatches . map ( row => row . dispatchNo ). sort (), [... proof . dispatchNos ]. sort ())
&& current . dispatches . every ( row => id ( row . customerId ) === id ( proof . customerId )), 'trip dispatch binding changed' )
for ( const number of proof . dispatchNos ) {
const detail = await get ( `/api/platform/v1/last-leg/dispatches/ ${ encodeURIComponent ( number ) } ` )
const cargo = load . cargo . filter ( row => row . dispatchNo === number )
const facts = rows => rows . map ( row => [ id ( row . id ?? row . dispatchItemId ), id ( row . warehousePalletId ), Number ( row . packageCount ?? row . packages )])
. sort (( a , b ) => a [ 0 ]. localeCompare ( b [ 0 ]))
check ( detail . dispatchNo === number && detail . status === 'DEPARTED' && id ( detail . overseasWarehouseId ) === id ( proof . warehouseId )
&& Number ( detail . packageCount ) === Number ( detail . shippedPackageCount ) && Number ( detail . pickedPackageCount ) === Number ( detail . shippedPackageCount )
&& same ( facts ( detail . items ), facts ( cargo )), 'dispatch shipped cargo changed' )
evidence . dispatchDetails . push ( detail )
}
const partial = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'PARTIAL' )
const deliveryNos = [... partial . proof . deliveryNos , trip . externalDispatch . deliveryNo ]
check ( new Set ( deliveryNos ). size === deliveryNos . length , 'duplicate delivery identities' )
for ( const number of deliveryNos ) {
const detail = await get ( `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( number ) } ?customerId= ${ encodeURIComponent ( proof . customerId ) } ` )
const order = detail . order
2026-10-03 22:13:15 +08:00
const started = returnSourceReady ? trip . returnRedispatch . requests . find ( row => row . method === 'PUT' && row . result ? . deliveryNo === number ) : null
2026-10-03 20:06:02 +08:00
check ( order ? . deliveryNo === number && id ( order . customerId ) === id ( proof . customerId ) && proof . dispatchNos . includes ( order . dispatchNo )
2026-10-03 22:13:15 +08:00
&& order . status === ( returnSourceReady ? 'DELIVERING' : 'WAIT_DELIVERY' )
&& id ( order . version ) === ( returnSourceReady ? String ( Number ( started ? . body . expectedVersion ) + 1 ) : '1' )
&& ( ! returnSourceReady || order . vehiclePlate === expectedTrip . vehiclePlate && order . driverName === expectedTrip . driverName && order . driverPhone === expectedTrip . driverPhone )
&& ! order . signedAt && ! Number ( order . signedPackages || 0 )
2026-10-03 20:06:02 +08:00
&& ! order . completedAt && order . documentSummary ? . categories ? . every ( row => Number ( row . fileCount ) === 0 ), 'delivery has started, changed or been signed' )
evidence . deliveries . push ( order )
}
check ( evidence . deliveries . reduce (( n , row ) => n + Number ( row . packageCount ), 0 ) === Number ( proof . departure . shippedPackages ), '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 ,
pickerToken : picker . token , dispatchNos : run . dispatchNos , deliveryNos : [... partial . proof . deliveryNos , trip . externalDispatch . deliveryNo ], sourceSelectionRecovery : true })
run . receipts = result . originalSignatures . map ( row => ({ deliveryNo : row . deliveryNo , signedAt : row . signedAt }))
context . context . saveFleetReturnRedispatchFixture ( result )
run . closure = await closeOriginal ( context , MODULE , trip )
if ( run . closure ? . status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' ) throw Error ( 'Recovered original trip did not close' )
await cleanup ( context , MODULE , trip )
run . stage = 'FINISHED' ; run . status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' ; await context . checkpoint ()
return run
} finally { await closePicker ( context , picker , 71 ) }
}
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
if ( state ? . status !== 'VERIFIED' || state . marker !== context . marker || run ? . departure ? . tripNo !== trip . tripNo
|| run . departure . status !== 'IN_TRANSIT' || ! state . originalDeparture ? . snapshot ? . confirmedLoad || ! state . browser
|| 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
if ( external ? . status !== 'VERIFIED' || ! external . bookingNo || ! external . warehousePalletId || ! externalDispatch ? . dispatchNo )
throw Error ( 'Cost expectations require this run\'s verified external receipt' )
const shares = dispatchDetails . map ( row => {
const pallets = [... new Set (( row . items || []). map ( item => id ( item . warehousePalletId )))]
if ( ! row . dispatchNo || ! pallets . length || pallets . some ( value => ! value )) throw Error ( 'Cost expectations require actual dispatch pallets' )
const isExternal = row . dispatchNo === externalDispatch . dispatchNo
if ( isExternal && ( pallets . length !== 1 || pallets [ 0 ] !== id ( external . warehousePalletId ))) throw Error ( 'External cost source does not match actual receipt' )
// The fixture's explicit measurement rule is one space per physical pallet; never derive this from cost totals.
return { businessNo : isExternal ? external . bookingNo : row . dispatchNo , sourceType : isExternal ? 'EXTERNAL' : 'OWN' ,
dispatchNo : row . dispatchNo , warehousePalletIds : pallets , palletSpaces : pallets . length }
})
const pallets = shares . flatMap ( row => row . warehousePalletIds )
if ( shares . filter ( row => row . sourceType === 'EXTERNAL' ). length !== 1 || ! shares . some ( row => row . sourceType === 'OWN' )
|| new Set ( pallets ). size !== pallets . length ) throw Error ( 'Cost fixture requires distinct own and external physical pallets' )
return shares
}
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-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-03 06:26:33 +08:00
const proof = resumeFull || resumeReceiptReady ? context . createdResources . fleetAcceptance
: context . createdResources . fleetAcceptance = { marker : context . marker , status : 'CHECKING' , stage : 'SOURCES' , checkpoints : [] }
function stage ( value ) { proof . stage = value ; proof . checkpoints . push ({ stage : value , at : new Date (). toISOString () }); console . log ( `FLEET_STAGE= ${ value } ` ) }
function check ( label , value , response ) { context . assert ( MODULE , label , response , Boolean ( value ), '' , OPTIONS ); if ( ! value ) throw Error ( label ) }
async function request ( method , path , body , label , token ) {
const response = await context . request ( method , path , body , token )
check ( label , success ( response ), response ); return response . body . data
}
const source = context . createdResources . phase4OverseasDispatch , receiving = context . createdResources . phase4OverseasPda
check ( '专项只承接本轮实际已收货及预排资源' , source ? . marker === context . marker && source . dispatchNo
&& receiving ? . marker === context . marker && receiving . finalStatus === 'COMPLETED' && receiving . customerId && receiving . overseasWarehouseId )
const customerId = id ( receiving . customerId ), warehouseId = id ( receiving . overseasWarehouseId )
proof . customerId = customerId ; proof . warehouseId = warehouseId
const dispatchBase = '/api/platform/v1/last-leg/dispatches'
let trip , partial
if ( resumeReceiptReady ) {
trip = context . createdResources . fleetTrips . find ( row => row . tripNo === proof . tripNo )
partial = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'PARTIAL' )
const current = await request ( 'GET' , `/api/platform/v1/last-leg/fleet/trips/ ${ encodeURIComponent ( trip . tripNo ) } ` , undefined , '恢复实收前核对原车次分配与客户执行单' )
const dispatch = await request ( 'GET' , ` ${ dispatchBase } / ${ encodeURIComponent ( partial . dispatchNo ) } ` , undefined , '恢复实收前核对原排车尚未拣货或发车' )
check ( '仍是原已核对车辆司机与未执行客户排车' , current . trip ? . status === 'PLANNED'
&& id ( current . trip . vehicleId ) === id ( trip . vehicleId ) && id ( current . trip . driverId ) === id ( trip . driverId )
&& current . dispatches ? . length === 1 && current . dispatches [ 0 ]. dispatchNo === partial . dispatchNo
&& id ( current . dispatches [ 0 ]. customerId ) === customerId && dispatch . dispatchNo === partial . dispatchNo
&& Number ( dispatch . pickedPackageCount ) === 0 && Number ( dispatch . shippedPackageCount ) === 0 )
} else {
const original = await request ( 'GET' , ` ${ dispatchBase } / ${ encodeURIComponent ( source . dispatchNo ) } ` , undefined , '读取本轮原始预排实物' )
if ( resumeFull ) check ( '原样本恢复前排车货物未变且未开始作业' , JSON . stringify ( original . items ) === JSON . stringify ( proof . originalDispatch . items )
&& Number ( original . pickedPackageCount ) === 0 && Number ( original . shippedPackageCount ) === 0 )
proof . originalDispatch = original
trip = resumeFull ? context . createdResources . fleetTrips . find ( row => row . tripNo === proof . tripNo )
: await prepareFleetTrip ( context , MODULE , source . dispatchNo , warehouseId )
proof . tripNo = trip . tripNo
const tripPath = `/api/platform/v1/last-leg/fleet/trips/ ${ encodeURIComponent ( trip . tripNo ) } `
function transferBody ( vehicle ) { return { idempotencyKey : key (), plannedDeliveryAt : new Date ( Date . now () + 21600000 ). toISOString (). slice ( 0 , 19 ),
deliveryMethod : 'TRUCK_PALLET' , pickupMethod : 'WAREHOUSE_PICKUP' , loadingType : 'PALLET' , vehicleType : 'TRUCK' , vehicleSize : '30托' ,
vehiclePlate : vehicle . vehiclePlate , driverName : vehicle . driverName , driverPhone : vehicle . driverPhone , appointmentAccount : 'SELF' ,
remark : ` ${ context . marker } 原费用覆盖专用验证` , fees : [{ feeType : 'DELIVERY_FEE' , feeName : '未覆盖货物派送费' , direction : 'RECEIVABLE' , currency : 'CNY' , amount : 10 }] } }
let detail = await request ( 'GET' , tripPath , undefined , '读取当前派车事实供转派送使用' )
check ( '当前车次资源及原客户排车关联仍一致' , id ( detail . trip . vehicleId ) === id ( trip . vehicleId )
&& id ( detail . trip . driverId ) === id ( trip . driverId ) && detail . dispatches . some ( row => row . dispatchNo === source . dispatchNo && id ( row . customerId ) === customerId ))
if ( ! resumeFull ) context . createdResources . fleetFeeCoverageCases = []
const full = resumeFull ? context . createdResources . fleetFeeCoverageCases [ 0 ]
: { marker : context . marker , mode : 'FULL' , dispatchNo : source . dispatchNo , customerId , transferBody : transferBody ( detail . trip ) }
if ( resumeFull && ! resumePersisted ) {
full . previousReadOnlyAttempts ||= []; full . previousReadOnlyAttempts . push ( full . proof ); delete full . proof
} else if ( ! resumeFull ) context . createdResources . fleetFeeCoverageCases . push ( full )
stage ( 'FULL_COVERAGE' ); await verifyFleetFeeCoverage ( context , MODULE , full , { reconcilePersisted : resumePersisted })
stage ( 'CANCEL_FULL_AND_REARRANGE' )
detail = await request ( 'GET' , tripPath , undefined , '读取取消未执行全覆盖用例前车次版本' )
await request ( 'POST' , ` ${ tripPath } /dispatches/unbind` , { recordVersion : detail . trip . recordVersion , dispatchNo : full . dispatchNo ,
reason : '全覆盖用例验证完毕,正式取消未执行发运并以新排车验证部分覆盖' }, '先解除未执行全覆盖排车关联' )
await request ( 'POST' , `/api/platform/v1/last-leg/outbound-orders/ ${ encodeURIComponent ( full . proof . outboundNo ) } /cancel` ,
{ reason : '本轮全覆盖验证完毕,正式取消未执行出库后重新安排' }, '取消未执行全覆盖出库' )
await request ( 'DELETE' , ` ${ dispatchBase } / ${ encodeURIComponent ( full . dispatchNo ) } ` , undefined , '正式取消原排车释放其托盘锁' )
const draftToken = key (), planPalletIds = original . items . map ( row => id ( row . planPalletId ))
check ( '原排车保留可追溯计划托盘' , planPalletIds . length > 0 && planPalletIds . every ( value => /^ [ 1 - 9 ] \ d * $ / . test ( value )))
proof . rearrange = { originalDispatchNo : full . dispatchNo , draftToken , planPalletIds }
const container = await request ( 'POST' , ` ${ dispatchBase } /drafts/containers` , { draftToken , capacity : 30 }, '建立同批当前库存的新排车草稿' )
for ( const planPalletId of planPalletIds ) await request ( 'POST' , ` ${ dispatchBase } /draft-locks/lock` , { planPalletId , draftToken , draftContainerId : id ( container . id ) }, '锁定已正式释放的本轮托盘' )
const arranged = await request ( 'POST' , ` ${ dispatchBase } /pre-arrange` , { planPalletIds , draftToken }, '以新排车重新安排本轮同批货物' )
check ( '取消重排产生新排车且货量不变' , arranged . dispatchNo && arranged . dispatchNo !== full . dispatchNo
&& Number ( arranged . packageCount ) === Number ( original . packageCount ))
proof . rearrange . dispatchNo = arranged . dispatchNo
detail = await request ( 'GET' , tripPath , undefined , '读取原车次当前可关联版本' )
detail = await request ( 'POST' , ` ${ tripPath } /dispatches/bind` , { recordVersion : detail . trip . recordVersion , dispatchNo : arranged . dispatchNo ,
reason : '本轮取消后新排车重新关联原计划车次' }, '将新排车正式关联原车次' )
trip . replacedDispatchNo = trip . dispatchNo ; trip . dispatchNo = arranged . dispatchNo
partial = { marker : context . marker , mode : 'PARTIAL' , dispatchNo : arranged . dispatchNo , customerId , transferBody : transferBody ( detail . trip ) }
context . createdResources . fleetFeeCoverageCases . push ( partial )
stage ( 'PARTIAL_COVERAGE' ); await verifyFleetFeeCoverage ( context , MODULE , partial )
stage ( 'RESERVATIONS' ); await verifyFleetBookingReservation ( context , MODULE , trip , controlledPalletMeasurements )
}
const access = await resolvePickerAccess ( context )
let picker = null
try {
if ( resumePalletized || resumeReceiptConfirmed ) {
const suffix = proof . pickerMarker . replace ( /[^A-Za-z0-9]/g , '' ). slice ( - 16 ). toUpperCase ()
const username = `pda_overseas_ ${ suffix . toLowerCase () } _71`
check ( '仅复用本轮创建的临时PDA身份' , proof . picker . username === username && access ? . role ? . id )
const users = await request ( 'GET' , `/api/platform/v1/iam/users? ${ new URLSearchParams ({ page : '1' , size : '200' , keyword : username } )}` , undefined , '核对原临时PDA账号绑定及角色' )
const user = users ? . records ? . find ( row => id ( row . membershipId ) === id ( proof . picker . membershipId ) && row . username === username )
check ( '原PDA仍仅有本仓作业员权限且不是受保护账号' , user && id ( user . tenantId ) === '0' && ! user . protectedAccount
&& id ( user . overseasWarehouseId ) === warehouseId && user . roleIds ? . length === 1 && id ( user . roleIds [ 0 ]) === id ( access . role . id ))
picker = { membershipId : user . membershipId , username , token : null }
const active = await request ( 'PUT' , `/api/platform/v1/iam/users/ ${ encodeURIComponent ( id ( user . membershipId )) } /status` , { status : 'ACTIVE' }, '为核对原PDA完成请求恢复本轮临时作业员' )
check ( '原临时PDA已恢复启用' , active ? . status === 'ACTIVE' )
const loggedIn = await request ( 'POST' , '/api/auth/login' , { loginId : username , password : `Pda ${ suffix } A719` }, '以原生成凭据登录本轮临时PDA' , '' )
picker . token = loggedIn ? . accessToken
} else {
const pickerContext = Object . create ( context )
proof . pickerMarker ||= ` ${ context . marker } -FPDA- ${ randomUUID (). slice ( 0 , 8 ) } `
pickerContext . marker = proof . pickerMarker
picker = access ? await createPicker ( pickerContext , 71 , warehouseId , access ) : null
}
check ( '专项使用本轮绑定起运仓的PDA账号' , picker ? . token && picker . membershipId )
proof . picker = { membershipId : picker . membershipId , username : picker . username }
stage ( 'EXTERNAL_RECEIPT' ); await verifyFleetExternalReceipt ( context , MODULE , trip , picker . token , { reconcilePalletized : resumePalletized , reconcileConfirmed : resumeReceiptConfirmed })
stage ( 'EXTERNAL_DISPATCH' ); await verifyFleetExternalDispatch ( context , MODULE , trip )
const external = trip . externalDispatch
stage ( 'FINANCE_CONSUMER' )
const sources = await request ( 'GET' , `/api/platform/v1/last-leg/finance-fee-sources? ${ new URLSearchParams ({ customerId , businessType : 'DELIVERY_ORDER' , businessNo : external . deliveryNo } )}` ,
undefined , '读取外接应收稳定来源编号和版本' )
const target = sources ? . filter ( row => row . feeItemCode === 'DELIVERY_FEE' && row . direction === 'RECEIVABLE' && row . sourceStatus === 'ACTIVE' )
check ( '外接应收来源只属于本次客户派送及实收' , target ? . length === 1 && target [ 0 ]. businessNo === external . deliveryNo
&& id ( target [ 0 ]. customerId ) === customerId && Number ( target [ 0 ]. originalAmount ) === 30 && target [ 0 ]. originalCurrency === 'CNY' )
let matched = []
for ( let attempt = 0 ; attempt < 30 ; attempt ++ ) {
const pool = await request ( 'GET' , `/api/platform/finance/v1/fee-pool?customerId= ${ encodeURIComponent ( customerId ) } &direction=RECEIVABLE` , undefined , '等待Finance消费本次外接费用' )
check ( 'Finance费用池为当前客户应收列表' , Array . isArray ( pool ) && pool . every ( row => id ( row . customerId ) === customerId && row . direction === 'RECEIVABLE' ))
matched = pool . filter ( row => id ( row . sourceId ) === id ( target [ 0 ]. sourceFeeId ) && Number ( row . sourceVersion ) === Number ( target [ 0 ]. sourceVersion ))
if ( matched . length ) break
await wait ( 1000 )
}
check ( 'Finance真实消费且仅一笔30CNY外接应收' , matched . length === 1 && matched [ 0 ]. sourceType === 'OVERSEAS_DELIVERY_FEE'
&& Number ( matched [ 0 ]. amount ) === 30 && matched [ 0 ]. currency === 'CNY' )
proof . finance = { status : 'CONSUMER_PERSISTED' , source : target [ 0 ], pool : matched , producerAck : 'NOT_VERIFIED' }
external . financeAcknowledgement = 'CONSUMER_PERSISTED_PRODUCER_ACK_NOT_VERIFIED'
stage ( 'PICK_EACH_DISPATCH' )
const dispatchNos = [ partial . dispatchNo , external . dispatchNo ]
proof . dispatchNos = dispatchNos
const dispatchDetails = []
for ( const [ index , dispatchNo ] of dispatchNos . entries ()) {
const pickingContext = Object . create ( context ); pickingContext . marker = ` ${ context . marker } -FLEET-PICK- ${ index } `
check ( '逐客户正式PDA拣货及完成成功' , await pickAll ( pickingContext , picker . token , dispatchNo ))
dispatchDetails . push ( await request ( 'GET' , ` ${ dispatchBase } / ${ encodeURIComponent ( dispatchNo ) } ` , undefined , '回查逐单拣货数量及未发车事实' ))
}
check ( '每张客户排车实际拣足但尚未发车' , dispatchDetails . every ( row => row . status === 'READY_TO_DEPART'
&& Number ( row . pickedPackageCount ) === Number ( row . packageCount ) && Number ( row . shippedPackageCount ) === 0 ))
stage ( 'WHOLE_TRIP_DEPARTURE' )
const recordVersion = await confirmFleetLoading ( context , MODULE , trip )
proof . departureRequest = { recordVersion , idempotencyKey : key (), remark : '本轮自有货与真实外接货统一发车' }
const departPath = `/api/pda/v1/last-leg/dispatches/ ${ encodeURIComponent ( partial . dispatchNo ) } /depart`
const departed = await request ( 'POST' , departPath , proof . departureRequest , 'PDA一次命令发出全车两张客户执行单' , picker . token )
check ( '整车发车包含所有排车且箱数守恒' , departed ? . tripNo === trip . tripNo && departed . status === 'IN_TRANSIT'
&& JSON . stringify ( departed . dispatches . map ( row => row . dispatchNo ). sort ()) === JSON . stringify ([... dispatchNos ]. sort ())
&& Number ( departed . shippedPackages ) === dispatchDetails . reduce (( n , row ) => n + Number ( row . packageCount ), 0 ))
const departedReplay = await request ( 'POST' , departPath , proof . departureRequest , '整车发车同键重放' , picker . token )
check ( '发车重放不改变整车实发数量' , departedReplay ? . replayed === true && Number ( departedReplay . shippedPackages ) === Number ( departed . shippedPackages ))
proof . departure = departed
2026-10-03 20:06:02 +08:00
await completeDepartedFleetScenario ( context , trip , picker , dispatchDetails , { costActors , onHandoverReady })
} 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 } = {}) {
const proof = context . createdResources . fleetAcceptance , customerId = proof . customerId , warehouseId = proof . warehouseId
const partial = context . createdResources . fleetFeeCoverageCases . find ( row => row . mode === 'PARTIAL' )
const external = trip . externalDispatch , dispatchNos = proof . dispatchNos
const stage = value => { proof . stage = value ; proof . checkpoints . push ({ stage : value , at : new Date (). toISOString () }); console . log ( 'FLEET_STAGE=' + value ) }
const check = ( label , ok ) => { if ( ! ok ) throw Error ( label ) }
async function request ( method , path ) { const r = await context . request ( method , path ); check ( 'Continuation read failed' , success ( r )); return r . body . data }
2026-10-03 20:24:56 +08:00
if ( ! costsAlreadyVerified ) {
stage ( 'TRIP_OPERATING_COSTS' )
await verifyCosts ( context , MODULE , trip , { ... costActors , shares : fleetCostShares ( trip , dispatchDetails ) })
} else check ( 'Recovery cost proof must remain verified' , trip . costAcceptance ? . status === 'VERIFIED' )
2026-10-03 14:52:04 +08:00
stage ( 'IN_TRANSIT_HANDOVER' )
2026-10-03 20:06:02 +08:00
await handover ( context , trip , picker . token , dispatchDetails , {
2026-10-03 21:00:07 +08:00
reviewedVehicle ,
2026-10-03 14:52:04 +08:00
onReady : onHandoverReady ? async fixture => {
stage ( 'PDA_UI_CHECKPOINT' )
await onHandoverReady ({ fixture , picker })
} : undefined ,
})
2026-10-03 06:26:33 +08:00
stage ( 'RECEIPTS_AND_RESOURCE_RELEASE' )
proof . receipts = []
2026-10-03 16:11:52 +08:00
if ( context . context . fleetScenario === 'return-redispatch' ) {
stage ( 'RETURN_REDISPATCH' )
const returned = await verifyFleetReturnRedispatch ( context , MODULE , trip , { customerId , warehouseId , pickerToken : picker . token ,
dispatchNos , deliveryNos : [... partial . proof . deliveryNos , external . deliveryNo ] })
proof . receipts = returned . originalSignatures . map ( row => ({ deliveryNo : row . deliveryNo , signedAt : row . signedAt }))
context . context . saveFleetReturnRedispatchFixture ( returned )
} else {
2026-10-03 06:26:33 +08:00
for ( const [ index , deliveryNo ] of [... partial . proof . deliveryNos , external . deliveryNo ]. entries ()) {
const delivery = await request ( 'GET' , `/api/platform/v1/last-leg/delivery-orders/ ${ encodeURIComponent ( deliveryNo ) } ?customerId= ${ encodeURIComponent ( customerId ) } ` ,
undefined , '签收前读取本次实际派送' )
2026-10-03 20:06:02 +08:00
const completed = await completeDelivery ( context , delivery . order , customerId , index + 71 )
2026-10-03 06:26:33 +08:00
check ( '每个目的地真实保存签收资料后完成' , completed ? . order ? . status === 'DELIVERED' && completed . order . signedAt )
proof . receipts . push ({ deliveryNo , signedAt : completed . order . signedAt })
}
2026-10-03 16:11:52 +08:00
}
2026-10-03 20:06:02 +08:00
proof . closure = await closeTrip ( context , MODULE , trip )
2026-10-03 06:26:33 +08:00
check ( '本次车次货物结案及车辆司机释放完成' , proof . closure ? . status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' )
2026-10-03 14:52:04 +08:00
stage ( 'HANDOVER_RESOURCE_CLEANUP' )
2026-10-03 20:06:02 +08:00
await cleanup ( context , MODULE , trip )
2026-10-03 06:26:33 +08:00
proof . status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' ; stage ( 'FINISHED' )
return proof
}
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 }
}