#!/usr/bin/env node import { mkdir, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import process from 'node:process' const REQUIRED_EVIDENCE = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'] const value = (name, fallback = '') => { const prefix = `--${name}=` const inline = process.argv.find((item) => item.startsWith(prefix)) if (inline) return inline.slice(prefix.length) const index = process.argv.indexOf(`--${name}`) return index >= 0 ? process.argv[index + 1] || fallback : fallback } const gateway = value('gateway', process.env.ADMIN_TEST_GATEWAY || 'http://8.163.30.128:9000').replace(/\/$/, '') const username = value('username', process.env.ADMIN_TEST_USERNAME || 'platform_admin') const password = value('password', process.env.ADMIN_TEST_PASSWORD) const taskIds = value('task-ids').split(',').map((item) => item.trim()).filter(Boolean) const seedEvidence = process.argv.includes('--seed-evidence') const stamp = new Date().toISOString().replace(/\D/g, '').slice(0, 14) const marker = `PDA-FINAL-DIAG-${stamp}` const output = resolve(value('output', `reports/pda-final-complete-diagnostic-${stamp}.json`)) if (!password || taskIds.length === 0) { throw new Error('required: ADMIN_TEST_PASSWORD (or --password) and --task-ids') } let token = '' const calls = [] async function call(method, path, body, headers = {}) { let response try { response = await fetch(`${gateway}${path}`, { method, headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers, }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000), }) const raw = await response.text() let payload try { payload = raw ? JSON.parse(raw) : null } catch { payload = { raw: raw.slice(0, 1000) } } const result = { method, path, ...(body === undefined ? {} : { body }), http: response.status, code: payload?.code == null ? null : String(payload.code), message: payload?.message || null, requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id'), data: payload?.data ?? null, } calls.push(result) return result } catch (error) { const result = { method, path, ...(body === undefined ? {} : { body }), http: 0, code: null, message: String(error), requestId: null, traceId: null, data: null } calls.push(result) return result } } async function uploadEvidence(tenantId, inboundId, evidenceType) { const path = `/api/platform/v1/files/loading-evidence/${encodeURIComponent(tenantId)}/${encodeURIComponent(inboundId)}?evidenceType=${encodeURIComponent(evidenceType)}` const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130]) const fileName = `${marker}-${inboundId}-${evidenceType.toLowerCase()}.png` const form = new FormData() form.append('file', new Blob([png], { type: 'image/png' }), fileName) try { const response = await fetch(`${gateway}${path}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, body: form, signal: AbortSignal.timeout(60_000), }) const raw = await response.text() let payload try { payload = raw ? JSON.parse(raw) : null } catch { payload = { raw: raw.slice(0, 1000) } } const result = { method: 'POST', path, body: { fileName, evidenceType }, http: response.status, code: payload?.code == null ? null : String(payload.code), message: payload?.message || null, requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id'), data: payload?.data ?? null } calls.push(result) return result } catch (error) { const result = { method: 'POST', path, body: { fileName, evidenceType }, http: 0, code: null, message: String(error), requestId: null, traceId: null, data: null } calls.push(result) return result } } const ok = (response) => response?.http === 200 && response?.code === '200' const id = (input) => input == null ? '' : String(input) const login = await call('POST', '/api/auth/login', { loginId: username, password }) login.body = { loginId: username, password: '[REDACTED]' } if (!ok(login) || !login.data?.accessToken) { login.data = null const failedReport = { marker, startedAt: new Date().toISOString(), gateway, taskIds, stopped: true, fatal: `login failed: HTTP=${login.http}, code=${login.code}, message=${login.message || 'missing'}`, results: [], calls } await mkdir(dirname(output), { recursive: true }) await writeFile(output, `${JSON.stringify(failedReport, null, 2)}\n`, 'utf8') console.log(JSON.stringify({ output, marker, stopped: true, fatal: failedReport.fatal }, null, 2)) process.exit(1) } token = login.data.accessToken login.data = { authenticated: true } const pendingResponse = await call('GET', '/api/platform/v1/first-leg/pda-loading/tasks/pending') const pendingRows = ok(pendingResponse) && Array.isArray(pendingResponse.data) ? pendingResponse.data : [] const results = [] let stopped = false for (const taskId of taskIds) { if (stopped) break const pending = pendingRows.find((item) => id(item?.taskId) === taskId) || null const warehouseId = id(pending?.warehouseId) const taskPath = `/api/platform/v1/first-leg/pda-loading/tasks/${encodeURIComponent(taskId)}` const detailResponse = warehouseId ? await call('GET', `${taskPath}?warehouseId=${encodeURIComponent(warehouseId)}`) : { http: 0, code: null, message: 'task missing from pending list; warehouseId unavailable', data: null } const detail = detailResponse.data const planId = id(detail?.loadingPlanId || pending?.loadingPlanId) const planPath = `/api/platform/v1/first-leg/smart-loading/plans/${encodeURIComponent(planId)}` const planBefore = planId ? await call('GET', planPath) : null const owners = [...new Map((detail?.items || []).map((item) => [ `${id(item?.tenantId)}:${id(item?.inboundId)}`, { tenantId: id(item?.tenantId), inboundId: id(item?.inboundId) }, ])).values()].filter((item) => item.tenantId && item.inboundId) const inboundBefore = [] const evidenceBefore = [] for (const owner of owners) { inboundBefore.push(await call('GET', `/api/platform/inbounds/${encodeURIComponent(owner.inboundId)}?tenantId=${encodeURIComponent(owner.tenantId)}`)) evidenceBefore.push(await call('GET', `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}`)) } const evidenceSeed = [] if (seedEvidence) { for (let ownerIndex = 0; ownerIndex < owners.length; ownerIndex += 1) { const owner = owners[ownerIndex] const activeTypes = new Set((evidenceBefore[ownerIndex]?.data?.evidences || []) .filter((item) => item?.status !== 'VOID').map((item) => item?.evidenceType)) for (const evidenceType of REQUIRED_EVIDENCE.filter((type) => !activeTypes.has(type))) { const upload = await uploadEvidence(owner.tenantId, owner.inboundId, evidenceType) const fileId = id(upload.data?.fileId) if (!ok(upload) || !fileId) { evidenceSeed.push({ evidenceType, upload, bind: null }) break } const bindPath = `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}` const bindBody = { expectedVersion: 0, evidenceType, fileId, fileName: 'client-must-not-override.png', sealNo: evidenceType === 'SEAL' ? `SEAL-${stamp}` : null, remark: marker } const bind = await call('POST', bindPath, bindBody, { 'Idempotency-Key': `${marker}-${owner.inboundId}-${evidenceType}`.slice(0, 128) }) evidenceSeed.push({ evidenceType, upload, bind }) if (!ok(bind)) break } } } const evidenceCurrent = [] for (const owner of owners) { evidenceCurrent.push(await call('GET', `/api/platform/v1/inbounds/${encodeURIComponent(owner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(owner.tenantId)}`)) } const unresolved = (detail?.items || []).filter((item) => ['PENDING', 'PICKING'].includes(item?.itemStatus)) const evidenceReady = evidenceCurrent.length > 0 && evidenceCurrent.every((response) => { const activeTypes = new Set((response.data?.evidences || []) .filter((item) => item?.status !== 'VOID').map((item) => item?.evidenceType)) return ok(response) && REQUIRED_EVIDENCE.every((type) => activeTypes.has(type)) }) const ready = ok(pendingResponse) && ok(detailResponse) && pending && pending.workflowStatus === 'AWAITING_REVIEW' && detail?.workflowStatus === 'AWAITING_REVIEW' && Number(pending?.pendingItemCount) === 0 && unresolved.length === 0 && Number.isInteger(Number(detail?.version)) && evidenceReady const item = { taskId, pending, detail: detail ? { id: detail.id, version: detail.version, workflowStatus: detail.workflowStatus, taskStatus: detail.taskStatus, inboundId: detail.inboundId, loadingPlanId: detail.loadingPlanId, containerBookingId: detail.containerBookingId, items: (detail.items || []).map((row) => ({ id: row.id, tenantId: row.tenantId, inboundId: row.inboundId, itemStatus: row.itemStatus, plannedPackageCount: row.plannedPackageCount, pickedPackageCount: row.pickedPackageCount, version: row.version })), } : null, owners, evidenceReady, evidenceSeed: evidenceSeed.map((entry) => ({ evidenceType: entry.evidenceType, upload: entry.upload ? { http: entry.upload.http, code: entry.upload.code, requestId: entry.upload.requestId, traceId: entry.upload.traceId, fileId: entry.upload.data?.fileId || null } : null, bind: entry.bind ? { http: entry.bind.http, code: entry.bind.code, requestId: entry.bind.requestId, traceId: entry.bind.traceId } : null })), ready, complete: null, } if (!ready) { item.blocker = `pending/detail/evidence not ready; pendingWorkflow=${pending?.workflowStatus || 'missing'}, detailWorkflow=${detail?.workflowStatus || 'missing'}, pendingItemCount=${pending?.pendingItemCount ?? 'missing'}, unresolved=${unresolved.length}, evidenceReady=${evidenceReady}` results.push(item) stopped = true continue } const body = { expectedTaskVersion: Number(detail.version), operationId: `${marker}-${taskId}`.slice(0, 128), remark: marker, } const complete = await call('POST', `${taskPath}/complete`, body, { 'Idempotency-Key': `${marker}-${taskId}`.slice(0, 128) }) item.complete = complete item.taskAfter = await call('GET', `${taskPath}?warehouseId=${encodeURIComponent(warehouseId)}`) item.planAfter = await call('GET', planPath) item.inboundAfter = [] for (const owner of owners) { item.inboundAfter.push(await call('GET', `/api/platform/inbounds/${encodeURIComponent(owner.inboundId)}?tenantId=${encodeURIComponent(owner.tenantId)}`)) } results.push(item) if (!ok(complete)) stopped = true } const report = { marker, startedAt: new Date().toISOString(), gateway, taskIds, seedEvidence, stopped, results, calls: calls.map((entry) => entry.path === '/api/auth/login' ? { ...entry, data: { authenticated: true } } : entry) } await mkdir(dirname(output), { recursive: true }) await writeFile(output, `${JSON.stringify(report, null, 2)}\n`, 'utf8') console.log(JSON.stringify({ output, marker, stopped, results: results.map((item) => ({ taskId: item.taskId, ready: item.ready, blocker: item.blocker || null, complete: item.complete ? { http: item.complete.http, code: item.complete.code, message: item.complete.message, requestId: item.complete.requestId, traceId: item.complete.traceId } : null, })) }, null, 2))