Files
baiyue-firstleg/scripts/admin-test-env-modules/cargo-discard-fulfillment.mjs

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
}
}