提交
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user