2026-10-03 22:13:15 +08:00
import { existsSync , openSync , writeFileSync , fsyncSync , closeSync , readFileSync } from 'node:fs'
import { join , resolve } from 'node:path'
import { redactEvidence } from '../fba-core-regression-test-env.mjs'
2026-10-03 16:11:52 +08:00
import { randomUUID , createHash } from 'node:crypto'
import { prepareFleetTrip , confirmFleetLoading , finishFleetTrip } from './fleet-trip-fulfillment.mjs'
2026-10-21 14:30:41 +08:00
import { pickAll , saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs'
2026-10-20 10:35:52 +08:00
import { version , createBody , candidatePage , readDispatch , replay as replayCommand } from './grouped-dispatch-acceptance.mjs'
2026-10-03 16:11:52 +08:00
const BASE = '/api/platform/v1/last-leg'
2026-10-20 10:35:52 +08:00
const id = value => { if ( typeof value === 'number' && ! Number . isSafeInteger ( value )) throw Error ( 'Unsafe server ID; retain Long string' ); return value == null ? '' : String ( value ) }
2026-10-03 16:11:52 +08:00
const key = () => `redispatch- ${ randomUUID () } `
const scoped = ( path , customerId ) => ` ${ path } ? ${ new URLSearchParams ({ customerId } )}`
const ok = response => response ? . http === 200 && String ( response ? . body ? . code ) === '200'
const copy = value => value === undefined ? undefined : structuredClone ( value )
function definiteResponse ( response ) {
const http = response ? . http , body = response ? . body
if ( ! Number . isInteger ( http ) || http < 200 || http >= 500 || ! body || typeof body !== 'object' || Array . isArray ( body )
|| ! response . requestId || ! response . traceId ) return false
const code = id ( body . code )
return http === 200 && code === '200' || /^[A-Z]\d{4}$/ . test ( code ) && ( http === 200 || http >= 400 )
}
export function selectReturnSources ( rows ) {
const domestic = rows . filter ( row => row . tail ? . orderType === 'FIRST_LEG' && row . tail . sourceInboundId && row . tail . inboundCode === row . relation . inboundCode )
const a = domestic . find ( row => Number ( row . tail . packageCount ) > 2 && row . relation . returnSources ? . some ( source => Number ( source . maxReturnPackages ) >= 2 ))
const b = domestic . find ( row => a && row . tail . sourceInboundId !== a . tail . sourceInboundId )
if ( ! a || ! b ) throw Error ( 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B' )
for ( const selected of [ a , b ]) {
const packages = domestic . filter ( row => row . tail . sourceInboundId === selected . tail . sourceInboundId )
. reduce (( sum , row ) => sum + ( row . relation . returnSources || []). reduce (( n , source ) => n + Number ( source . maxReturnPackages ), 0 ), 0 )
if ( packages !== Number ( selected . tail . packageCount )) throw Error ( 'Current departure must cover each selected source completely; no old shipments may fill the fixture' )
}
return { a , b }
}
2026-10-21 14:30:41 +08:00
/** 只消费本轮自动发运事实;来源恢复保留已启动派送,每次请求发送前持久记录。 */
2026-10-03 16:11:52 +08:00
export async function verifyFleetReturnRedispatch ( context , module , trip , input , dependencies = {}) {
const run = context . createdResources ? . fleetAcceptance
2026-10-20 10:35:52 +08:00
if ( Object . keys ( dependencies ). some ( name => ! [ 'wait' , 'onBeforeRedispatchSignature' ]. includes ( name ))) throw Error ( 'Only readonly audit and wait observers may be supplied; native production, picking and signature cannot be replaced' )
const departure = run ? . departure
2026-10-03 16:11:52 +08:00
if ( ! context . marker || ! context . createdResources ? . fleetTrips ? . includes ( trip ) || run ? . marker !== context . marker
2026-10-21 14:30:41 +08:00
|| run . tripNo !== trip . tripNo || departure ? . dispatchNo !== trip . dispatchNo || departure . status !== 'ACTIVE' || departure . stage !== 'DELIVERING' || departure . action !== 'LOADING_COMPLETE'
|| ! departure . result ? . confirmedLoad || ! input ? . pickerToken
2026-10-03 16:11:52 +08:00
|| id ( run . customerId ) !== id ( input . customerId ) || id ( run . warehouseId ) !== id ( input . warehouseId )
2026-10-20 10:35:52 +08:00
|| JSON . stringify ([ departure . dispatchNo ]) !== JSON . stringify ([...( input . dispatchNos || [])]. sort ())
2026-10-21 14:30:41 +08:00
|| JSON . stringify (( run . departureDeliveries || []). filter ( row => id ( row . customerId ) === id ( input . customerId )). map ( row => row . deliveryNo ). sort ()) !== JSON . stringify ([...( input . deliveryNos || [])]. sort ())
2026-10-03 16:11:52 +08:00
|| ! input . deliveryNos ? . length || trip . returnRedispatch || typeof context . checkpoint !== 'function' )
throw Error ( 'Redispatch requires a new owned departure, current PDA session and durable checkpoints; no automatic restart' )
const proof = trip . returnRedispatch = { marker : context . marker , status : 'CHECKING' , requests : [], returnPackages : 2 ,
readOnlyEvidenceRequired : [ 'Before redispatch signature: source A completion outbox count=0 and domestic inbound not DELIVERED; source B completion outbox count=1' ,
'After redispatch signature and exact replay: A and B each have exactly one OVERSEAS_DELIVERY_COMPLETED outbox event by customer+sourceInboundId; inspect delivery ACK and domestic inbound completion separately' ,
'Join return receipt ledger_id and source_outbound_no to immutable RETURN_STOCK_IN; join new picks by return_receipt_id; original shipment and signed facts remain unchanged' ] }
const check = ( label , condition ) => { if ( ! condition ) throw Error ( label ) }
2026-10-20 10:35:52 +08:00
let stopped = false
2026-10-03 16:11:52 +08:00
const io = Object . create ( context )
io . assert = (... args ) => { context . assert (... args ); if ( ! args [ 3 ]) throw Error ( args [ 1 ]) }
async function invoke ( method , path , body , token , file , options ) {
2026-10-20 10:35:52 +08:00
if ( stopped ) throw Error ( 'Unknown prior outcome; preserve original command and stop' )
2026-10-03 16:11:52 +08:00
const entry = { method , path , body : copy ( body ), actor : token ? 'PDA' : 'PLATFORM' , status : 'SENDING' }
2026-10-03 23:58:47 +08:00
if ( file ) entry . file = { name : file . name , type : file . type , sizeBytes : file . content . byteLength , sha256 : createHash ( 'sha256' ). update ( file . content ). digest ( 'hex' ) }
2026-10-03 16:11:52 +08:00
proof . requests . push ( entry ); await context . checkpoint ()
try {
const response = file ? await context . requestMultipart ( method , path , file ) : await context . request ( method , path , body , token , options )
if ( ! definiteResponse ( response )) {
2026-10-20 10:35:52 +08:00
stopped = true ; proof . outcomeUnknown = true
2026-10-03 16:11:52 +08:00
Object . assign ( entry , { status : 'UNKNOWN_RESULT' , http : response ? . http , code : response ? . body ? . code ,
requestId : response ? . requestId , traceId : response ? . traceId })
await context . checkpoint ()
throw Error ( `HTTP transport outcome unknown: ${ method } ${ path } ; preserve the original request, do not resend` )
}
Object . assign ( entry , { status : 'RESPONDED' , http : response ? . http , code : response ? . body ? . code ,
requestId : response ? . requestId , traceId : response ? . traceId , result : copy ( response ? . body ? . data ) })
await context . checkpoint ()
check ( `Missing HTTP correlation: ${ method } ${ path } ` , response ? . requestId && response ? . traceId )
return response
2026-10-20 10:35:52 +08:00
} catch ( error ) { if ( entry . status === 'SENDING' ) { stopped = true ; proof . outcomeUnknown = true ; entry . status = 'UNKNOWN_RESULT' ; entry . error = error . message ; await context . checkpoint () } throw error }
2026-10-03 16:11:52 +08:00
}
io . request = ( method , path , body , token , options ) => invoke ( method , path , body , token , null , options )
io . requestMultipart = ( method , path , file ) => invoke ( method , path , undefined , undefined , file )
async function request ( method , path , body , token ) {
const response = await io . request ( method , path , body , token ); check ( `HTTP failed: ${ method } ${ path } ` , ok ( response )); return response . body . data
}
2026-10-20 10:35:52 +08:00
const deps = { onBeforeRedispatchSignature : dependencies . onBeforeRedispatchSignature || context . context ? . onBeforeRedispatchSignature ,
wait : dependencies . wait || ( ms => new Promise ( resolve => setTimeout ( resolve , ms ))) }
const native = { command : ( method , path , body , label ) => request ( method , path , body ), check : ( label , response , passed ) => check ( label , passed ) }
const replay = async ( path , body , first , label ) => { const again = await replayCommand ( native , path , body , first , label ); check ( label , again . replayed === true ); return again }
2026-10-03 16:11:52 +08:00
const customerId = id ( input . customerId ), warehouseId = id ( input . warehouseId )
const tail = relation => request ( 'GET' , scoped ( ` ${ BASE } /orders/ ${ relation . lastMileOrderId } ` , customerId ))
const delivery = number => request ( 'GET' , scoped ( ` ${ BASE } /delivery-orders/ ${ encodeURIComponent ( number ) } ` , customerId ))
async function domestic ( selected ) {
const page = await request ( 'GET' , `/api/platform/inbounds? ${ new URLSearchParams ({ tenantId : customerId , inboundCode : selected . relation . inboundCode , page : '1' , size : '200' } )}` )
const matches = ( page . records || []). filter ( row => row . inboundCode === selected . relation . inboundCode )
check ( 'Domestic source must resolve uniquely through its current inbound code' , matches . length === 1 && matches [ 0 ]. processStatus )
return matches [ 0 ]
}
async function domesticBoundary ( a , b , allSigned ) {
const snapshots = []
for ( let attempt = 0 ; attempt < 30 ; attempt ++ ) {
const A = await domestic ( a ), B = await domestic ( b ); snapshots . push ({ A , B })
if ( ! allSigned ) check ( 'Incomplete A must never be marked delivered domestically' , A . processStatus !== 'DELIVERED' )
if ( B . processStatus === 'DELIVERED' && ( ! allSigned || A . processStatus === 'DELIVERED' )) return snapshots
if ( attempt < 29 ) await deps . wait ( 1000 )
}
throw Error ( allSigned ? 'Domestic source completion has not converged after redispatch signature' : 'Independent source B domestic completion did not converge' )
}
2026-10-20 10:35:52 +08:00
async function roots ( a , b , beforeSignature ) {
const snapshots = []
const known = value => [ 'DRAFT' , 'SUBMITTED' , 'APPROVED' , 'ACCEPTED' , 'REJECTED' , 'CANCELLED' , 'PROCESSING' , 'COMPLETED' ]. includes ( value )
for ( const [ name , selected ] of [[ 'A' , a ], [ 'B' , b ]]) {
const sourceId = id ( selected . tail . sourceOrderId )
check ( 'Original source order provenance is required for root and child guards' , /^[1-9]\d*$/ . test ( sourceId ))
const rawPath = order => `/api/platform/v1/orders/first-leg/ ${ encodeURIComponent ( order ) } ? ${ new URLSearchParams ({ tenantId : customerId } )}`
const source = await request ( 'GET' , rawPath ( sourceId )), order = source ? . order
const rootId = order ? . orderRole === 'MASTER' ? id ( order . id ) : id ( order ? . rootOrderId )
check ( 'Original source and fulfillment root belong to current customer' , id ( order ? . id ) === sourceId && id ( order . customerId ) === customerId && id ( order . tenantId ) === customerId
&& known ( order . status ) && /^[1-9]\d*$/ . test ( rootId ) && ( order . orderRole === 'MASTER' || order . orderRole === 'CHILD' && id ( order . parentOrderId ) === rootId ))
const root = rootId === sourceId ? source : await request ( 'GET' , rawPath ( rootId ))
const tree = await request ( 'GET' , `/api/platform/v1/orders/ ${ rootId } /fulfillment-tree? ${ new URLSearchParams ({ tenantId : customerId } )}` )
check ( 'Formal root and explicit child list remain customer-owned' , id ( root . order ? . id ) === rootId && root . order . orderRole === 'MASTER' && id ( root . order . customerId ) === customerId && id ( root . order . tenantId ) === customerId && known ( root . order . status )
&& id ( tree . parentOrderId ) === rootId && tree . parentOrderNo === root . order . orderNo && Array . isArray ( tree . children ) && Array . isArray ( root . statusHistory ))
const children = []
for ( const row of tree . children . filter ( row => id ( row . orderId ) !== rootId )) {
const child = await request ( 'GET' , rawPath ( id ( row . orderId )))
check ( 'Every formal child stays linked to the actual root' , id ( child . order ? . id ) === id ( row . orderId ) && child . order . orderRole === 'CHILD'
&& id ( child . order . rootOrderId ) === rootId && id ( child . order . parentOrderId ) === rootId && id ( child . order . customerId ) === customerId && id ( child . order . tenantId ) === customerId && known ( child . order . status ) && child . order . orderNo === row . orderNo )
children . push ( child . order )
}
check ( 'Original source is present in its real formal root tree' , sourceId === rootId || children . some ( child => id ( child . id ) === sourceId ))
const pending = children . some ( child => ! [ 'COMPLETED' , 'CANCELLED' ]. includes ( child . status ))
const completed = root . statusHistory . some ( event => event . action === 'OVERSEAS_DELIVERED' ) && ! pending
check ( 'Root cannot complete ahead of valid children or overseas delivery fact' , completed
? root . order . status === 'COMPLETED' && root . statusHistory . some ( event => event . action === 'FULFILLMENT_COMPLETED' )
: root . order . status !== 'COMPLETED' && ! root . statusHistory . some ( event => event . action === 'FULFILLMENT_COMPLETED' ))
if ( beforeSignature && name === 'A' ) check ( 'Incomplete A child cannot be completed before redispatch signature' , order . status !== 'COMPLETED' )
snapshots . push ({ source : name , sourceOrder : order , root , tree , children })
}
return snapshots
}
2026-10-03 16:11:52 +08:00
try {
const rows = []
proof . originalDeliveries = []
for ( const number of [... new Set ( input . deliveryNos )]) {
const current = await delivery ( number )
2026-10-20 10:35:52 +08:00
check ( 'Fixture deliveries must be unsigned and from the supplied current dispatches' , ! current . order . signedAt && input . dispatchNos . includes ( current . order . dispatchNo )
&& id ( current . order . customerId ) === customerId && id ( current . order . overseasWarehouseId ) === warehouseId )
2026-10-03 16:11:52 +08:00
proof . originalDeliveries . push ( copy ( current . order ))
2026-10-03 22:13:15 +08:00
if ( input . sourceSelectionRecovery ) check ( 'Reviewed source recovery must retain the already started delivery; never start it again' , current . order . status === 'DELIVERING' )
2026-10-03 16:11:52 +08:00
if ( current . order . status === 'WAIT_DELIVERY' ) {
2026-10-20 10:35:52 +08:00
version ( current . order . version )
2026-10-03 16:11:52 +08:00
const started = await request ( 'PUT' , scoped ( ` ${ BASE } /delivery-orders/ ${ number } /status` , customerId ), {
2026-10-20 10:35:52 +08:00
status : 'DELIVERING' , expectedVersion : version ( current . order . version ), remark : ` ${ context . marker } current-run delivery underway` ,
2026-10-03 16:11:52 +08:00
})
check ( 'Return candidates are opened only after formal delivery start' , started . deliveryStatus === 'DELIVERING' )
}
const relations = await request ( 'GET' , scoped ( ` ${ BASE } /delivery-orders/ ${ encodeURIComponent ( number ) } /return-inbound-options` , customerId ))
for ( const relation of relations ) {
2026-10-18 16:34:14 +08:00
check ( 'Return source relation belongs to the original delivery and has a real master ID' , /^[1-9][0-9]*$/ . test ( id ( relation . lastMileOrderId ))
&& id ( relation . customerId ) === customerId && id ( relation . overseasWarehouseId ) === warehouseId
&& relation . deliveryNo === number && relation . outboundNo === current . order . outboundNo )
2026-10-03 16:11:52 +08:00
const detail = await tail ( relation )
2026-10-18 16:34:14 +08:00
check ( 'Tail source identity, customer and warehouse match this candidate' , id ( detail . order . id ) === id ( relation . lastMileOrderId )
&& detail . order . orderNo === relation . lastMileOrderNo && detail . order . deliveryOrderNo === number
&& detail . order . outboundOrderNo === relation . outboundNo
&& id ( detail . order . customerId ) === customerId && id ( detail . order . overseasWarehouseId ) === warehouseId )
2026-10-03 16:11:52 +08:00
rows . push ({ relation , tail : detail . order , delivery : current . order })
}
}
const { a , b } = selectReturnSources ( rows )
proof . domesticBefore = { A : await domestic ( a ), B : await domestic ( b ) }
check ( 'Both source inbounds must be unfinished in this fresh sample' , proof . domesticBefore . A . processStatus !== 'DELIVERED' && proof . domesticBefore . B . processStatus !== 'DELIVERED' )
proof . sources = { A : copy ( a ), B : copy ( b ) }; await context . checkpoint ()
2026-10-18 16:34:14 +08:00
const source = a . relation . returnSources . find ( row => /^ [ 1 - 9 ][ 0 - 9 ] * $ / . test ( id ( row . outboundPalletItemId ))
&& /^[1-9][0-9]*$/ . test ( id ( row . inventoryPositionId )) && row . returnCartonId == null
&& Number . isSafeInteger ( Number ( row . maxReturnPackages )) && Number ( row . maxReturnPackages ) >= 2 )
check ( 'Selected master must contain the actual ordinary stock source for the two returned boxes' , Boolean ( source ))
2026-10-20 10:35:52 +08:00
const markPath = scoped ( ` ${ BASE } /delivery-orders/ ${ a . delivery . deliveryNo } /mark-return` , customerId )
proof . markReturnRequest = {
2026-10-18 16:34:14 +08:00
lastMileOrderId : id ( a . relation . lastMileOrderId ), reason : ` ${ context . marker } two boxes refused before signature` , returnDirection : 'OVERSEAS_WAREHOUSE' ,
2026-10-03 16:11:52 +08:00
returnLogisticsNo : ` ${ context . marker } -RET` , items : [{ outboundPalletItemId : source . outboundPalletItemId , inventoryPositionId : source . inventoryPositionId , packageCount : 2 }], idempotencyKey : key (),
2026-10-20 10:35:52 +08:00
}
const marked = await request ( 'POST' , markPath , proof . markReturnRequest )
await replay ( markPath , proof . markReturnRequest , marked , 'Mark-return original command replay' )
2026-10-03 16:11:52 +08:00
proof . exceptionNo = marked . exceptionNo
const exceptionPath = ` ${ BASE } /exception-orders/ ${ encodeURIComponent ( marked . exceptionNo ) } `
const resource = `/api/platform/v1/overseas-warehouse-resources/ ${ warehouseId } `
const palletCode = `RET- ${ randomUUID () } `
const palletId = id ( await request ( 'POST' , ` ${ resource } /pallets` , { palletCode , palletType : '木托盘' , capacityCbm : 2 , unitPrice : 0 , status : 'IDLE' , remark : context . marker }))
const pallet = await request ( 'GET' , ` ${ resource } /pallets/ ${ palletId } ` )
const locationPage = await request ( 'GET' , ` ${ resource } /locations?page=1&size=200` )
const location = ( Array . isArray ( locationPage ) ? locationPage : locationPage . records || []). find ( row => id ( row . id ) === id ( pallet . locationId ))
check ( 'New return pallet has a real same-warehouse location' , id ( pallet . warehouseId ) === warehouseId && location ? . locationCode && pallet . palletCode === palletCode )
2026-10-20 10:35:52 +08:00
const paged = async path => {
const rows = []; let total
for ( let page = 1 ; ; page ++ ) {
const current = await request ( 'GET' , ` ${ path } &page= ${ page } &size=200` )
check ( 'Inventory pagination must remain complete and stable' , Array . isArray ( current ? . records ) && Number . isSafeInteger ( Number ( current . total )) && Number ( current . total ) >= 0
&& ( total === undefined || total === Number ( current . total )) && ( current . records . length > 0 || rows . length === Number ( current . total )))
total ??= Number ( current . total ); rows . push (... current . records ); if ( rows . length >= total ) { check ( 'Inventory pagination cannot omit or duplicate rows' , rows . length === total && new Set ( rows . map ( row => id ( row . id ))). size === rows . length ); return rows }
}
}
const inventory = async label => {
const filters = new URLSearchParams ({ customerId , overseasWarehouseId : warehouseId , inboundCode : a . relation . inboundCode })
const balances = await paged ( ` ${ BASE } /inventory/balances? ${ filters } ` ), positions = []
for ( const balance of balances ) {
check ( 'Returned balance belongs to original customer and warehouse' , id ( balance . customerId ) === customerId && id ( balance . overseasWarehouseId ) === warehouseId )
const rows = await request ( 'GET' , ` ${ BASE } /inventory/balances/ ${ id ( balance . id ) } /positions? ${ new URLSearchParams ({ customerId , palletNo : palletCode } )}` )
check ( 'Actual return positions are explicit rows' , Array . isArray ( rows ))
for ( const row of rows . filter ( row => id ( row . palletId ) === palletId )) {
check ( 'Returned stock identity stays within original customer warehouse and inbound' , id ( row . customerId ) === customerId && id ( row . overseasWarehouseId ) === warehouseId && row . inboundCode === a . relation . inboundCode && row . palletNo === palletCode )
positions . push ( row )
}
}
const ledger = await paged ( ` ${ BASE } /inventory/ledger? ${ filters } & ${ new URLSearchParams ({ businessType : 'RETURN_STOCK_IN' , businessOrderNo : marked . exceptionNo , outboundOrderNo : a . delivery . outboundNo , palletNo : palletCode } )}` )
check ( 'Return stock-in ledger is immutable original-shipment evidence' , ledger . every ( row => id ( row . customerId ) === customerId && id ( row . overseasWarehouseId ) === warehouseId
&& row . inboundCode === a . relation . inboundCode && row . palletNo === palletCode && row . businessType === 'RETURN_STOCK_IN' && row . businessOrderNo === marked . exceptionNo && row . outboundOrderNo === a . delivery . outboundNo ))
const result = { label , positions , ledger , available : positions . reduce (( n , row ) => n + Number ( row . availablePackages ), 0 ), picked : positions . reduce (( n , row ) => n + Number ( row . pickedPackages ), 0 ) }
proof . inventory ||= []; proof . inventory . push ( copy ( result )); return result
}
const empty = await inventory ( 'BEFORE_RETURN' ); check ( 'Fresh target pallet contains no previous return stock' , empty . available === 0 && empty . picked === 0 && empty . ledger . length === 0 )
2026-10-03 16:11:52 +08:00
const started = await request ( 'POST' , scoped ( ` ${ exceptionPath } /start-processing` , customerId ), { expectedVersion : marked . version , idempotencyKey : key (), handlingAction : '实际退回2箱' })
const uploaded = await io . requestMultipart ( 'POST' , scoped ( ` ${ exceptionPath } /evidence` , customerId ), {
name : ` ${ context . marker } -return.txt` , type : 'text/plain' , content : new TextEncoder (). encode ( ` ${ context . marker } synthetic evidence of 2 returned boxes` ),
})
check ( 'Return evidence uploaded through owned exception API' , ok ( uploaded ) && uploaded . body . data ? . fileId )
const receipt = { expectedVersion : started . version , idempotencyKey : key (), handlingResult : '原出库2箱实际退回入仓' , evidenceFileIds : [ uploaded . body . data . fileId ],
receipts : [{ outboundPalletItemId : source . outboundPalletItemId , inventoryPositionId : source . inventoryPositionId , receivedPackages : 2 ,
targetPalletId : palletId , targetPalletNo : palletCode , targetLocationId : location . id , targetLocationCode : location . locationCode }] }
const restored = await request ( 'POST' , scoped ( ` ${ exceptionPath } /complete` , customerId ), receipt )
check ( 'Exactly two boxes restored' , restored . status === 'COMPLETED' && Number ( restored . restoredPackages ) === 2 )
2026-10-20 10:35:52 +08:00
const stockIn = await inventory ( 'RETURN_STOCK_IN' )
check ( 'Actual two returned boxes produce exactly one stock-in ledger' , stockIn . available === 2 && stockIn . picked === 0 && stockIn . ledger . length === 1 && Number ( stockIn . ledger [ 0 ]. deltaPackages ) === 2 )
const restoredReplay = await request ( 'POST' , scoped ( ` ${ exceptionPath } /complete` , customerId ), receipt )
check ( 'Same return key replays the same adjustment' , restoredReplay . replayed === true && id ( restoredReplay . inventoryAdjustmentId ) === id ( restored . inventoryAdjustmentId ))
const stockReplay = await inventory ( 'RETURN_REPLAY' )
check ( 'Return replay cannot duplicate stock or ledger' , stockReplay . available === stockIn . available && stockReplay . picked === stockIn . picked && JSON . stringify ( stockReplay . ledger ) === JSON . stringify ( stockIn . ledger ) && JSON . stringify ( stockReplay . positions ) === JSON . stringify ( stockIn . positions ))
const history = await request ( 'GET' , scoped ( exceptionPath , customerId ))
check ( 'Owned exception exposes the actual unique returned receipt business number' , history ? . order ? . exceptionNo === marked . exceptionNo && id ( history . order . customerId ) === customerId
&& history . order . outboundNo === a . delivery . outboundNo && history . returnReceipts ? . length === 1 && history . returnReceipts [ 0 ]. receiptNo && Number ( history . returnReceipts [ 0 ]. receivedPackages ) === 2 )
2026-10-03 16:11:52 +08:00
proof . returned = { palletId , palletCode , adjustmentId : restored . inventoryAdjustmentId }
proof . originalSignatures = []
for ( const [ index , number ] of input . deliveryNos . entries ()) {
const current = await delivery ( number )
2026-10-20 10:35:52 +08:00
const signed = await saveReceiptAndComplete ( io , current . order , customerId , 171 + index )
2026-10-03 23:09:24 +08:00
check ( 'Original remaining cargo actually signed with a stored quantity' , signed ? . order ? . signedAt
&& Number . isSafeInteger ( signed . order . signedPackages ) && signed . order . signedPackages >= 0 )
2026-10-03 16:11:52 +08:00
proof . originalSignatures . push ( copy ( signed . order ))
}
const beforeA = await tail ( a . relation ), beforeB = await tail ( b . relation )
check ( 'A remains incomplete while independently complete B is delivered' , ! beforeA . order . subOrderStatus ? . includes ( 'DELIVERY:DELIVERED' ) && beforeB . order . subOrderStatus ? . includes ( 'DELIVERY:DELIVERED' ))
proof . beforeRedispatch = { A : beforeA , B : beforeB }
proof . domesticBeforeRedispatch = await domesticBoundary ( a , b , false )
2026-10-20 10:35:52 +08:00
proof . rootsBeforeRedispatch = await roots ( a , b , true )
const candidates = await candidatePage ( native , warehouseId , { orderNo : a . relation . inboundCode })
const matches = candidates . filter ( row => row . selection ? . sourceType === 'RETURN_RECEIPT' && id ( row . selection . warehousePalletId ) === palletId
&& id ( row . customerId ) === customerId && id ( row . warehouseId ) === warehouseId && row . orderNo === a . tail . orderNo && row . inboundCode === a . relation . inboundCode && row . palletNo === palletCode )
check ( 'Exactly one fresh receipt candidate contains only the returned two boxes' , matches . length === 1 && Number ( matches [ 0 ]. availablePackages ) === 2 && matches [ 0 ]. selection . planPalletId == null )
const selected = matches [ 0 ]
proof . createRequest = createBody ( context . marker , 'RETURN-REDISPATCH' , [ selected ])
const arranged = await request ( 'POST' , ` ${ BASE } /dispatches` , proof . createRequest )
check ( 'Redispatch is a native new execution document containing only two boxes' , arranged . dispatchNo && ! input . dispatchNos . includes ( arranged . dispatchNo )
&& arranged . action === 'CREATE' && arranged . status === 'ACTIVE' && arranged . stage === 'BEFORE_LOADING' && Number ( arranged . counts ? . packageCount ) === 2 )
await replay ( ` ${ BASE } /dispatches` , proof . createRequest , arranged , 'Native Create original-key replay keeps the same redispatch' )
const actual = await readDispatch ( native , arranged . dispatchNo )
check ( 'Created redispatch readback preserves exact returned receipt customer and stock identity' , actual . warehouseId === warehouseId && actual . items . items . length === 1
&& actual . items . items [ 0 ]. sourceKey === selected . sourceKey && id ( actual . items . items [ 0 ]. returnReceiptId ) === id ( selected . selection . returnReceiptId )
&& id ( actual . items . items [ 0 ]. warehousePalletId ) === palletId && id ( actual . items . items [ 0 ]. customerId ) === customerId && Number ( actual . items . items [ 0 ]. scheduledPackages ) === 2 )
const newTrip = await prepareFleetTrip ( io , module , arranged . dispatchNo , warehouseId )
proof . newTripNo = newTrip . tripNo ; proof . returnReceiptNo = history . returnReceipts [ 0 ]. receiptNo ; proof . returnReceiptId = selected . selection . returnReceiptId ; proof . redispatchNo = arranged . dispatchNo
check ( 'Fresh return stock can be picked via PDA' , await pickAll ( io , input . pickerToken , arranged . dispatchNo ))
const picked = await inventory ( 'RETURN_PICKED' )
check ( 'Actual returned stock is picked once without rewriting the stock-in ledger' , picked . available === 0 && picked . picked === 2 && JSON . stringify ( picked . ledger ) === JSON . stringify ( stockIn . ledger ))
await confirmFleetLoading ( io , module , newTrip , input . pickerToken , input . actualMeasurements )
2026-10-21 14:30:41 +08:00
// 装车完成已原子生成派送并发运,只回查单据,不重复发送退役安排和发车命令。
2026-10-20 10:35:52 +08:00
const current = await request ( 'GET' , ` ${ BASE } /dispatches/ ${ encodeURIComponent ( arranged . dispatchNo ) } ` )
2026-10-21 14:30:41 +08:00
const transfer = current . documents
check ( 'Redispatch produces a new single destination delivery' , transfer ? . deliveries ? . length === 1 && ! input . deliveryNos . includes ( transfer . deliveries [ 0 ]. deliveryNo )
&& id ( transfer . deliveries [ 0 ]. customerId ) === customerId && Number ( transfer . deliveries [ 0 ]. packageCount ) === 2
&& transfer . deliveries [ 0 ]. status === 'DELIVERING' && transfer . outbounds ? . length === 1
&& transfer . outbounds [ 0 ]. status === 'OUTBOUNDED' && transfer . outbounds [ 0 ]. outboundNo === transfer . deliveries [ 0 ]. outboundNo
&& id ( transfer . outbounds [ 0 ]. customerId ) === customerId )
const departed = newTrip . loadingCommand . result
check ( 'Loading completion ships exactly two returned boxes' , current . status === 'ACTIVE' && current . stage === 'DELIVERING'
&& Number ( current . counts . pickedPackageCount ) === 2 && Number ( current . counts . shippedPackageCount ) === 2
&& departed . action === 'LOADING_COMPLETE' && departed . dispatchNo === arranged . dispatchNo
&& version ( current . recordVersion ) === version ( departed . recordVersion ))
2026-10-20 10:35:52 +08:00
const shipped = await inventory ( 'RETURN_DEPARTED' ); check ( 'Returned stock ships once and retains the original immutable stock-in ledger' , shipped . available === 0 && shipped . picked === 0 && JSON . stringify ( shipped . ledger ) === JSON . stringify ( stockIn . ledger ))
2026-10-03 16:11:52 +08:00
const newDelivery = await delivery ( transfer . deliveries [ 0 ]. deliveryNo )
2026-10-03 22:13:15 +08:00
proof . preSignatureGate = { status : 'WAITING' , deliveryNo : newDelivery . order . deliveryNo , newTripNo : newTrip . tripNo ,
departed , originalSignatureCount : proof . originalSignatures . length }
await context . checkpoint ()
check ( 'Readonly source event audit gate is required before redispatch signature' , typeof deps . onBeforeRedispatchSignature === 'function' )
const audit = await deps . onBeforeRedispatchSignature ( proof )
check ( 'Pre-signature audit proves A has zero completion events and B has one' , audit ? . sourceAEventCount === 0
&& audit . sourceBEventCount === 1 && /^[a-f0-9]{64}$/ . test ( audit . auditEvidenceSha256 || '' ))
Object . assign ( proof . preSignatureGate , { status : 'VERIFIED' , audit })
await context . checkpoint ()
2026-10-20 10:35:52 +08:00
const newSignature = await saveReceiptAndComplete ( io , newDelivery . order , customerId , 271 )
2026-10-03 23:09:24 +08:00
check ( 'Redispatched two boxes actually signed' , newSignature ? . order ? . signedAt && newSignature . order . signedPackages === 2 )
2026-10-03 16:11:52 +08:00
const afterA = await tail ( a . relation ), afterB = await tail ( b . relation )
check ( 'A completes only after redispatch; B remains complete' , afterA . order . subOrderStatus ? . includes ( 'DELIVERY:DELIVERED' ) && afterB . order . subOrderStatus === beforeB . order . subOrderStatus )
for ( const original of proof . originalSignatures ) {
const current = ( await delivery ( original . deliveryNo )). order
2026-10-03 23:09:24 +08:00
check ( 'Original signed facts stay immutable' , current . signedAt === original . signedAt && current . signedPackages === original . signedPackages && Number ( current . packageCount ) === Number ( original . packageCount ))
2026-10-03 16:11:52 +08:00
}
proof . afterRedispatch = { A : afterA , B : afterB , newSignature , departed }
proof . domesticAfterRedispatch = await domesticBoundary ( a , b , true )
2026-10-20 10:35:52 +08:00
proof . rootsAfterRedispatch = await roots ( a , b , false )
proof . closure = await finishFleetTrip ( io , module , newTrip )
2026-10-03 16:11:52 +08:00
check ( 'New trip cargo and resource lifecycle completes' , proof . closure ? . status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' )
proof . status = 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED' ; proof . sourceEventsVerified = false
await context . checkpoint (); return proof
} catch ( error ) { proof . status = 'FAILED' ; proof . failure = error . message ; await context . checkpoint (); throw error }
}
2026-10-03 22:13:15 +08:00
export async function waitForRedispatchSignatureAudit ( context , proof , { root , output , secrets = [],
wait = ms => new Promise ( resolve => setTimeout ( resolve , ms )), timeoutMs = 20 * 60 * 1000 } = {}) {
const gateNonce = randomUUID (), approvalPath = join ( output , 'approve-before-redispatch-signature.json' )
const checkpointPath = join ( output , 'before-redispatch-signature.json' )
const checkpoint = { gateNonce , marker : context . marker , runId : context . context . fleetRunId ,
originalTripNo : context . createdResources . fleetAcceptance . tripNo , newTripNo : proof . newTripNo ,
sourceAInboundId : proof . sources . A . tail . sourceInboundId , sourceBInboundId : proof . sources . B . tail . sourceInboundId ,
customerId : context . createdResources . fleetAcceptance . customerId , proof , createdResources : context . createdResources }
const descriptor = openSync ( checkpointPath , 'wx' )
try { writeFileSync ( descriptor , JSON . stringify ( redactEvidence ( checkpoint , secrets ), null , 2 ) + '\n' ); fsyncSync ( descriptor ) }
finally { closeSync ( descriptor ) }
const progressDescriptor = openSync ( join ( output , 'progress.json' ), 'w' )
try {
writeFileSync ( progressDescriptor , JSON . stringify ( redactEvidence ({ stage : 'RETURN_REDISPATCH_PRE_SIGNATURE' , marker : context . marker ,
steps : context . steps || [], modules : context . modules || [], createdResources : context . createdResources }, secrets ), null , 2 ) + '\n' )
fsyncSync ( progressDescriptor )
} finally { closeSync ( progressDescriptor ) }
console . log ( 'FLEET_SIGNATURE_AUDIT_WAITING=' + JSON . stringify ({ checkpointPath , approvalPath , gateNonce , marker : context . marker }))
const started = Date . now ()
while ( ! existsSync ( approvalPath )) {
if ( Date . now () - started >= timeoutMs ) throw Error ( 'Pre-signature audit gate timed out; do not sign or resume automatically' )
await wait ( 1000 )
}
const approval = JSON . parse ( readFileSync ( approvalPath , 'utf8' ))
if ( approval . gateNonce !== gateNonce || approval . marker !== checkpoint . marker || approval . newTripNo !== checkpoint . newTripNo
|| approval . sourceAEventCount !== 0 || approval . sourceBEventCount !== 1 || ! /^[a-f0-9]{64}$/ . test ( approval . auditEvidenceSha256 || '' )
|| ! /^reports\/[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_.-]+)?\.json$/ . test ( approval . auditEvidencePath || '' ))
throw Error ( 'Pre-signature approval does not match this exact gate and source counts' )
const evidencePath = resolve ( root , approval . auditEvidencePath )
if ( ! existsSync ( evidencePath ) || createHash ( 'sha256' ). update ( readFileSync ( evidencePath )). digest ( 'hex' ) !== approval . auditEvidenceSha256 )
throw Error ( 'Pre-signature audit evidence SHA256 mismatch' )
return approval
}