2026-10-20 10:35:52 +08:00
import { readFileSync , writeFileSync , existsSync , mkdirSync , renameSync } from 'node:fs'
import { resolve , dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
2026-09-15 15:13:15 +08:00
import { AcceptanceRun , parseArguments , validateExecutionOptions } from './admin-test-env-acceptance.mjs'
import { createPicker , disablePicker , resolvePickerAccess } from './admin-test-env-modules/phase4-overseas-pda.mjs'
2026-10-20 10:35:52 +08:00
import { pickAll , saveReceiptAndComplete , arrangeDispatchDelivery , verifyDepartureReadiness ,
dispatchInventorySources , inventorySnapshot , dispatchStockConservation , inventorySourcesPresent ,
relabelOutboundMarksMatch } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
import { prepareFleetTrip , confirmFleetLoading , finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
import { f40CreateDispatchBody } from './admin-test-env-modules/phase5-root-fulfillment.mjs'
2026-09-15 15:13:15 +08:00
2026-10-20 10:35:52 +08:00
const LASTLEG = '/api/platform/v1/last-leg'
const DISPATCH = ` ${ LASTLEG } /dispatches`
const PDA = '/api/pda/v1/last-leg/dispatches'
const MODULE = 'native-retained-outbound'
const id = value => {
if ( typeof value === 'number' && ! Number . isSafeInteger ( value )) throw Error ( 'Retained ID lost precision' )
return value == null ? '' : String ( value )
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
const validVersion = value => ( typeof value === 'string' || typeof value === 'number' && Number . isSafeInteger ( value )) && /^(0|[1-9]\d*)$/ . test ( String ( value ))
const success = response => response ? . http === 200 && String ( response ? . body ? . code ) === '200'
const same = ( a , b ) => JSON . stringify ( a ) === JSON . stringify ( b )
const requireFact = ( condition , message ) => { if ( ! condition ) throw Error ( message ) }
const sorted = values => [... new Set ( values )]. sort ()
const redact = value => {
if ( Array . isArray ( value )) return value . map ( redact )
if ( value && typeof value === 'object' ) return Object . fromEntries ( Object . entries ( value )
. filter (([ name ]) => ! /password|token|authorization|base64|^bytes$|^content$/i . test ( name )). map (([ name , nested ]) => [ name , redact ( nested )]))
return value
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
export function saveRetainedReport ( output , result ) {
mkdirSync ( dirname ( output ), { recursive : true })
writeFileSync ( ` ${ output } .tmp` , ` ${ JSON . stringify ( redact ({ ... result , updatedAt : new Date (). toISOString () } ), null, 2)}\n` )
renameSync ( ` ${ output } .tmp` , output )
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
export function validateRetainedScope ( result , scope ) {
requireFact ( result . schema === 'NATIVE_RETAINED_OUTBOUND_V1' && result . environment === 'TEST'
&& result . marker === scope . marker && result . sourceSha256 === scope . sourceSha256
&& result . customerId === id ( scope . customerId ) && result . warehouseId === id ( scope . warehouseId )
&& same ( result . inboundCodes , scope . inboundCodes ), 'Retained native marker/source/customer/warehouse scope mismatch; legacy reports cannot resume' )
requireFact ( ! result . requests ? . some ( row => [ 'SENDING' , 'UNKNOWN' ]. includes ( row . status )),
'Unknown write outcome retained: reconcile the original command before further writes' )
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
/** Every business write is saved before sending. Unknown results never generate another key. */
export function installRetainedJournal ( context , result , save ) {
const request = context . request . bind ( context ), multipart = context . requestMultipart ? . bind ( context )
context . createdResources = result . resources ||= {}
context . createdResources . phase5OverseasFulfillment = result . fulfillment ||= {}
context . context ||= {}
context . context . phase5InventorySources = result . inventorySources || {}
context . checkpoint = async () => { result . inventorySources = context . context . phase5InventorySources ; await save () }
result . requests ||= []
let stopped = false
const execute = async ( method , path , body , send , file ) => {
if ( method === 'GET' || path . startsWith ( '/api/auth/' )) return send ()
const cleanup = method === 'PUT' && /^\/api\/platform\/v1\/iam\/users\/[0-9]+\/status$/ . test ( path ) && body ? . status === 'DISABLED'
if ( stopped && ! cleanup ) throw Error ( 'Unknown write outcome: retained business writes stopped' )
const entry = { method , path , ...( file ? { file } : { body : structuredClone ( body ) }), status : 'SENDING' }
result . requests . push ( entry ); await context . checkpoint ()
let response
try { response = await send () }
catch ( error ) { stopped = true ; entry . status = 'UNKNOWN' ; await context . checkpoint (); throw error }
const missingNativeResult = success ( response ) && ( path . startsWith ( DISPATCH ) || path . startsWith ( ` ${ LASTLEG } /fleet/` ))
&& ( response . body ? . data == null || typeof response . body . data !== 'object' )
const known = response ? . http > 0 && response . http < 500 && response . body ? . code != null && ! missingNativeResult
Object . assign ( entry , { status : known ? 'RESPONDED' : 'UNKNOWN' , http : response ? . http ,
code : response ? . body ? . code , requestId : response ? . requestId , traceId : response ? . traceId , result : response ? . body ? . data })
if ( entry . status === 'UNKNOWN' ) stopped = true
await context . checkpoint ()
if ( stopped && ! cleanup ) throw Error ( 'Unknown write outcome retained; no automatic retry or replacement command' )
return response
}
context . request = ( method , path , body , token , headers ) => execute ( method , path , body , () => request ( method , path , body , token , headers ))
context . requestMultipart = ( method , path , file , token , headers ) => {
const bytes = Buffer . from ( file . content )
return execute ( method , path , undefined , () => multipart ( method , path , file , token , headers ),
{ name : file . name , type : file . type , size : bytes . length , sha256 : createHash ( 'sha256' ). update ( bytes ). digest ( 'hex' ) })
}
return context
}
export async function retainedApi ( context , method , path , body , token ) {
const response = await context . request ( method , path , body , token )
const data = context . apiSuccess ( MODULE , ` ${ method } ${ path } ` , response , '' , { deploymentBlocker : true })
if ( ! success ( response )) throw Error ( `Retained request failed: ${ method } ${ path } ; HTTP= ${ response ? . http } ; code= ${ response ? . body ? . code } ` )
return data
}
function sourceKey ( selection ) {
if ( selection . sourceType === 'INBOUND_PLAN' ) return `PLAN: ${ id ( selection . planPalletId ) } `
if ( selection . sourceType === 'RELABEL' ) return `RELABEL: ${ selection . relabelNo } : ${ id ( selection . warehousePalletId ) } `
throw Error ( 'Unsupported retained source selector' )
}
export function validateNativeRetainedDispatch ( native , scope ) {
requireFact ( native ? . marker === scope . marker && native ? . dispatchNo && native . createBody ? . idempotencyKey
&& id ( native . createBody . warehouseId ) === id ( scope . warehouseId ) && native . createBody . cargo ? . length
&& native . createResult ? . dispatchNo === native . dispatchNo && native . createResult ? . action === 'CREATE'
&& native . createResult ? . status === 'ACTIVE' && native . createResult ? . stage === 'BEFORE_LOADING'
&& validVersion ( native . createResult . recordVersion ) && native . detail ? . dispatchNo === native . dispatchNo
&& native . detail . status === 'ACTIVE' && validVersion ( native . detail . recordVersion )
&& Array . isArray ( native . detail . orderDetails ) && native . detail . orderDetails . length ,
'Retained dispatch requires native Create command/result/Detail and the original marker; dispatch number alone is insufficient' )
const keys = native . createBody . cargo . map ( sourceKey )
requireFact ( new Set ( keys ). size === keys . length && same ( sorted ( keys ), sorted ( native . detail . orderDetails . map ( row => row . sourceKey )))
&& native . detail . orderDetails . every ( row => id ( row . customerId ) === id ( scope . customerId )), 'Retained native source/customer identity mismatch' )
requireFact ( native . createBody . cargo . every ( row => /^ [ a - f \ d ]{ 64 } $ / i . test ( row . sourceVersion || '' )
&& /^[1-9]\d*$/ . test ( id ( row . warehousePalletId ))), 'Retained selectors require original immutable source versions and physical pallet IDs' )
return keys
}
async function ledger ( context , scope , dispatchNo ) {
const rows = []; let total
for ( let page = 1 ; page <= 50 ; page ++ ) {
const params = new URLSearchParams ({ customerId : id ( scope . customerId ), businessType : 'SHIP' , businessOrderNo : dispatchNo ,
outboundOrderNo : dispatchNo , page : String ( page ), size : '200' })
const data = await retainedApi ( context , 'GET' , ` ${ LASTLEG } /inventory/ledger? ${ params } ` )
requireFact ( Array . isArray ( data . records ) && Number . isSafeInteger ( Number ( data . total )), 'Incomplete retained SHIP ledger' )
total ??= Number ( data . total ); requireFact ( Number ( data . total ) === total , 'Ledger snapshot changed during pagination' )
rows . push (... data . records )
if ( rows . length >= total ) break
requireFact ( data . records . length > 0 , 'Truncated retained SHIP ledger' )
}
requireFact ( rows . length === total && new Set ( rows . map ( row => id ( row . id ))). size === rows . length
&& rows . every ( row => id ( row . customerId ) === id ( scope . customerId ) && row . businessType === 'SHIP'
&& row . businessOrderNo === dispatchNo && row . outboundOrderNo === dispatchNo ), 'Foreign or duplicated retained shipment ledger' )
return rows . map ( row => ({ ... row , id : id ( row . id ), customerId : id ( row . customerId ), overseasWarehouseId : id ( row . overseasWarehouseId ),
beforePackages : Number ( row . beforePackages ), deltaPackages : Number ( row . deltaPackages ), afterPackages : Number ( row . afterPackages ) })). sort (( a , b ) => a . id . localeCompare ( b . id ))
}
const NATIVE_DEPS = { prepareFleetTrip , pickAll , confirmFleetLoading , arrangeDispatchDelivery , verifyDepartureReadiness ,
saveReceiptAndComplete , finishFleetTrip , inventorySnapshot , dispatchInventorySources , dispatchStockConservation , ledger }
/** Continue only a proved native dispatch. All mutations remain real public API commands. */
export async function runRetainedNativeDispatch ( context , result , scope , pdaToken , overrides = {}) {
const deps = { ... NATIVE_DEPS , ... overrides }
const api = ( method , path , body , token ) => retainedApi ( context , method , path , body , token )
validateRetainedScope ( result , scope )
context . marker = scope . marker
let native = result . nativeDispatch
requireFact ( native , 'No original native dispatch proof; create it from current authoritative candidates first' )
const expectedSources = validateNativeRetainedDispatch ( native , scope ), dispatchNo = native . dispatchNo , path = ` ${ DISPATCH } / ${ encodeURIComponent ( dispatchNo ) } `
result . dispatchNo = dispatchNo
let detail = await api ( 'GET' , path )
const command = await api ( 'GET' , ` ${ path } /commands/ ${ encodeURIComponent ( native . createBody . idempotencyKey ) } ` )
requireFact ( command . dispatchNo === dispatchNo && command . action === 'CREATE'
&& String ( command . recordVersion ) === String ( native . createResult . recordVersion )
&& same ( command . result , native . createResult . result ), 'Original CREATE command is absent or differs from retained production receipt' )
const items = await api ( 'GET' , ` ${ path } /items` )
requireFact ( detail . status === 'ACTIVE' && validVersion ( detail . recordVersion )
&& same ( sorted ( items . items ? . map ( row => row . sourceKey ) || []), sorted ( expectedSources ))
&& items . items . every ( row => id ( row . customerId ) === id ( scope . customerId ) && scope . inboundCodes . includes ( row . inboundCode ))
&& Number ( detail . counts ? . packageCount ) === scope . expectedPackages , 'Live native cargo/customer/quantity drift' )
const task = await api ( 'GET' , ` ${ PDA } / ${ encodeURIComponent ( dispatchNo ) } ` , undefined , pdaToken )
requireFact ( task . dispatchNo === dispatchNo && id ( task . warehouseId ) === id ( scope . warehouseId )
&& Array . isArray ( task . items ) && task . items . length > 0 && ( ! scope . expectedPalletItems || task . items . length === scope . expectedPalletItems )
&& task . items . every ( row => id ( row . customerId ) === id ( scope . customerId ))
&& task . items . reduce (( sum , row ) => sum + Number ( row . scheduledPackages ), 0 ) === scope . expectedPackages , 'PDA warehouse/customer/item quantity differs from the original physical sources' )
const bol = await context . requestBinary ( 'GET' , ` ${ path } /prints/bol` )
requireFact ( bol . http === 200 && /application\/pdf/i . test ( bol . contentType || '' ) && Buffer . from ( bol . bytes || []). subarray ( 0 , 5 ). toString () === '%PDF-' , 'BOL is not a real PDF' )
result . bol = { sha256 : createHash ( 'sha256' ). update ( bol . bytes ). digest ( 'hex' ), size : bol . bytes . length }
let trip = result . tripProof || ( context . createdResources . fleetTrips || []). find ( row => row . dispatchNo === dispatchNo )
if ( detail . stage === 'BEFORE_LOADING' ) {
requireFact ( ! trip , 'Partly prepared resources already exist; retain the original commands instead of creating replacements' )
trip = await deps . prepareFleetTrip ( context , MODULE , dispatchNo , id ( scope . warehouseId )); result . tripProof = trip
detail = await api ( 'GET' , path )
}
requireFact ( trip ? . preparation === 'VERIFIED' && trip . dispatchNo === dispatchNo && id ( trip . warehouseId ) === id ( scope . warehouseId ), 'Native resource/review proof is missing or incomplete' )
result . tripProof = trip
if ( detail . stage === 'LOADING' ) {
if ( ! result . inventory ) {
requireFact ( Number ( task . counts ? . pickedPackageCount ) === 0 , 'Cannot reconstruct a pre-scan inventory baseline after physical scanning' )
const sources = deps . dispatchInventorySources ( task , dispatchNo , id ( scope . warehouseId ))
requireFact ( sources , 'Missing real PDA source positions/balances' )
result . inventory = { ... sources ,
stockBefore : await deps . inventorySnapshot ( context , id ( scope . customerId ), id ( scope . warehouseId ), scope . inboundCodes , 'retained-before-pick' , sources . sourceBalanceIds ),
ledgerBefore : await deps . ledger ( context , scope , dispatchNo ) }
requireFact ( result . inventory . stockBefore && inventorySourcesPresent ( result . inventory . stockBefore , sources . sourcePositionBalances ), 'Incomplete original pre-scan source inventory' )
await context . checkpoint ()
}
requireFact ( await deps . pickAll ( context , pdaToken , dispatchNo ), 'PDA real picks failed' )
result . inventory . stockAfter = await deps . inventorySnapshot ( context , id ( scope . customerId ), id ( scope . warehouseId ), scope . inboundCodes , 'retained-after-pick' , result . inventory . sourceBalanceIds )
result . inventory . ledgerAfter = await deps . ledger ( context , scope , dispatchNo )
const conservation = deps . dispatchStockConservation ( context , { ... result . inventory , customerId : id ( scope . customerId ), warehouseId : id ( scope . warehouseId ),
dispatchNo , expectedPackages : scope . expectedPackages })
result . inventory . conservation = conservation . proof
requireFact ( conservation . passed , 'Physical inventory/SHIP conservation failed' )
await context . checkpoint ()
try { await deps . confirmFleetLoading ( context , MODULE , trip , pdaToken , result . loadingMeasurements || native . loadingMeasurements ) }
catch ( error ) {
const field = [ 'palletSpaces' , 'weightKg' , 'volumeCbm' , 'deliveryIndex' ]. find ( value => String ( error ? . message ). includes ( value ))
result . blocker = { stage : 'LOADING_COMPLETE' , code : field ? 'ACTUAL_MEASUREMENT_REQUIRED' : 'LOADING_COMPLETE_NOT_VERIFIED' , field : field || null }
await context . checkpoint (); throw error
}
detail = await api ( 'GET' , path )
}
requireFact ( result . inventory ? . conservation , 'No original scan quantity/ledger proof for this retained native dispatch' )
const recovered = action => result . requests ? . find ( row => row . status === 'RESPONDED' && String ( row . code ) === '200' && row . result ? . dispatchNo === dispatchNo && row . result ? . action === action ) ? . result
if ( ! result . transfer ) {
const saved = recovered ( 'ARRANGE_DELIVERY' )
if ( saved ) result . transfer = { data : saved , deliveries : saved . result . deliveries , outbounds : saved . result . outbounds }
else {
requireFact ( detail . stage === 'AFTER_LOADING' , 'Missing original arrangement command; do not borrow existing historical delivery rows' )
result . transfer = await deps . arrangeDispatchDelivery ( context , dispatchNo )
requireFact ( result . transfer , 'Native arrangement failed' )
}
await context . checkpoint ()
}
const deliveries = result . transfer . deliveries , outbounds = result . transfer . outbounds
requireFact ( Array . isArray ( deliveries ) && deliveries . length > 0 && Array . isArray ( outbounds ) && outbounds . length === 1
&& id ( outbounds [ 0 ]. customerId ) === id ( scope . customerId ) && deliveries . every ( row => id ( row . customerId ) === id ( scope . customerId )
&& row . outboundNo === outbounds [ 0 ]. outboundNo ) && deliveries . reduce (( sum , row ) => sum + Number ( row . packageCount ), 0 ) === scope . expectedPackages ,
'Native arrangement does not conserve the retained customer and physical quantity' )
result . outboundNo = outbounds [ 0 ]. outboundNo ; result . deliveryNos = deliveries . map ( row => row . deliveryNo )
if ( ! result . departure ) result . departure = recovered ( 'DEPART' )
if ( detail . stage === 'AFTER_LOADING' ) {
const ready = await deps . verifyDepartureReadiness ( context , pdaToken , dispatchNo )
requireFact ( ready && validVersion ( ready . data . recordVersion ), 'Native server DEPART gates unavailable' )
result . departBody ||= { recordVersion : ready . data . recordVersion , idempotencyKey : ` ${ scope . marker } -DEPART` . slice ( 0 , 128 ), remark : ` ${ scope . marker } -TEST` }
await context . checkpoint ()
result . departure = await api ( 'POST' , ` ${ PDA } / ${ encodeURIComponent ( dispatchNo ) } /depart` , result . departBody , pdaToken )
const replay = await api ( 'POST' , ` ${ PDA } / ${ encodeURIComponent ( dispatchNo ) } /depart` , result . departBody , pdaToken )
requireFact ( replay . replayed === true && String ( replay . recordVersion ) === String ( result . departure . recordVersion )
&& same ( replay . result , result . departure . result ), 'Departure original command replay drift' )
result . departureReplay = replay
detail = await api ( 'GET' , path )
}
requireFact ( result . departure ? . stage === 'DELIVERING' && result . departure ? . status === 'ACTIVE'
&& Number ( result . departure . result ? . shippedPackages ) === scope . expectedPackages
&& same ( sorted ( result . departure . result ? . deliveries ? . map ( row => ` ${ id ( row . customerId ) } : ${ row . outboundNo } : ${ row . deliveryNo } ` ) || []),
sorted ( deliveries . map ( row => ` ${ id ( row . customerId ) } : ${ row . outboundNo } : ${ row . deliveryNo } ` )))
&& [ 'DELIVERING' , 'DELIVERED' ]. includes ( detail . stage ), 'Original native departure receipt missing or invalid' )
result . inventory . stockAfterDepart = await deps . inventorySnapshot ( context , id ( scope . customerId ), id ( scope . warehouseId ), scope . inboundCodes , 'retained-after-depart' , result . inventory . sourceBalanceIds )
result . inventory . ledgerAfterDepart = await deps . ledger ( context , scope , dispatchNo )
requireFact ( same ( result . inventory . stockAfter , result . inventory . stockAfterDepart )
&& same ( result . inventory . ledgerAfter , result . inventory . ledgerAfterDepart ), 'Departure repeated SHIP or changed inventory' )
result . outbound = await api ( 'GET' , ` ${ LASTLEG } /outbound-orders/ ${ encodeURIComponent ( result . outboundNo ) } ` )
requireFact ( result . outbound . outbound ? . status === 'OUTBOUNDED' && id ( result . outbound . outbound . customerId ) === id ( scope . customerId )
&& result . outbound . outbound . outboundNo === result . outboundNo && result . outbound . outbound . dispatchNo === dispatchNo
&& Number ( result . outbound . outbound . packageCount ) === scope . expectedPackages , 'Actual retained outbound is not complete' )
if ( scope . relabelCartonMarks ) requireFact ( relabelOutboundMarksMatch ( result . outbound , scope ), 'New relabel carton marks were lost, duplicated or replaced with old marks' )
result . deliveries ||= []
for ( const [ index , delivery ] of deliveries . entries ()) {
const deliveryPath = ` ${ LASTLEG } /delivery-orders/ ${ encodeURIComponent ( delivery . deliveryNo ) } ?customerId= ${ encodeURIComponent ( scope . customerId ) } `
const current = await api ( 'GET' , deliveryPath )
requireFact ( id ( current . order ? . customerId ) === id ( scope . customerId ) && current . order . outboundNo === result . outboundNo , 'Delivery customer/outbound identity drift' )
if ( current . order . status !== 'DELIVERED' ) {
requireFact ( current . order . status === 'DELIVERING' , 'Actual native departure must advance delivery before POD' )
requireFact ( await deps . saveReceiptAndComplete ( context , delivery , id ( scope . customerId ), index ), 'POD did not automatically complete delivery' )
} else {
const original = result . requests . find ( row => row . method === 'PUT' && row . path === ` ${ LASTLEG } /delivery-orders/ ${ encodeURIComponent ( delivery . deliveryNo ) } /documents?customerId= ${ encodeURIComponent ( scope . customerId ) } ` && row . status === 'RESPONDED' && row . result ? . categories )
requireFact ( original , 'Already signed delivery has no original retained POD save command' )
const replay = await api ( 'PUT' , original . path , original . body )
requireFact ( replay . replayed === true && String ( replay . version ) === String ( original . result . version ), 'POD original-command replay drift' )
}
const signed = await api ( 'GET' , deliveryPath )
requireFact ( signed . order ? . status === 'DELIVERED' && signed . order . signedAt , 'Real signedAt/POD completion missing' )
result . deliveries [ index ] = signed ; await context . checkpoint ()
}
for ( let attempt = 0 ; attempt < 30 ; attempt ++ ) {
detail = await api ( 'GET' , path )
if ( detail . stage === 'DELIVERED' ) break
await new Promise ( resolve => setTimeout ( resolve , 1000 ))
}
requireFact ( detail . status === 'ACTIVE' && detail . stage === 'DELIVERED' , 'All normal signatures did not aggregate native DELIVERED' )
result . nativeCompleted = detail
if ( ! trip . closure ) result . tripClosure = await deps . finishFleetTrip ( context , MODULE , trip )
else requireFact ( trip . closure . status === 'VERIFIED' , 'Retained resource closure is incomplete; preserve original release commands' )
result . finalStatus = 'PASSED' ; await context . checkpoint (); return result
}
export async function mainMixed ( args = process . argv . slice ( 2 )) {
const [ sourcePath , containerNo , label , outputPath ] = args
requireFact ( sourcePath && containerNo && [ 'MIXED-FBA' , 'MIXED-DROPSHIP' ]. includes ( label ) && outputPath ,
'Usage: source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report; explicit TEST authorization required' )
const options = parseArguments ([]); options . modules = []; validateExecutionOptions ( options )
requireFact ( options . environment === 'test' , 'Retained mixed outbound only supports explicit TEST' )
const sourceBytes = readFileSync ( resolve ( sourcePath )), source = JSON . parse ( sourceBytes )
const record = source . createdResources ? . dropshipPalletMatrix ? . records ? . find ( row => row . label === label )
requireFact ( source . environment === 'test' && source . marker && record ? . overseas ? . plan ? . containerNo === containerNo
&& record . overseas ? . receiving ? . receivingVerified , 'Explicit current TEST receipt source is required' )
const inboundCode = record . source . inboundCode , customerId = id ( record . overseas . plan . customerId ), warehouseId = id ( record . overseas . warehouseId )
const output = resolve ( outputPath ), scope = { marker : ` ${ source . marker } - ${ label } ` , customerId , warehouseId , inboundCodes : [ inboundCode ], expectedPackages : 4 ,
sourceSha256 : createHash ( 'sha256' ). update ( sourceBytes ). digest ( 'hex' ) }
const result = existsSync ( output ) ? JSON . parse ( readFileSync ( output , 'utf8' )) : { schema : 'NATIVE_RETAINED_OUTBOUND_V1' , environment : 'TEST' ,
... scope , containerNo , inboundCode , label , source : resolve ( sourcePath ), requests : [] }
validateRetainedScope ( result , scope )
const context = installRetainedJournal ( new AcceptanceRun ( options ), result , () => saveRetainedReport ( output , result ))
context . marker = scope . marker
const api = ( method , path , body , token ) => retainedApi ( context , method , path , body , token )
let picker
try {
const login = await context . request ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password }, '' )
requireFact ( success ( login ) && login . body ? . data ? . accessToken , 'Unified platform login failed' )
context . token = login . body . data . accessToken
if ( label === 'MIXED-DROPSHIP' ) {
const candidatePath = ` ${ LASTLEG } /outbound-orders/one-click-dropship/candidates?customerId= ${ customerId } &warehouseId= ${ record . overseas . warehouseId } `
2026-09-15 15:13:15 +08:00
if ( ! result . task ) {
const candidates = await api ( 'GET' , candidatePath )
const selected = candidates . filter ( row => row . inboundCode === inboundCode )
const inputs = selected . map ( row => {
const pallet = record . task . pallets . find ( p => p . palletNo === row . palletNo )
const cartonMarks = ( pallet ? . cartons || []). filter ( c => String ( c . inboundId ) === String ( record . source . inboundId )). map ( c => c . boxCode )
if ( cartonMarks . length !== Number ( row . availablePackages ) || row . customerId !== customerId ) throw new Error ( 'Dropship pallet scope mismatch' )
return { inventoryPositionId : row . inventoryPositionId , packages : cartonMarks . length , sku : row . skuCode , cartonMarks }
})
if ( inputs . length !== 2 || inputs . reduce (( n , i ) => n + i . packages , 0 ) !== 4 ) throw new Error ( 'Expected exactly two pallets and four cartons' )
result . selected = selected
result . inputs = inputs
result . createBody = { recipient : ` ${ context . marker } TEST ONLY` , items : inputs , remark : ` ${ context . marker } -test-only` , idempotencyKey : ` ${ context . marker } -OUTBOUND` }
2026-10-20 10:35:52 +08:00
await context . checkpoint ()
result . task = await api ( 'POST' , ` ${ LASTLEG } /outbound-orders/one-click-dropship?customerId= ${ customerId } ` , result . createBody )
await context . checkpoint ()
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
const before = await api ( 'GET' , ` ${ LASTLEG } /outbound-orders/ ${ result . task . outboundNo } ` )
2026-09-15 15:13:15 +08:00
if ( before . outbound . status !== 'OUTBOUNDED' ) {
const access = await resolvePickerAccess ( context )
picker = access ? await createPicker ( context , 9 , record . overseas . warehouseId , access ) : null
if ( ! picker ? . token ) throw new Error ( 'PDA operator unavailable' )
result . pickerMembershipId = picker . membershipId
2026-10-20 10:35:52 +08:00
await context . checkpoint ()
2026-09-15 15:13:15 +08:00
const path = `/api/pda/v1/last-leg/outbound-tasks/ ${ result . task . taskNo } `
const detail = await api ( 'GET' , path , undefined , picker . token )
for ( const [ index , candidate ] of result . selected . entries ()) {
const item = detail . items . find ( i => i . palletNo === candidate . palletNo && i . inboundCode === inboundCode && i . skuCode === candidate . skuCode )
if ( ! item || ! item . positions . some ( p => String ( p . inventoryPositionId ) === String ( candidate . inventoryPositionId ))) throw new Error ( 'PDA source position mismatch' )
const body = { outboundPalletItemId : item . outboundPalletItemId , inventoryPositionId : candidate . inventoryPositionId ,
packages : result . inputs [ index ]. packages , cartonMarks : result . inputs [ index ]. cartonMarks , idempotencyKey : ` ${ context . marker } -PICK- ${ index } ` }
await api ( 'POST' , ` ${ path } /pick` , body , picker . token )
await api ( 'POST' , ` ${ path } /pick` , body , picker . token )
}
result . completedTask = await api ( 'POST' , ` ${ path } /complete` , { idempotencyKey : ` ${ context . marker } -COMPLETE` }, picker . token )
}
2026-10-20 10:35:52 +08:00
result . outbound = await api ( 'GET' , ` ${ LASTLEG } /outbound-orders/ ${ result . task . outboundNo } ` )
2026-09-15 15:13:15 +08:00
result . remainingCandidates = ( await api ( 'GET' , candidatePath )). filter ( row => row . inboundCode === inboundCode )
result . finalStatus = result . outbound . outbound . status === 'OUTBOUNDED' && result . remainingCandidates . length === 0
? 'OUTBOUND_PASSED_DELIVERY_NOT_SUPPORTED' : 'OUTBOUND_FAILED'
2026-10-20 10:35:52 +08:00
} else {
if ( ! result . nativeDispatch ) {
const savedCreate = result . requests . find ( row => row . method === 'POST' && row . path === DISPATCH && row . status === 'RESPONDED'
&& String ( row . code ) === '200' && row . result ? . action === 'CREATE' )
if ( savedCreate ) {
requireFact ( result . createBody && same ( savedCreate . body , result . createBody ), 'Original native CREATE body must remain exact' )
const detail = await api ( 'GET' , ` ${ DISPATCH } / ${ encodeURIComponent ( savedCreate . result . dispatchNo ) } ` )
result . nativeDispatch = { marker : scope . marker , dispatchNo : savedCreate . result . dispatchNo , createBody : savedCreate . body , createResult : savedCreate . result , detail ,
loadingMeasurements : record . loadingMeasurements }
await context . checkpoint ()
}
}
if ( ! result . nativeDispatch ) {
const page = await api ( 'GET' , ` ${ DISPATCH } /candidates?warehouseId= ${ warehouseId } ` )
const selected = page ? . candidates ? . filter ( row => row . inboundCode === inboundCode && row . containerNo === containerNo
&& id ( row . customerId ) === customerId && id ( row . warehouseId ) === warehouseId )
requireFact ( page ? . complete === true && selected ? . length && selected . reduce (( sum , row ) => sum + Number ( row . availablePackages ), 0 ) === 4
&& selected . every ( row => row . destination ? . warehouseCode === 'ABE4' ), 'Live native FBA source/destination/quantity changed' )
result . createBody ||= f40CreateDispatchBody ( scope . marker , warehouseId , customerId , selected )
await context . checkpoint ()
const created = await api ( 'POST' , DISPATCH , result . createBody )
const detail = await api ( 'GET' , ` ${ DISPATCH } / ${ encodeURIComponent ( created . dispatchNo ) } ` )
result . nativeDispatch = { marker : scope . marker , dispatchNo : created . dispatchNo , createBody : result . createBody , createResult : created , detail ,
loadingMeasurements : record . loadingMeasurements }
await context . checkpoint ()
}
const access = await resolvePickerAccess ( context )
picker = access ? await createPicker ( context , 8 , warehouseId , access ) : null
requireFact ( picker ? . token , 'PDA operator unavailable' )
result . pickerMembershipId = picker . membershipId ; await context . checkpoint ()
await runRetainedNativeDispatch ( context , result , scope , picker . token )
for ( let attempt = 0 ; attempt < 30 ; attempt ++ ) {
const data = await api ( 'GET' , `/api/platform/inbounds?tenantId= ${ customerId } &inboundCode= ${ encodeURIComponent ( inboundCode ) } &page=1&size=20` )
result . domesticInbound = data . records ? . find ( row => row . inboundCode === inboundCode )
if ( result . domesticInbound ? . processStatus === 'DELIVERED' ) break
await new Promise ( resolve => setTimeout ( resolve , 1000 ))
}
requireFact ( result . domesticInbound ? . orderId , 'Missing original domestic source order' )
result . domesticOrder = ( await api ( 'GET' , `/api/platform/v1/orders/ ${ result . domesticInbound . orderId } ?tenantId= ${ customerId } ` )) ? . summary
requireFact ( result . domesticInbound . processStatus === 'DELIVERED' && result . domesticOrder ? . mainStatus === 'COMPLETED' , 'Domestic source completion callback did not converge' )
2026-09-15 15:13:15 +08:00
}
2026-09-15 15:25:05 +08:00
} catch ( error ) {
2026-10-20 10:35:52 +08:00
result . finalStatus = 'BLOCKED' ; result . failedStage = result . nativeDispatch ? . dispatchNo ? 'NATIVE_FULFILLMENT' : 'SOURCE_PREPARATION'
process . exitCode = 1
} finally {
try { await disablePicker ( context , picker , label === 'MIXED-DROPSHIP' ? 9 : 8 ) }
catch { result . finalStatus = 'CLEANUP_FAILED' }
finally { if ( picker ) picker . token = null }
result . checks = context . steps
if ( context . steps . some ( step => step . passed === false )) result . finalStatus = 'CHECK_FAILED'
if ( result . finalStatus !== 'PASSED' ) process . exitCode = 1
await context . checkpoint ()
console . log ( JSON . stringify ({ containerNo , inboundCode , finalStatus : result . finalStatus ,
dispatchNo : result . dispatchNo , outboundNo : result . outboundNo || result . task ? . outboundNo , deliveryNos : result . deliveryNos , report : output }))
2026-09-15 15:25:05 +08:00
}
2026-10-20 10:35:52 +08:00
return result
2026-09-15 15:13:15 +08:00
}
2026-10-20 10:35:52 +08:00
if ( process . argv [ 1 ] && resolve ( process . argv [ 1 ]) === fileURLToPath ( import . meta . url )) await mainMixed ()