This commit is contained in:
2026-09-15 15:15:46 +08:00
parent 419950482f
commit cadc5a175e
28 changed files with 879 additions and 89 deletions
+1
View File
@@ -273,5 +273,6 @@ Dry-run 只校验两个统一 SQL 的入口标记,打印当前 SHA-256、行
## 海外换标定向验收补充
- `scripts/current-session-overseas-relabel-acceptance.mjs`:本地开发环境定向入口,从当前 Codex 内置浏览器本地存储读取最新平台/客户 refresh token,在内存中刷新双区域会话,自动选择当前客户具备足量库存的海外仓并临时创建海外 PDA 操作员;随后串行运行换标拆/合托、智能预排、PDA 出库与派送完成。报告写入 `reports/current-session-overseas-relabel/`,不记录 Token/密码;会消耗当前浏览器 refresh token 并保留业务验收单据,临时 PDA 在 `finally` 中停用。只允许本机开发环境且须有当前轮真实写入授权。
- `scripts/overseas-relabel-real-acceptance.mjs`:定向真实 HTTP 换标作业,会创建换标、库存及排车记录;从显式环境文件读取平台/PDA账号、网关及客户参数,输出脚本配置的报告,不自动清理。失败保留证据与已创建资源,重跑前核对报告,必须另获指定环境授权。
- `scripts/overseas-relabel-fulfillment-acceptance.mjs`:读取 `OVERSEAS_ACCEPTANCE_SOURCE` 或指定排车/换标编号,执行 PDA 拣货、出库、派送后续真实 HTTP,会推进业务状态;凭据来自显式环境文件,报告默认 `reports/overseas-relabel-fulfillment-acceptance.json`(`OVERSEAS_FULFILLMENT_OUTPUT` 可覆盖)。失败保留资源,重跑前核对业务状态,不自动清理。两个定向脚本不替代 Admin 全量入口,本次合并仅做离线合同。
@@ -327,6 +327,93 @@ export async function run(context) {
{ ...mergeBody, weight: 3 }, {}, picker.token)
add(context, '合箱同键异载荷返回冲突', mergeConflict, conflict(mergeConflict))
}
const relabelCandidates = await overseas(context, merchantToken, 'GET', '客户查询本轮退货换标候选',
`${RETURN_ORDERS}/${encodeURIComponent(returnNo)}/relabel-candidates`)
const relabelCandidate = (Array.isArray(relabelCandidates.data) ? relabelCandidates.data : [])
.find(row => nonBlank(row?.cartonMark) && row?.status === 'AVAILABLE')
context.assert(MODULE, '退货换标候选来自本轮已签收可用箱', relabelCandidates.response,
Boolean(relabelCandidate) && relabelCandidate.sourceReturnNo === returnNo,
`cartonMark=${relabelCandidate?.cartonMark || 'missing'}`, OPTIONS)
if (relabelCandidate) {
const relabelBody = {
cartonMarks: [relabelCandidate.cartonMark], idempotencyKey: key(context, 'RELABEL'),
contactName: '退货换标验收', contactPhone: '13800000008', deliveryMethod: 'TRUCK', remark: context.marker,
}
const relabel = await overseas(context, merchantToken, 'POST', '客户从退货箱创建换标单',
`${RETURN_ORDERS}/${encodeURIComponent(returnNo)}/relabel`, relabelBody)
const relabelNo = id(relabel.data?.relabelNo)
context.assert(MODULE, '退货换标单绑定原退货单与选中箱', relabel.response,
nonBlank(relabelNo) && relabel.data?.returnNo === returnNo
&& relabel.data?.status === 'PENDING_PROCESSING'
&& relabel.data?.cartonMarks?.includes?.(relabelCandidate.cartonMark),
`relabelNo=${relabelNo || 'missing'}`, OPTIONS)
if (relabelNo) {
const form = new FormData()
form.append('file', new Blob([new TextEncoder().encode(`%PDF-1.4\n${context.marker}\n%%EOF`)],
{ type: 'application/pdf' }), `${suffix}-return-relabel.pdf`)
const uploaded = await context.requestOverseasMultipart('POST',
`/api/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/files?scope=CARTON_TOTAL`,
form, {}, merchantToken)
add(context, '客户编辑退货换标总资料', uploaded, success(uploaded))
const relabelDetail = await overseas(context, merchantToken, 'GET', '客户回查退货换标资料修改',
`/api/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`)
context.assert(MODULE, '退货换标详情持久化总资料且业务状态未漂移', relabelDetail.response,
relabelDetail.data?.application?.relabelNo === relabelNo
&& relabelDetail.data?.application?.status === 'PENDING_PROCESSING'
&& nonBlank(relabelDetail.data?.application?.totalFileId), trace(relabelDetail.response), OPTIONS)
context.createdResources.phase4OverseasReturnReceipt.relabelNo = relabelNo
}
}
const outboundCandidates = await overseas(context, context.overseasToken, 'GET', '平台查询本轮退货出库候选',
query('/api/platform/v1/last-leg/outbound-orders/return/candidates', {
customerId, overseasWarehouseId: warehouse.id,
}))
const outboundCandidate = (Array.isArray(outboundCandidates.data) ? outboundCandidates.data : [])
.find(row => row?.returnNo === returnNo && nonBlank(row?.cartonMark)
&& row.cartonMark !== relabelCandidate?.cartonMark && row.status === 'AVAILABLE')
context.assert(MODULE, '退货出库候选排除已冻结换标箱', outboundCandidates.response,
Boolean(outboundCandidate), `cartonMark=${outboundCandidate?.cartonMark || 'missing'}`, OPTIONS)
if (outboundCandidate) {
const outboundBody = {
recipient: `${context.marker} RETURN OUTBOUND`, overseasWarehouseId: warehouse.id,
returnNo, cartonMarks: [outboundCandidate.cartonMark], remark: context.marker,
idempotencyKey: key(context, 'OUTBOUND'),
}
const outbound = await overseas(context, context.overseasToken, 'POST', '平台创建独立退货出库单',
query('/api/platform/v1/last-leg/outbound-orders/return', { customerId }), outboundBody)
const taskNo = id(outbound.data?.taskNo)
const outboundNo = id(outbound.data?.outboundNo)
context.assert(MODULE, 'RETURN_OUTBOUND创建PDA待拣任务', outbound.response,
nonBlank(taskNo) && nonBlank(outboundNo) && outbound.data?.status === 'PENDING',
`outboundNo=${outboundNo || 'missing'}, taskNo=${taskNo || 'missing'}`, OPTIONS)
if (taskNo) {
const task = await overseas(context, picker.token, 'GET', 'PDA读取退货出库任务明细',
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}`)
const taskItem = task.data?.items?.find?.(row => String(row?.cartonMarks || '').includes(outboundCandidate.cartonMark))
|| task.data?.items?.[0]
const pickBody = { outboundPalletItemId: taskItem?.outboundPalletItemId,
inventoryPositionId: null, packages: 1, cartonMarks: [outboundCandidate.cartonMark],
idempotencyKey: key(context, 'OUTBOUND-PICK') }
const picked = await overseas(context, picker.token, 'POST', 'PDA一次扫描退货箱完成拣货',
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}/pick`, pickBody)
context.assert(MODULE, '退货出库拣货推进为PICKED且任务进行中', picked.response,
picked.data?.outboundNo === outboundNo && picked.data?.status === 'IN_PROGRESS', trace(picked.response), OPTIONS)
const completed = await overseas(context, picker.token, 'POST', 'PDA完成退货出库',
`/api/pda/v1/last-leg/outbound-tasks/${encodeURIComponent(taskNo)}/complete`,
{ idempotencyKey: key(context, 'OUTBOUND-COMPLETE') })
const outboundDetail = await overseas(context, context.overseasToken, 'GET', '平台回查退货出库最终状态',
query(`/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}`, { customerId }))
context.assert(MODULE, 'RETURN_OUTBOUND真实完成且箱唛只出库一次', outboundDetail.response,
completed.data?.status === 'COMPLETED'
&& outboundDetail.data?.outbound?.outboundType === 'RETURN_OUTBOUND'
&& outboundDetail.data?.outbound?.status === 'OUTBOUNDED'
&& outboundDetail.data?.pallets?.some?.(row => row.cartonMarks === outboundCandidate.cartonMark
&& Number(row.pickedPackageCount) === 1), trace(outboundDetail.response), OPTIONS)
context.createdResources.phase4OverseasReturnReceipt.returnOutboundNo = outboundNo
}
}
} finally {
await disablePicker(context, picker, 91)
}
@@ -65,6 +65,9 @@ export async function verifyWarehouseTransferReview(context, token, customerId,
ok(result) && ok(detail) && detail.body.data?.status === expected
&& detail.body.data?.orderType === 'WAREHOUSE_TRANSFER'
&& detail.body.data?.orderStatus === expectedOrder
&& detail.body.data?.reviewDecision === `${decision}ED`.replace('APPROVEED', 'APPROVED')
&& detail.body.data?.reviewRemark === `${context.marker}-审核原因`
&& Boolean(detail.body.data?.reviewedBy) && Boolean(detail.body.data?.reviewedAt)
&& (detail.body.data?.pallets || []).length === 0, evidence(detail), OPTIONS)
context.createdResources.phase4OverseasClientServices[`transit${decision}`] = { transferNo: no, status: detail.body?.data?.status }
}
@@ -0,0 +1,173 @@
#!/usr/bin/env node
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { mkdir, writeFile } from 'node:fs/promises'
import { dirname, join, 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 store = process.env.CODEX_BROWSER_LOCAL_STORAGE || join(process.env.APPDATA || '', 'Codex', 'web', 'Codex', 'Default', 'Partitions', 'codex-browser-app', 'Local Storage', 'leveldb')
const outputDir = resolve(process.env.OVERSEAS_CURRENT_SESSION_OUTPUT_DIR || 'reports/current-session-overseas-relabel')
const adminLoginId = process.env.ADMIN_TEST_USERNAME || 'platform_admin'
const adminPassword = process.env.ADMIN_TEST_PASSWORD || 'ChangeMe123!'
const marker = `CURRENT-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
const summary = { marker, startedAt: new Date().toISOString(), scope: 'CURRENT_CODEX_BROWSER_SESSION', passed: false }
function assert(value, message) { if (!value) throw new Error(message) }
function id(value) { return value == null ? '' : String(value) }
function jwtId(token, ...claimNames) {
try {
const payload = Buffer.from(token.split('.')[1], 'base64url').toString('utf8')
for (const claimName of claimNames) {
const match = payload.match(new RegExp(`"${claimName}"\\s*:\\s*(?:"(\\d+)"|(\\d+))`))
if (match) return match[1] || match[2]
}
} catch {}
return ''
}
async function api(base, token, method, path, body) {
const response = await fetch(`${base}${path}`, { method,
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, body) {
const response = await fetch(`${base}${path}`, { method,
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 = null }
return { http: response.status, code: String(value?.code || ''), data: value?.data }
}
function storedValues(key) {
assert(existsSync(store), `Codex browser local storage not found: ${store}`)
const values = []
const files = readdirSync(store).filter((name) => /\.(?:log|ldb)$/.test(name))
.map((name) => ({ name, path: join(store, name) }))
.sort((left, right) => readFileSync(right.path).length - readFileSync(left.path).length)
for (const file of files) {
const source = readFileSync(file.path).toString('latin1')
let offset = 0
while ((offset = source.indexOf(key, offset)) >= 0) {
const match = source.slice(offset + key.length, offset + key.length + 256).match(/[A-Za-z0-9_-]{48,160}/)
if (match) values.push(match[0])
offset += key.length
}
}
return [...new Set(values)].reverse()
}
function storedJwt(key) {
for (const file of readdirSync(store).filter((name) => /\.(?:log|ldb)$/.test(name)).reverse()) {
const source = readFileSync(join(store, file)).toString('latin1')
let offset = source.lastIndexOf(key)
while (offset >= 0) {
const match = source.slice(offset + key.length, offset + key.length + 2000)
.match(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/)
if (match) return match[0]
offset = source.lastIndexOf(key, offset - 1)
}
}
return ''
}
async function refreshStoredSession(key, label) {
for (const refreshToken of storedValues(key)) {
try { return await api(domestic, '', 'POST', '/api/auth/refresh', { refreshToken }) } catch {}
}
throw new Error(`No valid ${label} refresh token was found in the current Codex browser session`)
}
async function disableStalePdas(adminToken) {
const users = await api(domestic, adminToken, 'GET', '/api/platform/v1/iam/users?page=1&size=100&keyword=pda_current_')
for (const user of users.records || []) {
if (String(user.username || '').startsWith('pda_current_') && user.status === 'ACTIVE') {
await api(domestic, adminToken, 'PUT', `/api/platform/v1/iam/users/${id(user.membershipId || user.id)}/status`, { status: 'DISABLED' })
}
}
}
async function createPda(adminToken, warehouseId, onCreated) {
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_WAREHOUSE_PDA_OPERATOR role not found')
const suffix = `${Date.now()}${process.pid}`.slice(-12)
const username = `pda_current_${suffix}`
const password = `PdaCurrent${suffix}A9`
const created = await api(domestic, adminToken, 'POST', '/api/platform/v1/iam/users', {
username, displayName: `当前会话海外PDA验收-${suffix}`, phone: `19${suffix.slice(-9)}`,
email: `${username}@example.com`, tenantId: '0', roleIds: [id(role.id)],
overseasWarehouseId: warehouseId, password, status: 'ACTIVE',
})
onCreated({ membershipId: id(created.membershipId), token: null })
const login = await api(domestic, '', 'POST', '/api/auth/login', { loginId: username, password })
assert(login.overseasAccessToken, 'Temporary PDA login did not return an overseas token')
onCreated({ membershipId: id(created.membershipId), token: login.overseasAccessToken })
for (let attempt = 0; attempt < 60; attempt += 1) {
const response = await rawApi(overseas, login.overseasAccessToken, 'GET', '/api/pda/v1/last-leg/receiving-containers/current')
if (response.http === 200 && response.code !== '403' && response.code !== 'A0403') {
return { membershipId: id(created.membershipId), token: login.overseasAccessToken }
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
throw new Error('Temporary PDA security projection did not converge')
}
await mkdir(outputDir, { recursive: true })
let admin
let pda
try {
try {
admin = await refreshStoredSession('cc-admin-refresh-token', 'platform')
} catch {
admin = await api(domestic, '', 'POST', '/api/auth/login', { loginId: adminLoginId, password: adminPassword })
}
const storedMerchantToken = storedJwt('cc-client-overseas-access-token')
const storedCustomerId = jwtId(storedMerchantToken, 'tenant_id', 'tenantId')
let merchant
try {
merchant = await refreshStoredSession('cc-client-refresh-token', 'merchant')
} catch {
assert(storedCustomerId, 'Current Codex merchant session does not contain a customer id')
const ticket = await api(domestic, admin.accessToken, 'POST',
`/api/platform/v1/iam/tenants/${storedCustomerId}/impersonation`, { reason: `${marker} 当前客户海外API验收` })
merchant = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
}
assert(admin.accessToken && admin.overseasAccessToken && merchant.accessToken && merchant.overseasAccessToken, 'Refresh response did not contain both regional tokens')
const customerId = jwtId(merchant.overseasAccessToken, 'tenant_id', 'tenantId')
assert(customerId, 'Current merchant customer id is missing from the overseas token')
const warehouses = await api(domestic, merchant.accessToken, 'GET', '/api/merchant/v1/warehouses?warehouseType=OVERSEAS&page=1&size=200')
let selected = null
for (const warehouse of warehouses.records || []) {
if (warehouse.status !== 'ACTIVE') continue
const candidates = await api(overseas, merchant.overseasAccessToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${encodeURIComponent(id(warehouse.id))}`)
if (candidates.some((item) => String(item.inboundOrderNo || '').startsWith('DS-') && Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode && item.positions?.some((position) => Number(position.availablePackages) >= 5))) {
selected = warehouse; break
}
}
assert(selected, 'Current merchant has no active overseas warehouse with a five-carton DS inventory source')
await disableStalePdas(admin.accessToken)
pda = await createPda(admin.accessToken, id(selected.id), (created) => { pda = created })
const relabelReport = join(outputDir, 'relabel.json')
const fulfillmentReport = join(outputDir, 'fulfillment.json')
const childEnv = { ...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_TEST_WAREHOUSE_ID: id(selected.id), OVERSEAS_ACCEPTANCE_OUTPUT: relabelReport,
OVERSEAS_ACCEPTANCE_SOURCE: relabelReport, OVERSEAS_FULFILLMENT_OUTPUT: fulfillmentReport }
for (const script of ['overseas-relabel-real-acceptance.mjs', 'overseas-relabel-fulfillment-acceptance.mjs']) {
const result = spawnSync(process.execPath, [resolve('scripts', script)], { cwd: resolve('.'), env: childEnv, stdio: 'inherit' })
if (result.error) throw result.error
if (result.status !== 0) throw new Error(`${script} failed with exit code ${result.status}`)
}
Object.assign(summary, { customerId, warehouseId: id(selected.id), warehouseCode: selected.warehouseCode,
relabelReport, fulfillmentReport, passed: true, finishedAt: new Date().toISOString() })
} finally {
if (admin?.accessToken && pda?.membershipId) {
try { await api(domestic, admin.accessToken, 'PUT', `/api/platform/v1/iam/users/${pda.membershipId}/status`, { status: 'DISABLED' }); summary.pdaCleanup = 'DISABLED' }
catch (error) { summary.pdaCleanup = `FAILED: ${error.message}` }
}
await writeFile(join(outputDir, 'summary.json'), JSON.stringify(summary, null, 2), 'utf8')
}
console.log(JSON.stringify(summary, null, 2))
@@ -7,9 +7,13 @@ const directDispatchNo = process.env.OVERSEAS_DISPATCH_NO
const directRelabelNo = process.env.OVERSEAS_RELABEL_NO
const output = process.env.OVERSEAS_FULFILLMENT_OUTPUT || 'reports/overseas-relabel-fulfillment-acceptance.json'
const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID
const directPlatformToken = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN
const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN
const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN
const marker = `FUL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
const required = ['ADMIN_TEST_USERNAME', 'ADMIN_TEST_PASSWORD', 'PDA_TEST_USERNAME', 'PDA_TEST_PASSWORD']
for (const name of required) if (!process.env[name]) throw new Error(`Missing ${name}`)
if (!directPlatformToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing platform credentials or token')
if (!directMerchantToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing merchant token or platform credentials')
if (!directPdaToken && (!process.env.PDA_TEST_USERNAME || !process.env.PDA_TEST_PASSWORD)) throw new Error('Missing PDA credentials or token')
if ((!sourceReport && !directDispatchNo) || !customerId) throw new Error('Missing acceptance source/dispatch or customer id')
function id(value) { return value == null ? '' : String(value) }
@@ -33,16 +37,16 @@ const scenario = source?.scenarios.find((value) => value.name === 'SPLIT-TWO-TAR
|| { dispatchNo: directDispatchNo, relabelNo: directRelabelNo || '' }
assert(scenario?.dispatchNo, 'Dispatch not found in source report')
const dispatchNo = scenario.dispatchNo
const platformLogin = await api(domestic, '', 'POST', '/api/auth/login', {
const platformLogin = directPlatformToken ? null : await api(domestic, '', 'POST', '/api/auth/login', {
loginId: process.env.ADMIN_TEST_USERNAME, password: process.env.ADMIN_TEST_PASSWORD })
const pdaLogin = await api(domestic, '', 'POST', '/api/auth/login', {
const pdaLogin = directPdaToken ? null : await api(domestic, '', 'POST', '/api/auth/login', {
loginId: process.env.PDA_TEST_USERNAME, password: process.env.PDA_TEST_PASSWORD })
const ticket = await api(domestic, platformLogin.accessToken, 'POST',
const ticket = directMerchantToken ? null : await api(domestic, platformLogin.accessToken, 'POST',
`/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} 海外尾程履约验收` })
const merchantLogin = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
const platformToken = platformLogin.overseasAccessToken
const merchantToken = merchantLogin.overseasAccessToken
const pdaToken = pdaLogin.overseasAccessToken
const merchantLogin = directMerchantToken ? null : await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
const platformToken = directPlatformToken || platformLogin.overseasAccessToken
const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken
const pdaToken = directPdaToken || pdaLogin.overseasAccessToken
await api(overseas, platformToken, 'PUT', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, {
vehiclePlate: `UAT-${marker.slice(-8)}`, driverName: '尾程验收司机', driverPhone: '19900000000',
+14 -8
View File
@@ -8,12 +8,18 @@ const pdaUser = process.env.PDA_TEST_USERNAME
const pdaPassword = process.env.PDA_TEST_PASSWORD
const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID
const warehouseId = process.env.OVERSEAS_TEST_WAREHOUSE_ID
const directPlatformToken = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN
const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN
const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN
const output = process.env.OVERSEAS_ACCEPTANCE_OUTPUT || 'reports/overseas-relabel-real-acceptance.json'
const marker = `TAIL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}`
for (const [name, value] of Object.entries({ platformUser, platformPassword, pdaUser, pdaPassword, customerId, warehouseId })) {
for (const [name, value] of Object.entries({ customerId, warehouseId })) {
if (!value) throw new Error(`Missing environment variable: ${name}`)
}
if (!directPlatformToken && (!platformUser || !platformPassword)) throw new Error('Missing platform credentials or token')
if (!directMerchantToken && (!platformUser || !platformPassword)) throw new Error('Missing merchant token or platform credentials')
if (!directPdaToken && (!pdaUser || !pdaPassword)) throw new Error('Missing PDA credentials or token')
const report = { marker, startedAt: new Date().toISOString(), scope: 'OVERSEAS_LASTLEG_ONLY', scenarios: [], passed: false }
function key(value) { return `${marker}-${value}`.slice(0, 200) }
@@ -42,14 +48,14 @@ async function poll(action, predicate, attempts = 40) {
}
function assert(condition, message) { if (!condition) throw new Error(message) }
const platformLogin = await api(domestic, '', 'POST', '/api/auth/login', { loginId: platformUser, password: platformPassword })
const pdaLogin = await api(domestic, '', 'POST', '/api/auth/login', { loginId: pdaUser, password: pdaPassword })
const ticket = await api(domestic, platformLogin.accessToken, 'POST',
const platformLogin = directPlatformToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: platformUser, password: platformPassword })
const pdaLogin = directPdaToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: pdaUser, password: pdaPassword })
const ticket = directMerchantToken ? null : await api(domestic, platformLogin.accessToken, 'POST',
`/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} 海外尾程真实验收` })
const merchantLogin = await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
const platformToken = platformLogin.overseasAccessToken
const merchantToken = merchantLogin.overseasAccessToken
const pdaToken = pdaLogin.overseasAccessToken
const merchantLogin = directMerchantToken ? null : await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code })
const platformToken = directPlatformToken || platformLogin.overseasAccessToken
const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken
const pdaToken = directPdaToken || pdaLogin.overseasAccessToken
assert(platformToken && merchantToken && pdaToken, 'Missing overseas token')
const candidates = await api(overseas, merchantToken, 'GET',