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' 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) } /** 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 } } 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 }) } 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 })) } return result } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await mainRelabel()