2026-10-20 10:35:52 +08:00
import { readFileSync , existsSync } from 'node:fs'
import { resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { createHash } from 'node:crypto'
import { AcceptanceRun , parseArguments , validateExecutionOptions } from './admin-test-env-acceptance.mjs'
import { saveRetainedReport , validateRetainedScope , validateNativeRetainedDispatch ,
installRetainedJournal , retainedApi , runRetainedNativeDispatch } from './mixed-container-outbound-test-env.mjs'
2026-09-14 09:58:30 +08:00
2026-10-20 10:35:52 +08:00
const id = value => {
if ( typeof value === 'number' && ! Number . isSafeInteger ( value )) throw Error ( 'Relabel ID lost precision' )
return value == null ? '' : String ( value )
}
const requireFact = ( condition , message ) => { if ( ! condition ) throw Error ( message ) }
2026-09-14 09:58:30 +08:00
2026-10-20 10:35:52 +08:00
/** A dispatch number or legacy pre-arrange report cannot establish current source ownership. */
export function retainedRelabelSource ( source , customerId , dispatchNo ) {
const scenario = source ? . scenarios ? . find ( value => value . name === 'SPLIT-TWO-TARGETS' )
const native = scenario ? . nativeDispatch
requireFact ( source ? . environment === 'TEST' && source . marker && native ? . marker === source . marker
&& scenario . relabelNo && native . customerId === id ( customerId )
&& ( ! dispatchNo || native . dispatchNo === dispatchNo ), 'Relabel continuation requires this run native source report/customer/dispatch; old reports or a direct number cannot resume' )
const inboundCodes = [... new Set (( native . detail ? . orderDetails || []). map ( row => row . inboundCode ). filter ( Boolean ))]
const expectedPackages = Number ( native . detail ? . counts ? . packageCount )
requireFact ( inboundCodes . length > 0 && Number . isSafeInteger ( expectedPackages ) && expectedPackages > 0
&& native . createBody ? . cargo ? . every ( row => row . sourceType === 'RELABEL' && row . relabelNo === scenario . relabelNo ),
'Relabel native cargo does not belong to the original changed-carton sources' )
const newMarks = scenario . relabelCartonMarks , oldMarks = scenario . sourceCartonMarks
requireFact ( Array . isArray ( newMarks ) && newMarks . length === expectedPackages && newMarks . every ( mark => typeof mark === 'string' && mark . trim ())
&& new Set ( newMarks ). size === newMarks . length && Array . isArray ( oldMarks ) && oldMarks . length > 0
&& oldMarks . every ( mark => typeof mark === 'string' && mark . trim ()) && oldMarks . every ( mark => ! newMarks . includes ( mark )),
'Relabel source must retain original/new unique carton mark evidence; cannot infer marks from live final inventory' )
const scope = { marker : native . marker , customerId : id ( customerId ), warehouseId : id ( native . createBody . warehouseId ),
inboundCodes , expectedPackages , expectedPalletItems : scenario . partialRelabel ? 1 : 2 , relabelCartonMarks : newMarks , sourceCartonMarks : oldMarks }
validateNativeRetainedDispatch ( native , scope )
return { scenario , native , scope }
2026-09-14 09:58:30 +08:00
}
2026-10-20 10:35:52 +08:00
export async function mainRelabel ( env = process . env ) {
const sourcePath = env . OVERSEAS_ACCEPTANCE_SOURCE
requireFact ( sourcePath && env . OVERSEAS_TEST_CUSTOMER_ID , 'Explicit native source report and customer are required; dispatch number alone is insufficient' )
const sourceBytes = readFileSync ( resolve ( sourcePath )), source = JSON . parse ( sourceBytes )
const { scenario , native , scope } = retainedRelabelSource ( source , env . OVERSEAS_TEST_CUSTOMER_ID , env . OVERSEAS_DISPATCH_NO )
if ( env . OVERSEAS_RELABEL_NO ) requireFact ( env . OVERSEAS_RELABEL_NO === scenario . relabelNo , 'Explicit relabel number differs from original source' )
scope . sourceSha256 = createHash ( 'sha256' ). update ( sourceBytes ). digest ( 'hex' )
const output = resolve ( env . OVERSEAS_FULFILLMENT_OUTPUT || 'reports/overseas-relabel-fulfillment-acceptance.json' )
const result = existsSync ( output ) ? JSON . parse ( readFileSync ( output , 'utf8' )) : { schema : 'NATIVE_RETAINED_OUTBOUND_V1' , environment : 'TEST' ,
... scope , sourceReport : resolve ( sourcePath ), relabelNo : scenario . relabelNo , nativeDispatch : native , requests : [] }
validateRetainedScope ( result , scope )
const options = parseArguments ([]); options . modules = []; options . output = output ; validateExecutionOptions ( options )
requireFact ( options . environment === 'test' , 'Relabel native continuation only supports explicit TEST' )
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 )
try {
const platform = await api ( 'POST' , '/api/auth/login' , { loginId : options . username , password : options . password }, '' )
requireFact ( platform . accessToken , 'Unified platform accessToken is missing' )
context . token = platform . accessToken
requireFact ( env . PDA_TEST_ACCESS_TOKEN || options . pdaUsername && options . pdaPassword , 'Explicit current PDA token or complete credentials are required' )
const pdaToken = env . PDA_TEST_ACCESS_TOKEN || ( await api ( 'POST' , '/api/auth/login' , {
loginId : options . pdaUsername , password : options . pdaPassword }, '' )). accessToken
requireFact ( pdaToken , 'Unified PDA credentials or explicit current token are required' )
await runRetainedNativeDispatch ( context , result , scope , pdaToken )
result . visits = []
for ( const deliveryNo of result . deliveryNos ) {
const query = new URLSearchParams ({ page : '1' , size : '200' , customerId : scope . customerId , keyword : deliveryNo })
const visits = await api ( 'GET' , `/api/platform/v1/last-leg/customer-visits? ${ query } ` )
const visit = visits . records ? . find ( row => row . deliveryNo === deliveryNo )
requireFact ( visit ? . status === 'PENDING' && id ( visit . customerId ) === scope . customerId , 'Real signature did not create the original customer visit' )
result . visits . push ({ id : id ( visit . id ), deliveryNo , status : visit . status })
2026-09-17 17:37:29 +08:00
}
2026-10-20 10:35:52 +08:00
result . passed = true ; result . scope = 'OVERSEAS_LASTLEG_ONLY'
} catch ( error ) {
result . passed = false ; result . finalStatus = 'BLOCKED' ; result . failedStage = 'NATIVE_RELABEL_FULFILLMENT' ; process . exitCode = 1
} finally {
result . checks = context . steps
if ( context . steps . some ( step => step . passed === false )) { result . passed = false ; result . finalStatus = 'CHECK_FAILED' ; process . exitCode = 1 }
await context . checkpoint ()
console . log ( JSON . stringify ({ marker : result . marker , dispatchNo : result . dispatchNo , outboundNo : result . outboundNo ,
deliveryNos : result . deliveryNos , passed : result . passed === true , finalStatus : result . finalStatus , report : output }))
2026-09-17 17:37:29 +08:00
}
2026-10-20 10:35:52 +08:00
return result
2026-09-14 09:58:30 +08:00
}
2026-10-20 10:35:52 +08:00
if ( process . argv [ 1 ] && resolve ( process . argv [ 1 ]) === fileURLToPath ( import . meta . url )) await mainRelabel ()