import { writeFile } from 'node:fs/promises' 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 platformUser = process.env.ADMIN_TEST_USERNAME const platformPassword = process.env.ADMIN_TEST_PASSWORD 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}` const chargeAmount = Number(process.env.OVERSEAS_RELABEL_CHARGE_AMOUNT ?? '0.01') if (!Number.isFinite(chargeAmount) || chargeAmount < 0) throw new Error('Invalid charge amount') 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', chargeAmount, photoEvidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK', scenarios: [], passed: false } function key(value) { return `${marker}-${value}`.slice(0, 200) } function id(value) { return value == null ? '' : String(value) } async function api(base, token, method, path, body, headers = {}) { const response = await fetch(`${base}${path}`, { method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), ...headers }, body: body === undefined ? undefined : body instanceof FormData ? body : 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 poll(action, predicate, attempts = 40) { for (let index = 0; index < attempts; index += 1) { const value = await action() if (predicate(value)) return value await new Promise((resolve) => setTimeout(resolve, 500)) } throw new Error('Polling timed out') } function assert(condition, message) { if (!condition) throw new Error(message) } 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 = 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', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${warehouseId}`) const source = candidates.find((item) => String(item.inboundOrderNo || '').startsWith('DS-') && Number(item.availablePackages) >= 5 && item.destinationFbaWarehouseCode && item.positions?.some((position) => Number(position.availablePackages) >= 5)) assert(source, 'No overseas inventory source with at least five available cartons') const sourcePosition = source.positions.find((position) => Number(position.availablePackages) >= 5) async function resolvePallet(palletCode) { return api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode }) } async function createAndConfirm(name, cartonCount, operationType = 'CARTON_AND_SKU', prepare = null) { const suffix = `${name}-${Date.now()}` const items = Array.from({ length: cartonCount }, (_, index) => ({ sourceInventoryBalanceId: id(source.inventoryBalanceId), sourceInventoryPositionId: id(sourcePosition.positionId), sourceInboundOrderId: id(source.inboundOrderId), skuCode: source.skuCode, productNameZh: source.productNameZh || source.skuCode, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: `SHIP-${suffix}-${index + 1}`, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), labelRequirement: marker, cartonMark: `CM-${suffix}-${index + 1}`, fbaCartonMark: operationType === 'CARTON_AND_SKU' ? `FBA-${suffix}-${index + 1}` : null, destinationFbaWarehouseCode: source.destinationFbaWarehouseCode, operationType, skuItems: [{ skuCode: source.skuCode, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), quantity: 1, remark: marker }], })) const created = await api(overseas, platformToken, 'POST', `/api/platform/v1/last-leg/relabel-applications?customerId=${customerId}`, { customerOrderNo: `TAIL-${suffix}`.slice(0, 128), operationWarehouseId: warehouseId, labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), contactName: '尾程验收', contactPhone: '13800000006', ...(chargeAmount > 0 ? { chargeAmount } : {}), remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`), deliveryMethod: operationType === 'SKU_LABEL' ? null : 'TRUCK', printShippingLabel: false, items, }) const relabelNo = created.application.relabelNo if (chargeAmount > 0) await poll(() => api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/relabel-applications/${relabelNo}`), (detail) => detail.application.walletStatus === 'FROZEN') let detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key(`${name}-CLAIM`) }) for (const [index, item] of detail.items.entries()) { if (operationType === 'CARTON_AND_SKU') { detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, { cartonMark: item.cartonMark, expectedVersion: Number(detail.version), idempotencyKey: key(`${name}-CARTON-${index}`), }) const stageItem = detail.items.find((row) => id(row.itemId) === id(item.itemId)) assert(Number(stageItem?.cartonProcessedPackages) === 1 && Number(stageItem?.skuProcessedPackages) === 0, `${relabelNo} carton stage incorrectly locked or completed SKU stage`) } detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, { itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: null, skuVerifications: (item.skuItems || []).map((sku) => ({ skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity), })), expectedVersion: Number(detail.version), idempotencyKey: key(`${name}-SCAN-${index}`), }) } const sourceBefore = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) const scenario = { name, operationType, relabelNo, items: detail.items, sourceBefore } if (prepare) await prepare(scenario) const form = new FormData() // Valid synthetic PNG for HTTP validation; not a photograph of real warehouse work. form.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), `${marker}.png`) const photo = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/photos/files`, form) detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/complete`, { photoFileIds: [id(photo.fileId)], expectedVersion: Number(detail.version), idempotencyKey: key(`${name}-COMPLETE`), remark: marker, }) assert(detail.applicationStatus === 'CUSTOMER_CONFIRM_PENDING', `${relabelNo} did not reach customer confirmation`) const confirmed = await api(overseas, merchantToken, 'POST', `/api/merchant/v1/last-leg/relabel-applications/${relabelNo}/confirm-completion`, { idempotencyKey: key(`${name}-CONFIRM`), remark: marker, }) const expectedStatus = operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND' assert(confirmed.application.status === expectedStatus, `${relabelNo} did not reach ${expectedStatus}`) assert(confirmed.items.every((item) => item.destinationFbaWarehouseCode === source.destinationFbaWarehouseCode), `${relabelNo} did not preserve the destination warehouse`) return { ...scenario, items: confirmed.items } } async function printPallet(relabelNo, suffix) { return api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-label', { relabelNo, idempotencyKey: key(`PRINT-${suffix}`), }) } async function moveCarton(scenario, item, target, index) { const sourcePallet = await resolvePallet(sourcePosition.palletNo) const targetPallet = await resolvePallet(target.palletCode) const result = await api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-disassemble', { idempotencyKey: key(`${scenario.name}-MOVE-${index}`), relabelNo: scenario.relabelNo, source: { palletCode: sourcePallet.palletCode, expectedVersion: Number(sourcePallet.palletVersion) }, target: { palletCode: targetPallet.palletCode, expectedVersion: Number(targetPallet.palletVersion) }, cartonMarks: [item.cartonMark], reason: `${marker}-${scenario.name}`, }) assert(result.operationType === 'RELABEL_PALLET_DISASSEMBLE', 'Unexpected pallet operation type') return result } const skuOnly = await createAndConfirm('SKU-ONLY', 1, 'SKU_LABEL') skuOnly.sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) assert(skuOnly.sourceAfter === skuOnly.sourceBefore, 'SKU-only relabel must not split or regroup pallets') skuOnly.targetPallets = [] skuOnly.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode delete skuOnly.items let sharedTarget const fresh = await createAndConfirm('NEW-PALLET', 1, 'CARTON_AND_SKU', async scenario => { sharedTarget = await printPallet(scenario.relabelNo, 'SHARED') const printReplay = await printPallet(scenario.relabelNo, 'SHARED') assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent') await moveCarton(scenario, scenario.items[0], sharedTarget, 0) scenario.targets = [sharedTarget] }) const sharedBefore = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) const merged = await createAndConfirm('MERGE-EXISTING', 1, 'CARTON_AND_SKU', async scenario => { await moveCarton(scenario, scenario.items[0], sharedTarget, 0) scenario.targets = [sharedTarget] }) const sharedAfter = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) assert(sharedAfter === sharedBefore + 1, 'Existing target pallet carton count did not increase') merged.targets = [sharedTarget] const split = await createAndConfirm('SPLIT-TWO-TARGETS', 2, 'CARTON_AND_SKU', async scenario => { const splitA = await printPallet(scenario.relabelNo, 'SPLIT-A') const splitB = await printPallet(scenario.relabelNo, 'SPLIT-B') await moveCarton(scenario, scenario.items[0], splitA, 0) await moveCarton(scenario, scenario.items[1], splitB, 1) scenario.targets = [splitA, splitB] }) for (const scenario of [fresh, merged, split]) { const sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) scenario.sourceAfter = sourceAfter scenario.targetPallets = scenario.targets.map((target) => ({ palletId: id(target.palletId), palletCode: target.palletCode })) const candidateRows = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/dispatches/candidates?orderNo=${encodeURIComponent(scenario.relabelNo)}&sourceStage=RECEIVED`) const targetIds = new Set(scenario.targets.map((target) => id(target.palletId))) const rows = candidateRows.filter((row) => row.sourceStage === 'RELABEL' && targetIds.has(id(row.warehousePalletId))) const marks = rows.flatMap((row) => String(row.relabelCartonMarks || '').split(',').filter(Boolean)) assert(rows.length === targetIds.size, `${scenario.relabelNo} missing smart-dispatch pallet candidate`) assert(rows.every((row) => !String(row.cartonMarks || '').trim()), `${scenario.relabelNo} exposed normal carton marks`) assert(scenario.items.every((item) => marks.includes(item.fbaCartonMark)), `${scenario.relabelNo} missing relabel carton mark`) const dispatch = await api(overseas, platformToken, 'POST', '/api/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: rows.map((row) => id(row.warehousePalletId)), route: `${marker}-${scenario.name}`, relabelNo: scenario.relabelNo, }) scenario.dispatchNo = dispatch.dispatchNo scenario.dispatchStatus = dispatch.status scenario.relabelCartonMarks = marks delete scenario.items delete scenario.targets } const ledger = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/inventory/ledger?customerId=${customerId}&page=1&size=100&businessType=RELABEL_PALLET_DISASSEMBLE`) const ledgerRows = ledger.records.filter((row) => String(row.remark || '').includes(marker)) assert(ledgerRows.some((row) => Number(row.deltaPackages) < 0), 'Missing negative relabel pallet ledger') assert(ledgerRows.some((row) => Number(row.deltaPackages) > 0), 'Missing positive relabel pallet ledger') assert(ledgerRows.every((row) => String(row.remark).startsWith('换标拆托')), 'Unexpected relabel ledger reason') report.scenarios = [skuOnly, fresh, merged, split] report.ledger = { rows: ledgerRows.length, deltas: ledgerRows.map((row) => Number(row.deltaPackages)) } report.passed = true report.finishedAt = new Date().toISOString() await writeFile(output, JSON.stringify(report, null, 2), 'utf8') console.log(JSON.stringify(report, null, 2))