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'
import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs'
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 16:11:52 +08:00
import { verifyFleetReturnRedispatch } 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 16:11:52 +08:00
if ( ! [ '--environment' , '--run-id' , '--output' , '--resume-report' , '--resume-stage' , '--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 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' )
if ( Boolean ( config . resumeReport ) !== Boolean ( config . resumeStage ) || ( config . resumeStage && ! [ 'FULL_COVERAGE' , 'FULL_COVERAGE_PERSISTED' , 'EXTERNAL_RECEIPT_READY' , 'EXTERNAL_PALLETIZED' , 'EXTERNAL_RECEIPT_CONFIRMED' , 'TERMINAL_READONLY' ]. includes ( config . resumeStage ))) throw Error ( 'Explicit reviewed checkpoint required' )
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 16:11:52 +08:00
if ( ! [ 'handover' , 'return-redispatch' ]. includes ( config . scenario ) || ( config . scenario === 'return-redispatch' && config . resumeReport ))
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 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 14:52:04 +08:00
outputDir : config . outputDir || `reports/fleet- ${ config . runId } ` , producer : config . resumeReport ? 'Reviewed read-only FULL_COVERAGE checkpoint in the same original fixture' : 'phase4-overseas-dispatch with its normal fresh prerequisites' ,
2026-10-03 06:26:33 +08:00
resumeReport : config . resumeReport || null , resumeStage : config . resumeStage || null ,
stages : [ 'FULL original coverage' , 'formal cancel/unbind/re-arrange' , 'PARTIAL original coverage' , 'reservation lifecycle' ,
'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 )
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 14:52:04 +08:00
export async function runFleetHandoverStage ( context , trip , pickerToken , dispatchDetails , { verify = verifyFleetHandover , onReady } = {}) {
const state = await verify ( context , MODULE , trip , pickerToken , dispatchDetails )
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 ,
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
}
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 ) {
const entry = { method , path , body : redactEvidence ( body ), status : 'SENDING' }
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 ,
requestId : response ? . requestId , traceId : response ? . traceId , result : redactEvidence ( response ? . body ? . data ) })
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 } ` ,
phone : await context . findAvailablePhone ( MODULE ), email : ` ${ username } @example.com` , tenantId : '0' ,
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
}
}
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 14:52:04 +08:00
stage ( 'TRIP_OPERATING_COSTS' )
await verifyFleetTripCosts ( context , MODULE , trip , { ... costActors , shares : fleetCostShares ( trip , dispatchDetails ) })
stage ( 'IN_TRANSIT_HANDOVER' )
await runFleetHandoverStage ( context , trip , picker . token , dispatchDetails , {
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 , '签收前读取本次实际派送' )
const completed = await saveReceiptAndComplete ( context , delivery . order , customerId , index + 71 )
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 06:26:33 +08:00
proof . closure = await finishFleetTrip ( context , MODULE , trip )
check ( '本次车次货物结案及车辆司机释放完成' , proof . closure ? . status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' )
2026-10-03 14:52:04 +08:00
stage ( 'HANDOVER_RESOURCE_CLEANUP' )
await cleanupFleetHandoverResources ( context , MODULE , trip )
2026-10-03 06:26:33 +08:00
proof . status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' ; stage ( 'FINISHED' )
} finally { await disablePicker ( context , picker , 71 ) }
return proof
}
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 )
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 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 = {}) => {
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 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 }
}