import { mkdir, writeFile } from 'node:fs/promises' const MODULE = 'phase4-overseas-client-services' const PDA = '/api/pda/v1/last-leg/cargo-discards' const CLIENT = '/api/merchant/v1/last-leg/cargo-discards' 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) const integer = value => /^\d+$/.test(String(value)) && Number.isSafeInteger(Number(value)) const ok = response => response?.http === 200 && String(response?.body?.code) === '200' /** 仅衔接本轮单箱一件代发废弃申请;所有写入前保存请求,未知结果不重试。 */ export async function verifyCargoDiscardFulfillment(context, { proof, merchantToken, pdaToken, snapshot, checkpoint }) { if (!proof?.approvedNo || proof.status !== 'APPROVAL_HTTP_VERIFIED_CARTON_SQL_PENDING' || !merchantToken || !pdaToken || typeof snapshot !== 'function' || proof.customerId !== String(context.context?.merchantQualificationFixture?.tenantId) || context.createdResources.phase4OverseasClientServices?.marker !== context.marker) { throw new Error('废弃履约缺少本轮已批准申请、客户或PDA会话') } if (proof.fulfillment) throw new Error('已有废弃履约证据,不自动重跑') if (!/^[A-Za-z0-9_-]{1,100}$/.test(context.marker)) throw new Error('废弃批次标识不合法') const evidence = proof.fulfillment = { marker: context.marker, status: 'RUNNING', requests: [], snapshots: {} } let initialized = false const directory = new URL('../../reports/cargo-discard-checkpoints/', import.meta.url) const persist = checkpoint || (async () => { await mkdir(directory, { recursive: true }) await writeFile(new URL(`${context.marker}.json`, directory), JSON.stringify(proof, null, 2), { encoding: 'utf8', flag: initialized ? 'w' : 'wx' }) initialized = true }) const require = async (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), '', { scenario: 'cargo-discard-fulfillment', deploymentBlocker: true }) if (!passed) throw new Error(name) } async function command(method, path, body, token, upload = false, expectedConflict = false) { const entry = { method, path, body: upload ? { syntheticImage: true } : body, expectedConflict, 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() if (expectedConflict) { await require('废弃重复照片明确冲突且保留诊断', [200, 409].includes(response?.http) && ['409', 'A0409'].includes(String(response?.body?.code)) && Boolean(response.requestId) && Boolean(response.traceId)) return response.body.data } await require(`废弃履约 ${method} ${path}`, ok(response)) return response.body.data } const capture = async name => { const state = await snapshot() evidence.snapshots[name] = structuredClone(state); await persist(); return state } const matchTask = task => task?.discardNo === proof.approvedNo && task?.customerId === proof.customerId && task?.overseasWarehouseId === proof.warehouseId && typeof task?.palletNo === 'string' && task.palletNo.length > 0 try { await persist() const before = await capture('before') await require('废弃执行前仍为同客户同仓已批准单箱申请', before.detail?.summary?.discardNo === proof.approvedNo && before.detail.summary.customerId === proof.customerId && before.detail.summary.overseasWarehouseId === proof.warehouseId && before.detail.summary.status === 'APPROVED' && Number(before.detail.summary.totalPackages) === 1) const tasks = await command('GET', PDA, undefined, pdaToken) const selected = Array.isArray(tasks) ? tasks.filter(task => task.discardNo === proof.approvedNo) : [] await require('本轮废弃任务唯一且客户仓库一致', selected.length === 1 && matchTask(selected[0])) const task = selected[0] await require('单箱废弃任务明细和真实箱唛完整', task.status === 'PENDING' && task.skus?.length === 1 && task.skus[0].skuCode === proof.skuCode && task.skus[0].quantityUnit === '箱' && Number(task.skus[0].plannedPackages) === 1 && /^[1-9]\d*$/.test(String(task.skus[0].itemId)) && task.cartonMarks?.length === 1 && typeof task.cartonMarks[0] === 'string' && task.cartonMarks[0].length > 0) const resolved = await command('POST', `${PDA}/resolve`, { discardNo: proof.approvedNo, palletNo: task.palletNo }, pdaToken) await require('PDA扫码解析保持原任务及明细', stable(resolved) === stable(task)) const root = `${PDA}/${encodeURIComponent(proof.approvedNo)}/pallets/${encodeURIComponent(task.palletNo)}` const form = new FormData() form.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')], { type: 'image/png' }), `${context.marker}-synthetic-discard.png`) const photo = await command('POST', `${root}/photos/files`, form, pdaToken, true) await require('废弃图片绑定当前托盘', /^[1-9]\d*$/.test(String(photo?.fileId)) && photo.palletNo === task.palletNo) const body = { lines: [{ itemId: String(task.skus[0].itemId), actualPackages: 1 }], photoFileIds: [String(photo.fileId)], scannedCartonMarks: [...task.cartonMarks], confirmAllCartons: false } const beforeInvalid = await capture('beforeDuplicatePhoto') await command('POST', `${root}/confirm`, { ...body, photoFileIds: [String(photo.fileId), String(photo.fileId)] }, pdaToken, false, true) await require('重复照片拒绝后申请与库存零漂移', stable(await capture('duplicatePhotoRejected')) === stable(beforeInvalid)) const completedTask = await command('POST', `${root}/confirm`, body, pdaToken) await require('PDA完成保留原任务身份和实际数量', matchTask(completedTask) && completedTask.palletNo === task.palletNo && completedTask.status === 'COMPLETED' && completedTask.skus?.length === 1 && String(completedTask.skus[0].itemId) === String(task.skus[0].itemId) && Number(completedTask.skus[0].actualPackages) === 1) const completed = await capture('pdaCompleted') await require('客户详情等待客户确认且实际废弃数量为一箱', completed.detail?.summary?.status === 'PDA_COMPLETED' && completed.detail.summary.discardNo === proof.approvedNo && Number(completed.detail.summary.actualPackages) === 1) const scopedBalances = state => state.balances.filter(row => row.inboundCode === proof.inboundCode && row.skuCode === proof.skuCode) const prior = scopedBalances(before), after = scopedBalances(completed) await require('废弃库存快照包含同一来源余额', prior.length > 0 && prior.length === after.length && [...prior, ...after].every(row => integer(row.totalPackages) && integer(row.availablePackages))) const sum = (rows, field) => rows.reduce((value, row) => value + Number(row[field]), 0) await require('PDA废弃只扣本来源一箱总量和可用量', sum(prior, 'totalPackages') - sum(after, 'totalPackages') === 1 && sum(prior, 'availablePackages') - sum(after, 'availablePackages') === 1) const movements = completed.ledger.filter(row => row.businessOrderNo === proof.approvedNo) await require('本申请只有一笔正确废弃总账', !before.ledger.some(row => row.businessOrderNo === proof.approvedNo) && movements.length === 1 && movements[0].businessType === 'DISCARD' && Number(movements[0].deltaPackages) === -1 && /^[1-9]\d*$/.test(String(movements[0].id)) && movements[0].customerId === proof.customerId && movements[0].overseasWarehouseId === proof.warehouseId && movements[0].palletNo === task.palletNo && movements[0].skuCode === proof.skuCode && movements[0].inboundCode === proof.inboundCode && integer(movements[0].beforePackages) && integer(movements[0].afterPackages) && Number(movements[0].beforePackages) - Number(movements[0].afterPackages) === 1) await command('POST', `${root}/confirm`, body, pdaToken) await require('PDA完成重放库存与单据零漂移', stable(await capture('pdaReplay')) === stable(completed)) const clientPath = `${CLIENT}/${encodeURIComponent(proof.approvedNo)}/confirm-completion` await command('POST', clientPath, undefined, merchantToken) const confirmed = await capture('customerCompleted') await require('客户确认完成且库存不再扣减', confirmed.detail?.summary?.status === 'COMPLETED' && Boolean(confirmed.detail.summary.customerConfirmedAt) && stable(confirmed.balances) === stable(completed.balances) && stable(confirmed.ledger) === stable(completed.ledger)) await command('POST', clientPath, undefined, merchantToken) await command('POST', `${root}/confirm`, body, pdaToken) await require('客户及PDA终态重放保持原完成事实', stable(await capture('terminalReplay')) === stable(confirmed)) evidence.status = 'HTTP_VERIFIED'; proof.status = 'COMPLETED_HTTP_VERIFIED_CARTON_SQL_PENDING' proof.remainingEvidence = '公共库存总账已核对;独立箱、SKU及箱级流水底层证据仍待验证。' await persist() return evidence } catch (error) { evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error } }