import { mkdir, writeFile } from 'node:fs/promises' const MODULE = 'phase4-overseas-client-services' const PALLETS = '/api/pda/v1/last-leg/pallet-operations' const validId = value => /^[1-9]\d*$/.test(String(value)) const count = value => /^\d+$/.test(String(value)) && Number.isSafeInteger(Number(value)) const stable = value => JSON.stringify(value, (_, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(key => [key, item[key]])) : item) /** 原申请单箱贴标作业链;独立贴标准备排车,关联转运恢复原单预留。 */ export async function verifyClientRelabelFulfillment(context, { proof, transferProof, merchantToken, pdaToken, checkpoint, delay = () => new Promise(resolve => setTimeout(resolve, 1000)) }) { // 已有库存转运的换标必须回到原转运预留,不进入普通贴标排车链。 if (transferProof && (context.createdResources?.phase4OverseasClientServices?.transferFulfillment !== transferProof || transferProof.relabelFulfillment !== proof || transferProof.marker !== context.marker || transferProof.status !== 'WAIT_RELABEL' || transferProof.allocation?.status !== 'ALLOCATED_WAIT_RELABEL' || transferProof.relabelNo !== proof?.relabelNo || transferProof.transferNo !== proof?.sourceOrderNo || transferProof.customerId !== proof?.customerId || transferProof.warehouseId !== proof?.warehouseId || transferProof.sourcePositionId !== proof?.sourcePositionId || transferProof.sourcePalletNo !== proof?.sourcePalletNo)) { throw new Error('转运关联换标缺少原分配证据') } if (proof?.marker !== context.marker || !proof?.relabelNo || !merchantToken || !pdaToken || proof.customerId !== String(context.context?.merchantQualificationFixture?.tenantId) || !validId(proof.warehouseId) || !validId(proof.sourcePositionId) || !proof.sourcePalletNo) { throw new Error('贴标后续缺少本轮客户申请、来源仓位及会话') } if (proof.execution) throw new Error('已有贴标执行证据,不自动重跑') if (!/^[A-Za-z0-9_-]{1,100}$/.test(context.marker)) throw new Error('贴标批次标识不合法') const evidence = proof.execution = { status: 'RUNNING', requests: [] } let initialized = false const directory = new URL(transferProof ? '../../reports/transfer-relabel-checkpoints/' : '../../reports/client-relabel-checkpoints/', import.meta.url) const persist = checkpoint || (async () => { await mkdir(directory, { recursive: true }) await writeFile(new URL(`${context.marker}.json`, directory), JSON.stringify(transferProof || proof, null, 2), { encoding: 'utf8', flag: initialized ? 'w' : 'wx' }) initialized = true }) const require = (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), '', { scenario: 'client-relabel-fulfillment', deploymentBlocker: true }) if (!passed) throw new Error(name) } const key = action => `${context.marker}-CLIENT-RELABEL-${proof.relabelNo}-${action}` const platform = `/api/platform/v1/last-leg/relabel-applications/${encodeURIComponent(proof.relabelNo)}` const merchant = `/api/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(proof.relabelNo)}` const work = `/api/pda/v1/last-leg/relabel-tasks/${encodeURIComponent(proof.relabelNo)}` async function command(method, path, body, token, upload = false) { const entry = { method, path, body: upload ? { syntheticImage: true } : body, status: 'SENDING' } evidence.requests.push(entry); await persist() let response try { response = upload ? await context.requestMultipart(method, path, body, token) : await context.request(method, path, body, token) } catch (error) { entry.status = 'UNKNOWN_RESULT'; await persist(); throw error } Object.assign(entry, { status: response?.http === 0 || response?.http >= 500 || !response?.body ? 'UNKNOWN_RESULT' : 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await persist() require(`贴标后续 ${method} ${path}`, response?.http === 200 && String(response?.body?.code) === '200') return response.body.data } const read = () => command('GET', platform, undefined, context.token) const sameApplication = detail => detail?.application?.relabelNo === proof.relabelNo && detail.application.customerOrderNo === proof.sourceOrderNo && String(detail.application.overseasWarehouseId) === proof.warehouseId const resolve = palletCode => command('POST', `${PALLETS}/pallets/resolve`, { code: palletCode }, pdaToken) try { await persist() const profile = await command('GET', '/api/merchant/v1/iam/tenant/profile', undefined, merchantToken) require('贴标当前客户会话身份有效', String(profile?.id) === proof.customerId && profile?.status === 'ACTIVE') const owned = await command('GET', merchant, undefined, merchantToken) require('客户可读取同一贴标申请', sameApplication(owned)) const original = await read() require('贴标原申请客户、仓库、来源仓位与待审核状态一致', sameApplication(original) && original.application.status === 'PENDING_REVIEW' && original.items?.length === 1 && String(original.items[0].sourceInventoryPositionId) === proof.sourcePositionId) require('贴标审核渠道与业务来源一致', original.application.reviewChannel === (transferProof ? 'RELABEL_ORDER' : 'TAIL_ORDER') && validId(original.application.lastlegOrderId)) const reviewPath = transferProof ? `${platform}/status` : `/api/platform/v1/last-leg/orders/${original.application.lastlegOrderId}/relabel-review?customerId=${encodeURIComponent(proof.customerId)}` const reviewed = await command('PUT', reviewPath, { status: 'PENDING_PROCESSING', remark: context.marker }, context.token) require('贴标审核推进同一申请', sameApplication(reviewed) && reviewed.application.status === 'PENDING_PROCESSING') let funded for (let attempt = 0; attempt < 31; attempt++) { funded = await read() require('贴标等待资金期间申请身份不变', sameApplication(funded)) if (funded.application.walletStatus === 'FROZEN') break if (attempt < 30) await delay() } require('贴标费用已冻结后才开始PDA作业', funded.application.walletStatus === 'FROZEN') const claim = { idempotencyKey: key('claim') } let task = await command('POST', `${work}/claim`, claim, pdaToken) const replay = await command('POST', `${work}/claim`, claim, pdaToken) require('认领原键重放不改变任务', stable(task) === stable(replay)) require('PDA原申请单箱任务及版本有效', task.relabelNo === proof.relabelNo && task.taskStatus === 'PROCESSING' && count(task.version) && task.items?.length === 1 && Number(task.items[0].expectedPackages) === 1) const item = task.items[0] require('PDA箱唛及SKU校验来自原申请', validId(item.itemId) && Boolean(item.cartonMark) && ['CARTON_AND_SKU', 'SKU_LABEL'].includes(item.operationType) && item.skuItems?.length > 0 && item.skuItems.every(sku => sku.skuCode && sku.newSkuCode && count(sku.quantity) && Number(sku.quantity) > 0)) if (item.operationType === 'CARTON_AND_SKU') task = await command('POST', `${work}/cartons/scan`, { cartonMark: item.cartonMark, expectedVersion: Number(task.version), idempotencyKey: key('carton') }, pdaToken) const scan = { itemId: String(item.itemId), oldLabel: item.cartonMark, newLabel: item.fbaCartonMark || item.newSkuCode, skuVerifications: item.skuItems.map(sku => ({ skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity) })), expectedVersion: Number(task.version), idempotencyKey: key('unit') } task = await command('POST', `${work}/units`, scan, pdaToken) const scannedReplay = await command('POST', `${work}/units`, scan, pdaToken) require('单箱贴标完成且扫描重放不重复计数', task.relabelNo === proof.relabelNo && Number(task.relabelledPackages) === 1 && Number(task.skippedPackages) === 0 && Number(task.exceptionPackages) === 0 && stable(task) === stable(scannedReplay)) const detail = await read() require('组托前来源位置和箱唛仍属于原申请', sameApplication(detail) && detail.items?.length === 1 && String(detail.items[0].sourceInventoryPositionId) === proof.sourcePositionId && detail.items[0].cartonMark === item.cartonMark) const target = await command('POST', `${PALLETS}/relabel-pallet-label`, { relabelNo: proof.relabelNo, idempotencyKey: key('label') }, pdaToken) require('换标生成本次业务目标托盘', validId(target?.palletId) && Boolean(target.palletCode) && target.palletCode !== proof.sourcePalletNo) const from = await resolve(proof.sourcePalletNo), to = await resolve(target.palletCode) require('源目标托盘数量版本有效', from?.palletCode === proof.sourcePalletNo && to?.palletCode === target.palletCode && [from, to].every(value => count(value?.totalPackages) && count(value?.palletVersion))) const binding = { relabelNo: proof.relabelNo, source: { palletCode: from.palletCode, expectedVersion: Number(from.palletVersion) }, target: { palletCode: to.palletCode, expectedVersion: Number(to.palletVersion) }, cartonMarks: [item.cartonMark], reason: context.marker, idempotencyKey: key('bind') } const bound = await command('POST', `${PALLETS}/relabel-disassemble`, binding, pdaToken) const boundReplay = await command('POST', `${PALLETS}/relabel-disassemble`, binding, pdaToken) const fromAfter = await resolve(proof.sourcePalletNo), toAfter = await resolve(target.palletCode) require('换标原箱源减一目标增一且组托重放复用操作', validId(bound.operationId) && bound.status === 'COMPLETED' && boundReplay.replayed === true && String(bound.operationId) === String(boundReplay.operationId) && Number(fromAfter.totalPackages) === Number(from.totalPackages) - 1 && Number(toAfter.totalPackages) === Number(to.totalPackages) + 1) if (transferProof) { // 明确的受控TEST补测输入;不按箱数、原托尺寸或计费重量推算,也不作为真实硬件实测证据。 const input = { lengthCm: 120, widthCm: 100, heightCm: 80, grossWeightKg: 12.5 } proof.palletMeasurementInput = { syntheticTestMaterials: true, input } let measurements = await command('POST', `${PALLETS}/relabel-pallet-measurements`, { relabelNo: proof.relabelNo }, pdaToken) const codes = new Set([from.palletCode, target.palletCode]) require('补测只接受本次源目标非空托盘及当前库存版本', Array.isArray(measurements) && measurements.length > 0 && measurements.every(row => validId(row.palletId) && codes.has(row.palletCode) && Number(row.packages) > 0 && row.stockVersion)) for (const row of [...measurements]) { if (['grossWeightKg', 'volumeCbm'].every(field => Number(row[field]) > 0) && (row.cartonAggregated || ['lengthCm', 'widthCm', 'heightCm'].every(field => Number(row[field]) > 0))) continue require('箱级实测不完整时不能用整托输入绕过', row.cartonAggregated !== true) measurements = await command('POST', `${PALLETS}/relabel-pallet-measure`, { relabelNo: proof.relabelNo, palletCode: row.palletCode, stockVersion: row.stockVersion, idempotencyKey: key(`measure-${row.palletId}`), ...input }, pdaToken) const measured = measurements.find(value => String(value.palletId) === String(row.palletId)) require('补测回执保持本托库存版本和明确TEST输入', measured?.stockVersion === row.stockVersion && Object.entries(input).every(([field, value]) => Number(measured[field]) === value) && Number(measured.volumeCbm) > 0) } proof.palletMeasurements = measurements } task = await command('GET', work, undefined, pdaToken) const photo = await command('POST', `${work}/photos/files`, { name: `${context.marker}-synthetic-relabel.png`, type: 'image/png', content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64') }, pdaToken, true) require('换标完工图片返回正式文件引用', validId(photo?.fileId)) const complete = { expectedVersion: Number(task.version), photoFileIds: [String(photo.fileId)], manualCompletion: false, idempotencyKey: key('complete'), remark: context.marker } const completed = await command('POST', `${work}/complete`, complete, pdaToken) const completedReplay = await command('POST', `${work}/complete`, complete, pdaToken) require('PDA完工及重放等待客户确认', completed.relabelNo === proof.relabelNo && completed.applicationStatus === 'CUSTOMER_CONFIRM_PENDING' && stable(completed) === stable(completedReplay)) const confirm = { idempotencyKey: key('confirm'), remark: context.marker } const confirmed = await command('POST', `${merchant}/confirm-completion`, confirm, merchantToken) const finalReplay = await command('POST', `${merchant}/confirm-completion`, confirm, merchantToken) require('客户确认原申请进入对应出库流程且原键重放零漂移', sameApplication(confirmed) && confirmed.application.status === 'READY_FOR_OUTBOUND' && confirmed.application.fulfillmentStatus === (transferProof ? 'WAIT_TRANSFER_OUTBOUND' : 'WAIT_PRE_DISPATCH') && stable(confirmed) === stable(finalReplay)) if (transferProof) { const transfer = await command('GET', `/api/platform/v1/last-leg/warehouse-transfers/${encodeURIComponent(transferProof.transferNo)}?customerId=${encodeURIComponent(proof.customerId)}`, undefined, context.token) const active = transfer?.allocations?.filter(row => row.status === 'RESERVED') || [] require('关联换标完成后原转运保留新SKU实托唯一预留', transfer?.transferNo === transferProof.transferNo && transfer.status === 'ALLOCATED' && transfer.relabelNo === proof.relabelNo && String(transfer.relabelApplicationId) === transferProof.relabelApplicationId && active.length === 1 && active[0].packageCount === 1 && String(active[0].palletId) === String(target.palletId) && active[0].inboundCode === transferProof.inboundCode && validId(active[0].inventoryPositionId) && active[0].skuCode === item.skuItems[0].newSkuCode) Object.assign(transferProof, { status: 'ALLOCATED', outboundAllocations: active }) } Object.assign(proof, { status: 'READY_FOR_OUTBOUND', targetPalletIds: [String(target.palletId)], sourceCartonMarks: [item.cartonMark], relabelCartonMarks: [item.fbaCartonMark].filter(Boolean) }) evidence.status = transferProof ? 'HTTP_VERIFIED_READY_FOR_TRANSFER_OUTBOUND' : 'HTTP_VERIFIED_READY_FOR_DISPATCH' await persist(); return proof } catch (error) { evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error } }