133 lines
11 KiB
JavaScript
133 lines
11 KiB
JavaScript
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, merchantToken, pdaToken, checkpoint, delay = () => new Promise(resolve => setTimeout(resolve, 1000)) }) {
|
|
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('../../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(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-${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)
|
|
const reviewed = await command('PUT', `${platform}/status`, { 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)
|
|
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 === 'WAIT_PRE_DISPATCH'
|
|
&& stable(confirmed) === stable(finalReplay))
|
|
Object.assign(proof, { status: 'READY_FOR_OUTBOUND', targetPalletIds: [String(target.palletId)],
|
|
sourceCartonMarks: [item.cartonMark], relabelCartonMarks: [item.fbaCartonMark].filter(Boolean) })
|
|
evidence.status = 'HTTP_VERIFIED_READY_FOR_DISPATCH'; await persist(); return proof
|
|
} catch (error) {
|
|
evidence.status = 'FAILED'; evidence.failure = error.message; await persist(); throw error
|
|
}
|
|
}
|