#!/usr/bin/env node import { mkdir, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { spawnSync } from 'node:child_process' const domestic = process.env.ADMIN_TEST_GATEWAY || 'http://127.0.0.1:9000' const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300' const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID || '2093269089353961474' const transferNo = process.env.WAREHOUSE_TRANSFER_NO || 'WT-WH-20260919-673986' const relabelNo = process.env.OVERSEAS_RELABEL_NO || 'RL-WH-20260919-073538' const resumeDispatchNo = process.env.OVERSEAS_DISPATCH_NO || '' const output = resolve(process.env.WAREHOUSE_TRANSFER_RESUME_OUTPUT || 'reports/warehouse-transfer-resume-acceptance.json') const marker = `WT-RESUME-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}` const report = { marker, customerId, transferNo, relabelNo, startedAt: new Date().toISOString(), passed: false, steps: [] } function id(value) { return value == null ? '' : String(value) } function assert(value, message) { if (!value) throw new Error(message) } function step(name, data = {}) { report.steps.push({ name, at: new Date().toISOString(), ...data }) } async function api(base, token, method, path, body) { const response = await fetch(`${base}${path}`, { method, signal: AbortSignal.timeout(15000), headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), }, body: body === undefined ? undefined : JSON.stringify(body), }) const text = await response.text() let value try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } } if (!response.ok || String(value?.code) !== '200') { throw new Error(`${method} ${path}: HTTP=${response.status}, code=${value?.code}, message=${value?.message}`) } return value.data } async function rawApi(base, token, method, path) { const response = await fetch(`${base}${path}`, { method, headers: token ? { Authorization: `Bearer ${token}` } : {}, }) const text = await response.text() let value try { value = text ? JSON.parse(text) : null } catch { value = null } return { http: response.status, code: String(value?.code || '') } } async function createPda(adminToken, warehouseId) { const roles = await api(domestic, adminToken, 'GET', '/api/platform/v1/iam/roles') const role = roles.find((item) => item.roleCode === 'OVERSEAS_WAREHOUSE_PDA_OPERATOR' && item.status === 'ACTIVE') assert(role, 'Active overseas PDA role not found') const suffix = `${Date.now()}${process.pid}`.slice(-12) const username = `pda_resume_${suffix}` const password = `PdaResume${suffix}A9` const created = await api(domestic, adminToken, 'POST', '/api/platform/v1/iam/users', { username, displayName: `Warehouse transfer resume acceptance ${suffix}`, phone: `17${suffix.slice(-9)}`, email: `${username}@example.com`, tenantId: '0', roleIds: [id(role.id)], overseasWarehouseId: warehouseId, password, status: 'ACTIVE', }) const login = await api(domestic, '', 'POST', '/api/auth/login', { loginId: username, password }) assert(login.overseasAccessToken, 'Temporary PDA login did not return an overseas token') for (let attempt = 0; attempt < 60; attempt += 1) { const response = await rawApi(overseas, login.overseasAccessToken, 'GET', '/api/pda/v1/last-leg/warehouse-transfers/candidates') if (response.http === 200 && !['403', 'A0403'].includes(response.code)) { return { membershipId: id(created.membershipId), token: login.overseasAccessToken } } await new Promise((resolveWait) => setTimeout(resolveWait, 500)) } throw new Error('Temporary PDA security projection did not converge') } await mkdir(dirname(output), { recursive: true }) let admin let pda try { admin = await api(domestic, '', 'POST', '/api/auth/login', { loginId: process.env.ADMIN_TEST_USERNAME || 'platform_admin', password: process.env.ADMIN_TEST_PASSWORD || 'ChangeMe123!', }) assert(admin.accessToken && admin.overseasAccessToken, 'Platform login did not return both tokens') const ticket = await api(domestic, admin.accessToken, 'POST', `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} resume acceptance` }) const merchant = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code }) assert(merchant.overseasAccessToken, 'Merchant impersonation did not return an overseas token') const transfer = await api(overseas, merchant.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/warehouse-transfers/${transferNo}`) assert(transfer.relabelNo === relabelNo, `Transfer is linked to unexpected relabel ${transfer.relabelNo}`) assert(['READY_FOR_DISPATCH', 'OUTBOUNDED', 'COMPLETED'].includes(transfer.status), `Unexpected transfer status ${transfer.status}`) const warehouseId = id(transfer.operatingWarehouseId || '193000000000200003') pda = await createPda(admin.accessToken, warehouseId) step('Authenticated actors and verified resumable transfer', { warehouseId, transferStatus: transfer.status }) let dispatch if (resumeDispatchNo) { dispatch = { dispatchNo: resumeDispatchNo, status: 'RESUMED' } step('Resumed existing smart pre-dispatch', dispatch) } else { const candidates = await api(overseas, admin.overseasAccessToken, 'GET', `/api/platform/v1/last-leg/dispatches/candidates?orderNo=${encodeURIComponent(relabelNo)}&sourceStage=RECEIVED`) const rows = candidates.filter((value) => value.sourceStage === 'RELABEL' && String(value.orderRefs || '').split(',').includes(relabelNo)) assert(rows.length === 2, `Expected two relabel dispatch candidates, got ${rows.length}`) assert(rows.every((value) => Number(value.packageCount) > 0), 'A dispatch candidate has no packages') step('Verified smart-dispatch candidates', { pallets: rows.map((value) => ({ warehousePalletId: id(value.warehousePalletId), palletNo: value.palletNo, packageCount: Number(value.packageCount) })), }) dispatch = await api(overseas, admin.overseasAccessToken, 'POST', '/api/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: rows.map((value) => id(value.warehousePalletId)), route: marker, relabelNo, }) assert(dispatch.dispatchNo, 'Pre-dispatch did not return a dispatch number') step('Created smart pre-dispatch', { dispatchNo: dispatch.dispatchNo, status: dispatch.status }) } const sourceReport = resolve('reports', `${marker}-source.json`) const fulfillmentReport = resolve('reports', `${marker}-fulfillment.json`) await writeFile(sourceReport, JSON.stringify({ marker, passed: true, scenarios: [{ name: 'SPLIT-TWO-TARGETS', relabelNo, dispatchNo: dispatch.dispatchNo, }] }, null, 2), 'utf8') const child = spawnSync(process.execPath, [resolve('scripts', 'overseas-relabel-fulfillment-acceptance.mjs')], { cwd: resolve('.'), stdio: 'inherit', env: { ...process.env, ADMIN_TEST_GATEWAY: domestic, ADMIN_OVERSEAS_GATEWAY: overseas, ADMIN_OVERSEAS_ACCESS_TOKEN: admin.overseasAccessToken, MERCHANT_OVERSEAS_ACCESS_TOKEN: merchant.overseasAccessToken, PDA_OVERSEAS_ACCESS_TOKEN: pda.token, OVERSEAS_TEST_CUSTOMER_ID: customerId, OVERSEAS_ACCEPTANCE_SOURCE: sourceReport, OVERSEAS_FULFILLMENT_OUTPUT: fulfillmentReport, }, }) if (child.error) throw child.error if (child.status !== 0) throw new Error(`Fulfillment acceptance failed with exit code ${child.status}`) const finalTransfer = await api(overseas, merchant.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/warehouse-transfers/${transferNo}`) Object.assign(report, { passed: true, dispatchNo: dispatch.dispatchNo, transferStatus: finalTransfer.status, fulfillmentReport, finishedAt: new Date().toISOString(), }) } catch (error) { report.error = error instanceof Error ? error.message : String(error) report.finishedAt = new Date().toISOString() throw error } finally { if (admin?.accessToken && pda?.membershipId) { try { await api(domestic, admin.accessToken, 'PUT', `/api/platform/v1/iam/users/${pda.membershipId}/status`, { status: 'DISABLED', }) report.pdaCleanup = 'DISABLED' } catch (error) { report.pdaCleanup = `FAILED: ${error instanceof Error ? error.message : String(error)}` } } await writeFile(output, JSON.stringify(report, null, 2), 'utf8') } console.log(JSON.stringify(report, null, 2))