import { mkdir, writeFile, readFile } from 'node:fs/promises' import { createHash } from 'node:crypto' const base = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300' if (!['localhost', '127.0.0.1'].includes(new URL(base).hostname)) throw new Error('Local acceptance only') const merchant = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN, pda = process.env.PDA_OVERSEAS_ACCESS_TOKEN, admin = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN const warehouse = process.env.OVERSEAS_TEST_WAREHOUSE_ID if (!merchant || !pda || !admin || !warehouse) throw new Error('Explicit session and warehouse required') const marker = `CLOSE-${Date.now()}` const report = { marker, passed: false, scope: 'REAL_HTTP_SYNTHETIC_WAREHOUSE_EVIDENCE_NO_FINANCE', scenarios: [] } const key = () => `${marker}-${crypto.randomUUID()}` const root = '/api/merchant/v1/last-leg/relabel-applications' const work = '/api/pda/v1/last-leg/relabel-tasks' const pallets = '/api/pda/v1/last-leg/pallet-operations' const assert = (condition, message) => { if (!condition) throw new Error(message) } async function raw(token, method, path, body) { const response = await fetch(base + path, { method, headers: { Authorization: `Bearer ${token}`, ...(body && !(body instanceof FormData) ? { 'Content-Type': 'application/json' } : {}) }, body: body instanceof FormData ? body : body ? JSON.stringify(body) : undefined }) const result = await response.json() return { ...result, http: response.status } } async function api(token, method, path, body) { const result = await raw(token, method, path, body) assert(result.http === 200 && String(result.code) === '200', `${path}: ${result.code} ${result.message}`) return result.data } async function reject(token, path, body, message) { const result = await raw(token, 'POST', path, body) assert(String(result.code) !== '200' && message.test(result.message || ''), `Expected rejection at ${path}, got ${result.code} ${result.message}`) } const resolve = code => api(pda, 'POST', `${pallets}/pallets/resolve`, { code }) async function move(no, mark, from, to) { const source = await resolve(from), target = await resolve(to) const request = { relabelNo: no, source: { palletCode: from, expectedVersion: source.palletVersion }, target: { palletCode: to, expectedVersion: target.palletVersion }, cartonMarks: [mark], reason: marker, idempotencyKey: key() } const result = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request) const replay = await api(pda, 'POST', `${pallets}/relabel-disassemble`, request) assert(replay.replayed === true, 'Move replay was not idempotent') const after = await resolve(to) assert(Number(after.totalPackages) === Number(target.totalPackages) + 1 && after.status === 'OCCUPIED', 'Target stock/status did not update exactly once') assert(Number((await resolve(from)).totalPackages) === Number(source.totalPackages) - 1, 'Source stock was not reduced once') return result } try { for (const deliveryMethod of ['TRUCK_PICKUP', 'EXPRESS', 'EXPRESS_PICKUP']) { const candidates = await api(merchant, 'GET', `${root}/inventory-candidates?operationWarehouseId=${warehouse}`) const candidate = candidates.find(value => value.destinationFbaWarehouseCode && value.positions?.some(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND')) assert(candidate, 'No available inventory for another scenario') const position = candidate.positions.find(pos => Number(pos.availablePackages) > 0 && pos.palletNo !== 'UNBOUND') const mark = `${marker}-${deliveryMethod}` const request = { customerOrderNo: mark, operationWarehouseId: warehouse, labelDeadline: new Date(Date.now() + 86400000).toISOString().slice(0, 19), contactName: '流程验收', contactPhone: '13800000000', deliveryMethod, printShippingLabel: false, idempotencyKey: key(), items: [{ sourceInventoryBalanceId: candidate.inventoryBalanceId, sourceInventoryPositionId: position.positionId, sourceInboundOrderId: candidate.inboundOrderId, skuCode: candidate.skuCode || 'TEST', productNameZh: '验收测试货物', totalPackageCount: 1, notifiedPackageCount: 1, cartonMark: mark, fbaCartonMark: `FBA-${mark}`, destinationFbaWarehouseCode: candidate.destinationFbaWarehouseCode, operationType: 'CARTON_LABEL', skuItems: [] }] } request.items[0].shipmentNo = `SHIP-${mark}` const created = await api(merchant, 'POST', root, request) const no = created.application.relabelNo const scenario = { deliveryMethod, relabelNo: no, originalPallet: position.palletNo, checks: [] } report.scenarios.push(scenario) await reject(merchant, root, { ...request, idempotencyKey: key() }, /已用于换标单/) scenario.checks.push('DUPLICATE_APPLICATION_REJECTED') // Read an actual Print-generated test document from File, then upload it through the merchant API. const pdfReport = JSON.parse(await readFile('reports/relabel-closure-20260916/pdf.json', 'utf8')) const doc = pdfReport.files[0] const pdfResponse = await fetch(`http://127.0.0.1:9206/internal/file/v1/business-files/${process.env.OVERSEAS_TEST_CUSTOMER_ID}/${doc.category}/${pdfReport.businessId}/${doc.fileId}/content`, { headers: { 'X-Internal-Service-Token': process.env.INTERNAL_SERVICE_TOKEN } }) const pdf = Buffer.from(await pdfResponse.arrayBuffer()) assert(pdfResponse.ok && pdf.subarray(0, 5).toString() === '%PDF-', 'Test PDF not available') for (const scope of ['CARTON_TOTAL', 'SKU_TOTAL', 'ITEM']) { const form = new FormData(); form.append('file', new Blob([pdf], { type: 'application/pdf' }), `${marker}.pdf`) await api(merchant, 'POST', `${root}/${no}/files?scope=${scope}${scope === 'ITEM' ? `&itemId=${created.items[0].id}` : ''}`, form) } let task = await api(pda, 'POST', `${work}/${no}/claim`, { idempotencyKey: key() }) const documents = await api(pda, 'GET', `${work}/${no}/documents`) assert(documents.length >= 3, 'PDA is missing uploaded label documents') for (const file of documents) { const response = await fetch(`${base}${work}/${no}/documents/${file.fileId}/download?scope=${file.scope}${file.itemId ? `&itemId=${file.itemId}` : ''}`, { headers: { Authorization: `Bearer ${pda}` } }) const downloaded = Buffer.from(await response.arrayBuffer()) assert(response.ok && createHash('sha256').update(downloaded).digest('hex') === createHash('sha256').update(pdf).digest('hex'), 'PDA document mismatch') } scenario.checks.push('MERCHANT_UPLOAD_PDA_DOWNLOAD_THREE_SCOPES') task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() }) const photoForm = new FormData(); photoForm.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), 'synthetic.png') const photo = await api(pda, 'POST', `${work}/${no}/photos/files`, photoForm) const complete = () => ({ photoFileIds: [photo.fileId], expectedVersion: task.version, remark: marker, idempotencyKey: key() }) await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/) scenario.checks.push('UNBOUND_COMPLETION_REJECTED') const first = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() }) await move(no, mark, position.palletNo, first.palletCode) const second = await api(pda, 'POST', `${pallets}/relabel-pallet-label`, { relabelNo: no, idempotencyKey: key() }) await move(no, mark, first.palletCode, second.palletCode) scenario.targetPallet = second.palletCode scenario.checks.push('REBIND_STOCK_CONSERVED_AND_REPLAY_SAFE') task = await api(pda, 'POST', `${work}/${no}/complete`, complete()) if (deliveryMethod === 'TRUCK_PICKUP') { task = await api(admin, 'POST', `/api/platform/v1/last-leg/relabel-applications/${no}/rework`, { decision: 'REWORK', expectedVersion: task.version, idempotencyKey: key(), remark: '验收退回:请核对并重新贴标' }) assert(task.reviewRemark?.includes('验收退回'), 'Rework remark missing on PDA') task = await api(pda, 'POST', `${work}/${no}/cartons/scan`, { cartonMark: mark, expectedVersion: task.version, idempotencyKey: key() }) await reject(pda, `${work}/${no}/complete`, complete(), /托盘绑定/) await move(no, mark, second.palletCode, first.palletCode) scenario.targetPallet = first.palletCode task = await api(pda, 'POST', `${work}/${no}/complete`, complete()) scenario.checks.push('REWORK_RESCAN_REBIND_AND_RESUBMIT') } const confirmed = await api(merchant, 'POST', `${root}/${no}/confirm-completion`, { idempotencyKey: key(), remark: marker }) scenario.outboundNo = confirmed.application.outboundOrderNo assert(scenario.outboundNo, 'Customer confirmation did not create direct outbound') await reject(pda, `${pallets}/relabel-disassemble`, { relabelNo: no, source: { palletCode: scenario.targetPallet, expectedVersion: 0 }, target: { palletCode: position.palletNo, expectedVersion: 0 }, cartonMarks: [mark], idempotencyKey: key() }, /排车或出库/) const resolved = await api(pda, 'GET', `/api/pda/v1/last-leg/outbound-tasks/resolve/${scenario.outboundNo}`) const picked = await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/pallets/${scenario.targetPallet}/pick`, { idempotencyKey: key() }) await api(pda, 'POST', `/api/pda/v1/last-leg/outbound-tasks/${resolved.taskNo}/complete`, { idempotencyKey: key() }) const outbound = await api(admin, 'GET', `/api/platform/v1/last-leg/outbound-orders/${scenario.outboundNo}?page=1&size=20`) assert(outbound.outbound.status === 'OUTBOUNDED', 'Direct outbound not completed') scenario.outboundStatus = outbound.outbound.status scenario.checks.push('DIRECT_OUTBOUND_COMPLETED', 'OUTBOUND_REBIND_REJECTED') } report.passed = true } catch (error) { report.error = error.message; process.exitCode = 1 } finally { await mkdir('reports/relabel-closure-20260916', { recursive: true }) await writeFile('reports/relabel-closure-20260916/extra.json', JSON.stringify(report, null, 2)) console.log(JSON.stringify(report, null, 2)) }