#!/usr/bin/env node import { randomUUID } from 'node:crypto' import { mkdir, writeFile } from 'node:fs/promises' import { dirname, resolve } from 'node:path' import { pathToFileURL } from 'node:url' const MAX_COUNT = 100 // Local test-environment settings. Keep PASSWORD empty in committed code. const CONFIG = { GATEWAY: 'http://8.163.30.128:9000', USERNAME: '17512095155', PASSWORD: 'd6KW2ZC4vv44WJ4a', COUNT: 10, } export function parseArguments(values) { const options = {} for (let index = 0; index < values.length; index += 1) { const value = values[index] if (!value.startsWith('--')) throw new Error(`Unknown argument: ${value}`) const [rawKey, inlineValue] = value.slice(2).split('=', 2) if (rawKey === 'help') { options.help = true continue } const next = inlineValue ?? values[index + 1] if (inlineValue === undefined) index += 1 if (next == null || next.startsWith('--')) throw new Error(`--${rawKey} requires a value`) options[rawKey] = next } return options } export function validateCount(value) { const count = Number(value ?? CONFIG.COUNT) if (!Number.isInteger(count) || count < 1 || count > MAX_COUNT) { throw new Error(`--count must be an integer between 1 and ${MAX_COUNT}`) } return count } function trimSlash(value) { return String(value || '').replace(/\/+$/, '') } function compactTimestamp(value = new Date()) { return value.toISOString().replace(/\D/g, '').slice(0, 14) } function addDays(value, days) { const date = new Date(value) date.setUTCDate(date.getUTCDate() + days) return date.toISOString().slice(0, 10) } function records(value) { return Array.isArray(value?.records) ? value.records : [] } function items(value) { return Array.isArray(value) ? value : [] } function requireSuccess(response, action) { if (response.http !== 200 || String(response.body?.code) !== '200') { throw new Error(`${action} failed: http=${response.http}, code=${response.body?.code ?? 'missing'}, message=${response.body?.message ?? 'missing'}, requestId=${response.requestId || 'missing'}, traceId=${response.traceId || 'missing'}`) } return response.body?.data } async function request(gateway, timeoutMs, method, path, { token, body, headers = {} } = {}) { const requestHeaders = { Accept: 'application/json', ...headers } if (token) requestHeaders.Authorization = `Bearer ${token}` let requestBody if (body instanceof FormData) { requestBody = body } else if (body !== undefined) { requestHeaders['Content-Type'] = 'application/json' requestBody = JSON.stringify(body) } const response = await fetch(`${gateway}${path}`, { method, headers: requestHeaders, body: requestBody, signal: AbortSignal.timeout(timeoutMs), }) const text = await response.text() let responseBody = null try { responseBody = text ? JSON.parse(text) : null } catch { responseBody = { code: 'INVALID_JSON', message: text.slice(0, 500) } } return { http: response.status, body: responseBody, requestId: response.headers.get('x-request-id') || responseBody?.requestId, traceId: response.headers.get('x-trace-id') || responseBody?.traceId, } } async function loadMasterData(gateway, timeoutMs, token) { const domesticResponse = await request(gateway, timeoutMs, 'GET', '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=DOMESTIC', { token }) const domestic = records(requireSuccess(domesticResponse, 'Query active domestic warehouses')) const destinationResponse = await request(gateway, timeoutMs, 'GET', '/api/merchant/v1/warehouses?page=1&size=200&status=ACTIVE&warehouseType=FBA', { token }) const destinations = records(requireSuccess(destinationResponse, 'Query active FBA warehouses')) if (domestic.length === 0 || destinations.length === 0) { throw new Error('No active domestic receiving warehouse or FBA destination warehouse is available') } for (const receiving of domestic) { const channelResponse = await request(gateway, timeoutMs, 'GET', `/api/merchant/v1/first-leg/pricing/channels?originWarehouseId=${encodeURIComponent(receiving.id)}`, { token }) const channels = items(requireSuccess(channelResponse, 'Query active pricing channels')) .filter((channel) => channel?.channelCode && items(channel.pricingTaxModes).length > 0) for (const channel of channels) { for (const pricingTaxMode of channel.pricingTaxModes) { for (const destination of destinations) { const estimateResponse = await request(gateway, timeoutMs, 'POST', '/api/merchant/v1/first-leg/pricing/estimate', { token, body: { originWarehouseId: receiving.id, channelCode: channel.channelCode, destinationCode: destination.warehouseCode, destinationType: 'FBA', destinationWarehouseId: destination.id, destinationName: destination.warehouseName, pricingTaxMode, actualWeightKg: 20.5, actualVolumeCbm: 0.024, }, }) if (estimateResponse.http === 200 && String(estimateResponse.body?.code) === '200') { return { receiving, destination, channel, pricingTaxMode } } } } } } throw new Error('No priced route is available for the fixed merchant account') } function testPng() { return Uint8Array.from(Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')) } async function uploadProductImage(gateway, timeoutMs, token, marker) { const form = new FormData() form.append('file', new Blob([testPng()], { type: 'image/png' }), `pending-order-${marker}.png`) const response = await request(gateway, timeoutMs, 'POST', '/api/merchant/v1/files/product-images', { token, body: form }) const data = requireSuccess(response, 'Upload product image') if (!data?.fileId) throw new Error('Product image upload did not return fileId') return data.fileId } export function buildOrderPayload({ marker, index, profile, master, productImageFileId }) { const sequence = String(index).padStart(3, '0') const shipmentId = `SHIP-${sequence}-${marker}`.slice(0, 64) const referenceId = `REF-${sequence}-${marker}`.slice(0, 64) const companyName = profile.tenantName || `固定客户-${marker}` return { customerId: profile.id, customerName: companyName, transportMode: 'SEA', tradeTerm: 'FOB', destinationCountryCode: master.destination.countryCode || 'US', destinationWarehouseId: master.destination.id, destinationWarehouseName: master.destination.warehouseName, expectedShipDate: addDays(new Date(), 30), customerRemark: `待审核订单 ${sequence} ${marker}`, pickupMethod: 'PICKUP', receivingWarehouseId: master.receiving.id, receivingWarehouseName: master.receiving.warehouseName, channelCode: master.channel.channelCode, channelName: master.channel.channelName, carrierId: null, carrierName: null, taxMode: 'BUY_DECLARATION', pricingTaxMode: master.pricingTaxMode, containerMode: 'LCL', customsInspection: false, destinationType: 'FBA', amazonWarehouseCode: master.destination.warehouseCode, shipmentId, referenceId, sender: { companyName, contactName: '订单测试联系人', phone: '13800138000', email: 'pending-order@example.com', countryCode: 'CN', province: '广东省', city: '深圳市', district: '南山区', addressLine: `${marker} 固定客户测试地址`, postalCode: '518000', }, receiver: null, cargoes: [{ skuNo: `SKU-${sequence}-${marker}`.slice(0, 64), cargoName: '待审核测试产品', cargoNameEn: 'Pending review test product', hsCode: '8518210000', customsHsCode: '8518210000', packageType: 'CARTON', lengthCm: 30, widthCm: 20, heightCm: 20, unitsPerPackage: 12, packageCount: 2, grossWeightKg: 20.5, volumeCbm: 0.024, totalQuantity: 24, netWeightKg: 18, unitDeclaredValue: 8.5, declaredValue: 204, currency: 'USD', material: '塑料 / Plastic', usage: '接口测试 / API testing', model: `MODEL-${sequence}`, brand: 'TEST-NO-LOGO', productLengthCm: 12, productWidthCm: 8, productHeightCm: 6, shipmentId, referenceId, upcNo: `${Date.now()}${sequence}`.slice(-12), productImageFileId, dangerousGoods: false, cargoRemark: `待审核订单货物 ${sequence} ${marker}`, }], } } function printHelp() { console.log(`Usage: node scripts/create-pending-review-orders.mjs --gateway --count <1-${MAX_COUNT}> Environment: CLIENT_TEST_GATEWAY Test environment Gateway URL MERCHANT_TEST_PASSWORD Password for the configured fixed account MERCHANT_TEST_USERNAME Optional username override PENDING_ORDER_REPORT Optional JSON report path The GATEWAY, USERNAME, PASSWORD, and COUNT fields can also be filled in CONFIG at the top of this file.`) } export async function main(argv = process.argv.slice(2)) { const args = parseArguments(argv) if (args.help) { printHelp() return 0 } const gateway = trimSlash(args.gateway ?? process.env.CLIENT_TEST_GATEWAY ?? CONFIG.GATEWAY) if (!gateway) throw new Error('Gateway is required via CONFIG.GATEWAY, --gateway, or CLIENT_TEST_GATEWAY') const count = validateCount(args.count) const username = args.username ?? process.env.MERCHANT_TEST_USERNAME ?? CONFIG.USERNAME const password = args.password ?? process.env.MERCHANT_TEST_PASSWORD ?? CONFIG.PASSWORD if (!username) throw new Error('Fixed merchant username is required in CONFIG.USERNAME') if (!password) throw new Error('Merchant password is required in CONFIG.PASSWORD or MERCHANT_TEST_PASSWORD') const timeoutMs = Number(args['timeout-ms'] ?? process.env.CLIENT_TEST_TIMEOUT_MS ?? 30_000) if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) throw new Error('--timeout-ms must be at least 1000') const marker = `PENDING-${compactTimestamp()}-${randomUUID().slice(0, 8)}` const outputPath = resolve(args.output ?? process.env.PENDING_ORDER_REPORT ?? `reports/pending-review-orders-${marker}.json`) const startedAt = new Date().toISOString() const orders = [] const loginResponse = await request(gateway, timeoutMs, 'POST', '/api/auth/login', { body: { username, password }, }) const login = requireSuccess(loginResponse, 'Merchant login') if (!login?.accessToken) throw new Error('Merchant login did not return accessToken') const token = login.accessToken const profileResponse = await request(gateway, timeoutMs, 'GET', '/api/merchant/v1/iam/tenant/profile', { token }) const profile = requireSuccess(profileResponse, 'Query merchant profile') if (!profile?.id || profile.status !== 'ACTIVE') { throw new Error(`Fixed merchant account is not ready: tenantId=${profile?.id ?? 'missing'}, status=${profile?.status ?? 'missing'}`) } const master = await loadMasterData(gateway, timeoutMs, token) const productImageFileId = await uploadProductImage(gateway, timeoutMs, token, marker) for (let index = 1; index <= count; index += 1) { const payload = buildOrderPayload({ marker, index, profile, master, productImageFileId }) const createResponse = await request(gateway, timeoutMs, 'POST', '/api/merchant/v1/orders/first-leg', { token, body: payload, headers: { 'Idempotency-Key': `${marker}-create-${index}`.slice(0, 128) }, }) const orderId = String(requireSuccess(createResponse, `Create order ${index}`) ?? '') if (!orderId) throw new Error(`Create order ${index} did not return orderId`) const submitResponse = await request(gateway, timeoutMs, 'POST', `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}/submit`, { token, headers: { 'Idempotency-Key': `${marker}-submit-${index}`.slice(0, 128) }, }) requireSuccess(submitResponse, `Submit order ${index}`) const detailResponse = await request(gateway, timeoutMs, 'GET', `/api/merchant/v1/orders/first-leg/${encodeURIComponent(orderId)}`, { token }) const detail = requireSuccess(detailResponse, `Verify order ${index}`) if (detail?.order?.status !== 'SUBMITTED') { throw new Error(`Order ${orderId} is not pending review: status=${detail?.order?.status ?? 'missing'}`) } const orderNo = String(detail.order.orderNo || '') if (!orderNo) throw new Error(`Order ${orderId} detail did not return orderNo`) const unifiedResponse = await request(gateway, timeoutMs, 'GET', `/api/merchant/v1/orders/${encodeURIComponent(orderId)}`, { token }) const unifiedOrder = requireSuccess(unifiedResponse, `Verify unified order ${index}`) if (unifiedOrder?.summary?.mainStatus !== 'PENDING_REVIEW') { throw new Error(`Order ${orderId} unified status is not pending review: mainStatus=${unifiedOrder?.summary?.mainStatus ?? 'missing'}`) } const result = { index, orderId, orderNo, clientStatus: detail.order.status, mainStatus: unifiedOrder.summary.mainStatus, } orders.push(result) console.log(`CREATED ${index}/${count} orderId=${result.orderId} orderNo=${result.orderNo} status=PENDING_REVIEW`) } const report = { reportName: '固定客户待审核订单批量生成', success: true, marker, username, tenantId: String(profile.id), requestedCount: count, createdCount: orders.length, startedAt, completedAt: new Date().toISOString(), productImageFileId: String(productImageFileId), orders, securityNote: '报告不保存密码或访问令牌。', } await mkdir(dirname(outputPath), { recursive: true }) await writeFile(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8') console.log(`REPORT=${outputPath}`) console.log(`RESULT=PASS created=${orders.length}`) return 0 } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { main().then((code) => { process.exitCode = code }).catch((error) => { console.error(error instanceof Error ? error.message : String(error)) process.exitCode = 1 }) }