128 lines
9.7 KiB
JavaScript
128 lines
9.7 KiB
JavaScript
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
|
|
}
|
|
}
|