提交
This commit is contained in:
@@ -0,0 +1,299 @@
|
||||
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { prepareFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
|
||||
|
||||
const gateway = process.env.ADMIN_TEST_GATEWAY?.replace(/\/$/, '')
|
||||
if (!gateway || !['localhost', '127.0.0.1'].includes(new URL(gateway).hostname)) throw Error('必须显式指定本机 Gateway')
|
||||
const output = resolve(process.env.MIXED_ACCEPTANCE_OUTPUT || 'reports/local-mixed-looseboard-workflow.json')
|
||||
const scenario = process.env.MIXED_ACCEPTANCE_SCENARIO || 'multi-return'
|
||||
if (!['multi-return', 'normal'].includes(scenario)) throw Error('验收场景仅支持 multi-return 或 normal')
|
||||
let report
|
||||
try { report = JSON.parse(await readFile(output, 'utf8')) } catch (error) { if (error.code !== 'ENOENT') throw error }
|
||||
report ||= { marker: `MIXLB-${Date.now()}`, environment: 'LOCAL', syntheticEvidence: true, orders: [], operations: {}, checks: [], createdResources: {} }
|
||||
if (report.scenario && report.scenario !== scenario || !report.scenario && report.orders.length && scenario !== 'multi-return') throw Error('不同场景必须使用独立验收报告')
|
||||
report.scenario = scenario
|
||||
const root = '/api/platform/v1/last-leg', pr = '/api/pda/v1/last-leg'
|
||||
const key = name => createHash('sha256').update(`${report.marker}:${name}`).digest('hex')
|
||||
let admin, pda, merchant
|
||||
/** 原子保存写请求和回执,失败留单续跑,不删除或重复造单。 */
|
||||
async function save() { await mkdir(dirname(output), { recursive: true }); await writeFile(`${output}.tmp`, JSON.stringify(report, null, 2)); await rename(`${output}.tmp`, output) }
|
||||
/** 断言保留业务含义,失败立即停止后续写入。 */
|
||||
function check(value, label) { report.checks.push({ label, passed: Boolean(value) }); if (!value) throw Error(label) }
|
||||
/** 统一Gateway真实HTTP;会话凭据不写入报告。 */
|
||||
async function raw(method, path, body, token = admin) {
|
||||
const form = body instanceof FormData
|
||||
const response = await fetch(gateway + path, { method, signal: AbortSignal.timeout(45000), headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body !== undefined && !form ? { 'Content-Type': 'application/json' } : {}), ...(path.endsWith('/exception-orders/return-inbound-plans') ? { 'Idempotency-Key': key(`return-plan-${body.plan.orderId}`) } : {}) }, body: body === undefined ? undefined : form ? body : JSON.stringify(body) })
|
||||
const value = await response.json()
|
||||
return { http: response.status, ...value, requestId: value.requestId || response.headers.get('x-request-id'), traceId: value.traceId || response.headers.get('x-trace-id') }
|
||||
}
|
||||
/** 仅读显式配置的现有账号,不新建或修改权限。 */
|
||||
async function login(username, password) {
|
||||
if (!username || !password) throw Error('缺少显式验收账号配置')
|
||||
const result = await raw('POST', '/api/auth/login', { loginId: username, password }, '')
|
||||
if (String(result.code) !== '200') throw Error(`登录失败:${result.code} ${result.message}`)
|
||||
return result.data.accessToken
|
||||
}
|
||||
/** 每条命令前读取当前服务端事实,禁止自行推算版本。 */
|
||||
async function get(path, token = admin) {
|
||||
const result = await raw('GET', path, undefined, token)
|
||||
if (result.http !== 200 || String(result.code) !== '200') throw Error(`GET ${path}: ${result.http}/${result.code} ${result.message}`)
|
||||
return result.data
|
||||
}
|
||||
/** 原步骤成功回执可恢复;未知写结果停写,保留原幂等键供核对。 */
|
||||
async function write(name, method, path, body, token = admin) {
|
||||
if (report.operations[name]?.status === 'SUCCESS') return report.operations[name].result
|
||||
if (report.unknownWrite) throw Error(`上次写结果未知:${report.unknownWrite}`)
|
||||
const entry = { method, path, body: body instanceof FormData ? 'SYNTHETIC_TEST_IMAGE' : body, status: 'SENDING' }
|
||||
report.operations[name] = entry; await save()
|
||||
let result
|
||||
try { result = await raw(method, path, body, token) } catch (error) { entry.status = 'UNKNOWN'; report.unknownWrite = name; await save(); throw error }
|
||||
Object.assign(entry, { http: result.http, code: result.code, requestId: result.requestId, traceId: result.traceId, message: result.message })
|
||||
entry.status = result.http === 200 && String(result.code) === '200' ? 'SUCCESS' : result.http >= 500 || result.code == null ? 'UNKNOWN' : 'FAILED'
|
||||
if (entry.status === 'UNKNOWN') report.unknownWrite = name
|
||||
if (entry.status === 'SUCCESS') entry.result = result.data
|
||||
await save()
|
||||
if (entry.status !== 'SUCCESS') throw Error(`${method} ${path}: ${result.http}/${result.code} ${result.message}`)
|
||||
return result.data
|
||||
}
|
||||
/** 合成小图仅证明文件协议和归属,不能代表现场物流证据。 */
|
||||
async function photo(name, path, token = admin) {
|
||||
const form = new FormData(); form.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', 'base64')], { type: 'image/png' }), `${report.marker}-${name}-SYNTHETIC-TEST.png`)
|
||||
return write(name, 'POST', path, form, token)
|
||||
}
|
||||
/** 三箱分两托实收,订单展示应汇总一行,PDA沿用真实托盘明细。 */
|
||||
async function receive(order) {
|
||||
const path = `${pr}/warehouse-transfers/${order.transferNo}`, label = order.orderNo
|
||||
let d = await get(path, pda)
|
||||
if (d.status === 'READY_FOR_DISPATCH') return
|
||||
while (d.pallets.length < 2) { await write(`${label}-draft-${d.pallets.length}`, 'POST', `${path}/pallets/drafts`, { idempotencyKey: key(`${label}-draft-${d.pallets.length}`) }, pda); d = await get(path, pda) }
|
||||
for (const initial of d.pallets) {
|
||||
let p = d.pallets.find(row => row.id === initial.id)
|
||||
if (p.status === 'DRAFT') await write(`${label}-generate-${p.id}`, 'POST', `${path}/pallets/${p.id}/generate`, { idempotencyKey: key(`${label}-generate-${p.id}`) }, pda)
|
||||
d = await get(path, pda); p = d.pallets.find(row => row.id === initial.id)
|
||||
if (p.status === 'GENERATED') await write(`${label}-bind-${p.id}`, 'POST', `${path}/pallets/${p.id}/bind`, { idempotencyKey: key(`${label}-bind-${p.id}`), palletCode: p.palletNo }, pda)
|
||||
d = await get(path, pda)
|
||||
}
|
||||
if (!d.cartons.every(row => row.countedAt)) await write(`${label}-count`, 'POST', `${path}/cartons/count`, { idempotencyKey: key(`${label}-count`), forecastPackageCount: d.packageCount,
|
||||
boxes: d.cartons.map((row, i) => ({ cartonMark: row.cartonMark, transferPalletId: d.pallets[i === 0 ? 0 : 1].id, length: 40, width: 30, height: 25, weight: 4, items: [{ skuCode: d.items[0].skuCode, quantity: 1 }] })) }, pda)
|
||||
const f = await photo(`${label}-measurement`, `${path}/measurement-photos/files`, pda)
|
||||
await write(`${label}-palletize`, 'POST', `${path}/palletize`, { idempotencyKey: key(`${label}-palletize`), photoFileIds: [f.fileId] }, pda)
|
||||
d = await get(path, pda); check(d.status === 'READY_FOR_DISPATCH' && d.pallets.length === 2, `${label}三箱两托实收完成`); order.receiving = d; await save()
|
||||
}
|
||||
/** 六张本轮原单:三单到仓实收,三单保留异地上门取货。 */
|
||||
async function prepare() {
|
||||
const customers = (await get('/api/platform/v1/iam/tenants?page=1&size=200')).records
|
||||
report.customerId ||= customers.find(row => row.status === 'ACTIVE' && /测试/.test(row.tenantName || row.name))?.id
|
||||
report.warehouseId ||= (await get('/api/platform/v1/warehouse-admin?page=1&size=100&warehouseType=OVERSEAS&status=ACTIVE')).records.find(row => row.warehouseCode === 'US-LAX-01')?.id
|
||||
check(report.customerId && report.warehouseId, '复用有效测试客户和洛杉矶仓'); await save()
|
||||
const ticket = await raw('POST', `/api/platform/v1/iam/tenants/${report.customerId}/impersonation`, { reason: `${report.marker}本机混排验收` })
|
||||
check(String(ticket.code) === '200', '获取本轮客户下单会话')
|
||||
merchant = (await raw('POST', '/api/auth/impersonations/exchange', { code: ticket.data.code }, '')).data.accessToken
|
||||
for (const mode of ['DELIVERY_TO_WAREHOUSE', 'WAREHOUSE_PICKUP']) for (let i = 0; i < 3; i++) {
|
||||
const label = `${mode}-${i}`, remote = mode === 'WAREHOUSE_PICKUP'
|
||||
let order = report.orders.find(row => row.label === label)
|
||||
if (!order) {
|
||||
const created = await write(label, 'POST', '/api/merchant/v1/last-leg/local-looseboard-orders', { operatingWarehouseId: report.warehouseId, destinationType: 'PRIVATE', destinationPostalCode: '91761', deliveryMode: mode,
|
||||
deliveryContactName: '合成验收联系人', deliveryContactPhone: '0000000000', estimatedArrivalAt: '2026-11-06T09:00:00', trackingNo: `${report.marker}-${label}`,
|
||||
origin: 'Ontario TEST', originAddress: `100 TEST Pickup Road ${i}`, destination: 'Ontario TEST', destinationAddress: '200 TEST Destination Road', productName: `${report.marker}-${label}`,
|
||||
estimatedPalletCount: 2, packageCount: remote ? 2 : 3, totalWeightKg: remote ? 8 : 12, totalVolumeCbm: remote ? 0.06 : 0.09, budgetAmount: 25, currency: 'USD',
|
||||
originContactName: '合成验收取货员', originContactPhone: '0000000000', destinationContactName: '合成验收收货员', destinationContactPhone: '0000000000',
|
||||
pickupAt: '2026-11-06T09:00:00', deliveryAt: '2026-11-07T09:00:00', remark: `${report.marker} SYNTHETIC TEST`, idempotencyKey: key(label) }, merchant)
|
||||
order = { label, mode, orderNo: created.summary.orderNo }; report.orders.push(order); await save()
|
||||
}
|
||||
const path = `${root}/local-looseboard-orders/${order.orderNo}`, query = `?customerId=${report.customerId}`
|
||||
let d = await get(path + query)
|
||||
if (d.summary.status === 'PENDING_ACCEPTANCE') await write(`${label}-review`, 'POST', `${path}/review${query}`, { version: d.summary.version, decision: 'APPROVE', idempotencyKey: key(`${label}-review`), remark: report.marker })
|
||||
d = await get(path + query); order.transferNo = d.summary.transferNo; await save(); check(order.transferNo, `${order.orderNo}审核生成真实履约来源`)
|
||||
if (!remote && !order.receiving) await receive(order)
|
||||
}
|
||||
if (!report.dispatchNo) { report.dispatchNo = (await write('create-dispatch', 'POST', `${root}/dispatches`, { idempotencyKey: key('create-dispatch'), warehouseId: report.warehouseId, publishLooseboardDemand: true, cargo: [], batchAllocations: [], remark: `${report.marker} 仓内九箱+异地六箱` })).dispatchNo; await save() }
|
||||
const path = `${root}/dispatches/${report.dispatchNo}`, cp = `${root}/dispatches/capacity-demands/${report.dispatchNo}/cargo`
|
||||
if (!report.operations['add-cargo']?.result) {
|
||||
const selected = (await get(`${cp}?page=1&size=50`)).candidates.filter(row => report.orders.some(order => row.inboundCode === order.transferNo || row.orderNo === order.orderNo || row.sourceBusinessNo === order.orderNo))
|
||||
check(selected.length >= 6 && selected.every(row => !row.blockReasons.length), '六单均为正式可选来源')
|
||||
const sources = new Map(selected.flatMap(row => row.selectionGroup.sources).map(row => [row.sourceKey, row.selection]))
|
||||
await write('add-cargo', 'POST', cp, { recordVersion: (await get(path)).recordVersion, idempotencyKey: key('add-cargo'), cargo: [...sources.values()], batchAllocations: [] })
|
||||
}
|
||||
report.preparedDetail = await get(path); await save()
|
||||
check(Number(report.preparedDetail.counts.packageCount) === 15, '混排十五箱:仓内九箱、异地六箱')
|
||||
check(report.preparedDetail.orderDetails.length === 6, '关联订单按六张散板原单汇总')
|
||||
if (!report.fleet) {
|
||||
const context = { marker: report.marker, options: {}, createdResources: report.createdResources, checkpoint: save,
|
||||
request: async (method, url, body, token) => { const r = await raw(method, url, body, token || admin); return { http: r.http, body: r } },
|
||||
requestMultipart: async (method, url, file) => { const f = new FormData(); f.append('file', new Blob([file.content], { type: file.type }), file.name); const r = await raw(method, url, f); return { http: r.http, body: r } },
|
||||
apiSuccess: (module, label, response) => { check(response.http === 200 && String(response.body.code) === '200', `${label}: ${response.body.message}`); return response.body.data }, assert: (module, label, response, passed) => check(passed, label) }
|
||||
report.fleet = await prepareFleetTrip(context, 'local-mixed-looseboard', report.dispatchNo, report.warehouseId); await save()
|
||||
}
|
||||
}
|
||||
/** PDA只拣仓内九箱;异地六箱不进入拣货校验和仓内出库。 */
|
||||
async function pickAndLoad() {
|
||||
const path = `${pr}/dispatches/${report.dispatchNo}`
|
||||
let task = await get(path, pda); report.pdaBefore ||= task; await save()
|
||||
check(Number(task.counts.packageCount) === 9 && task.items.every(row => row.positions?.length), 'PDA只要求仓内九箱')
|
||||
for (const initial of task.items) {
|
||||
task = await get(path, pda); let item = task.items.find(row => row.dispatchItemId === initial.dispatchItemId)
|
||||
for (const position of item.positions) {
|
||||
const packages = Math.min(Number(position.availablePackages), Number(item.scheduledPackages) - Number(item.pickedPackages))
|
||||
if (packages <= 0) continue
|
||||
const name = `pick-${item.dispatchItemId}-${position.positionId}`
|
||||
const prep = await write(`${name}-rent`, 'POST', `${pr}/storage-rent/preparations`, { mode: 'DISPATCH', businessNo: report.dispatchNo, itemId: item.dispatchItemId, positionId: position.positionId, packages, productQuantity: packages, idempotencyKey: key(name) }, pda)
|
||||
check(prep.ready === true, '本轮新入库货物仓租预校验就绪')
|
||||
await write(name, 'POST', `${path}/items/${item.dispatchItemId}/pick`, { recordVersion: task.recordVersion, positionId: position.positionId, packages, productQuantity: packages, idempotencyKey: key(name), remark: report.marker }, pda)
|
||||
task = await get(path, pda); item = task.items.find(row => row.dispatchItemId === initial.dispatchItemId)
|
||||
}
|
||||
}
|
||||
task = await get(path, pda); report.pdaPicked = task; await save(); check(task.pickingReconciled && Number(task.counts.pickedPackageCount) === 9, '九箱实际拣货核平')
|
||||
if (task.stage === 'LOADING') { const loading = await get(`${path}/loading`, pda); await write('loading-complete', 'POST', `${path}/loading-complete`, { recordVersion: loading.recordVersion, idempotencyKey: key('loading-complete'), manifestHash: loading.manifestHash, remark: report.marker }, pda) }
|
||||
report.afterLoading = await get(`${root}/dispatches/${report.dispatchNo}`); await save(); check(report.afterLoading.stage === 'DELIVERING', '仓内装车完成立即发运,不等待异地取货')
|
||||
}
|
||||
/** 上门取货三单各存一组取货/送达照片,分托不增加资料块。 */
|
||||
async function remoteDocuments() {
|
||||
const path = `${root}/dispatches/${report.dispatchNo}`
|
||||
let docs = await get(`${path}/loading-documents`)
|
||||
for (const slot of ['LOADING_SCENE', 'LOADING_LIST', 'PRE_DEPARTURE_CHECK', 'DRIVER_LICENSE', 'RECEIPT']) {
|
||||
if (docs.photos.some(row => row.slot === slot)) continue
|
||||
const name = `loading-photo-${slot}`, uploadKey = key(`${name}-upload`)
|
||||
const f = await photo(name, `${path}/loading-documents/${slot}/uploads?reviewRound=${docs.reviewRound}&idempotencyKey=${uploadKey}`)
|
||||
await write(`${name}-bind`, 'PUT', `${path}/loading-documents/${slot}`, { recordVersion: docs.recordVersion, idempotencyKey: key(`${name}-bind`), fileId: f.fileId, uploadKey })
|
||||
docs = await get(`${path}/loading-documents`)
|
||||
}
|
||||
report.loadingDocuments = docs; check(docs.complete, '五类装车资料补齐')
|
||||
let execution = await get(`${path}/looseboard-execution`)
|
||||
report.looseboardBefore ||= execution; await save()
|
||||
const items = execution.items.filter(row => row.pickupRequired)
|
||||
check(items.length === 3 && new Set(items.map(row => row.orderNo)).size === 3, '上门取货按三张订单展示资料,六个计划托不拆六块')
|
||||
for (const phase of ['PICKUP', 'ARRIVAL']) {
|
||||
if (phase === 'ARRIVAL') {
|
||||
execution = await get(`${path}/looseboard-execution`)
|
||||
if (!execution.pickupCompleted) await write('pickup-complete', 'POST', `${path}/looseboard-execution/pickup-complete`, { recordVersion: execution.recordVersion, idempotencyKey: key('pickup-complete') })
|
||||
}
|
||||
for (const initial of items) {
|
||||
execution = await get(`${path}/looseboard-execution`)
|
||||
const row = execution.items.find(item => item.itemId === initial.itemId), photos = phase === 'PICKUP' ? row.pickupPhotos : row.arrivalPhotos
|
||||
if (photos.length) continue
|
||||
const name = `${initial.orderNo}-${phase}`, f = await photo(`${name}-upload`, `${path}/looseboard-execution/${initial.itemId}/${phase}/uploads`)
|
||||
await write(`${name}-save`, 'PUT', `${path}/looseboard-execution/${initial.itemId}/${phase}`, { recordVersion: execution.recordVersion, idempotencyKey: key(`${name}-save`), fileIds: [f.fileId] })
|
||||
}
|
||||
}
|
||||
execution = await get(`${path}/looseboard-execution`)
|
||||
if (!execution.pickupCompleted) await write('pickup-complete', 'POST', `${path}/looseboard-execution/pickup-complete`, { recordVersion: execution.recordVersion, idempotencyKey: key('pickup-complete') })
|
||||
report.looseboardAfter = await get(`${path}/looseboard-execution`); await save()
|
||||
check(report.looseboardAfter.pickupCompleted && report.looseboardAfter.items.filter(row => row.pickupRequired).every(row => row.pickupPhotos.length && row.arrivalPhotos.length), '三单上门取货和送达资料全部完成')
|
||||
}
|
||||
/** 一次选择同派送的两张原订单,生成两张异常并沿PDA分批回仓实收。 */
|
||||
async function exceptionReturns() {
|
||||
const path = `${root}/dispatches/${report.dispatchNo}`, query = `?customerId=${report.customerId}`
|
||||
if (!report.operations['multi-order-return']?.result) {
|
||||
const candidates = await get(`${path}/exception-return-options`)
|
||||
report.returnOptions = candidates; await save()
|
||||
const originals = report.orders.filter(row => row.mode === 'DELIVERY_TO_WAREHOUSE').slice(0, 2)
|
||||
const selected = originals.map(order => candidates.find(row => row.lastMileOrderNo === order.transferNo || row.lastMileOrderNo === order.orderNo))
|
||||
check(selected.every(Boolean) && selected[0].deliveryNo === selected[1].deliveryNo, '同一派送单下两张原单均可选择')
|
||||
const returns = selected.map(row => ({ deliveryNo: row.deliveryNo, lastMileOrderId: row.lastMileOrderId,
|
||||
items: [{ outboundPalletItemId: row.sources[0].outboundPalletItemId, inventoryPositionId: row.sources[0].inventoryPositionId,
|
||||
returnCartonId: row.sources[0].returnCartonId, packageCount: 1 }] }))
|
||||
await write('multi-order-return', 'POST', `${path}/exceptions`, { recordVersion: (await get(path)).recordVersion, idempotencyKey: key('multi-order-return'),
|
||||
exceptionType: 'PARTIAL_RECEIPT', returnDirection: 'OVERSEAS_WAREHOUSE', description: `${report.marker} 两单各退一箱,其余签收`, imageFileIds: [], returnSelections: returns })
|
||||
}
|
||||
const rows = (await get(`${root}/exception-orders?page=1&size=100&keyword=${encodeURIComponent(report.marker)}`)).records.filter(row => row.reason?.includes(report.marker))
|
||||
check(rows.length === 2, '一次多选实际生成两张独立异常')
|
||||
report.exceptions ||= []
|
||||
for (const row of rows) {
|
||||
const name = row.exceptionNo, ep = `${root}/exception-orders/${name}`, pp = `${pr}/return-receipts/exceptions/${name}`
|
||||
let detail = await get(ep + query)
|
||||
if (!detail.returnOrderNo) await write(`${name}-generate`, 'POST', `${ep}/generate-return-inbound${query}`, { expectedVersion: detail.order.version, idempotencyKey: key(`${name}-generate`), handlingAction: '合成测试退回海外仓' })
|
||||
detail = await get(ep + query)
|
||||
const tails = (await get(`${root}/orders?page=1&size=50&orderNo=${detail.returnOrderNo}`)).records.filter(item => item.orderNo === detail.returnOrderNo)
|
||||
check(tails.length === 1, `${name}唯一关联退回尾程单`)
|
||||
const tail = tails[0]
|
||||
if (!report.operations[`${name}-plan`]?.result) await write(`${name}-plan`, 'POST', `${root}/exception-orders/return-inbound-plans`, { expectedArrivalAt: '2026-11-06T10:00:00',
|
||||
plan: { customerId: report.customerId, orderId: tail.id, expectedOrderVersion: tail.version, palletCount: 1, palletType: 'STANDARD', assigned: false, mergeRequired: false, urgent: false, remark: report.marker } })
|
||||
if (detail.order.status !== 'COMPLETED') {
|
||||
const tasks = await get(`${pr}/return-receipts/exceptions`, pda); check(tasks.some(task => task.exceptionNo === name), `${name}出现在PDA回仓任务`)
|
||||
const started = await write(`${name}-start`, 'POST', `${pp}/start`, {}, pda)
|
||||
const original = report.orders.find(order => order.transferNo === row.lastMileOrderNo)
|
||||
// 原出库托盘可能已报废;回仓另建实际可用托盘,沿用原仓有效虚拟库位。
|
||||
const sourcePallet = original.receiving.pallets[0]
|
||||
const resourcePath = `/api/platform/v1/overseas-warehouse-resources/${report.warehouseId}`
|
||||
const targetId = await write(`${name}-target-pallet`, 'POST', `${resourcePath}/pallets`, {
|
||||
palletCode: `${report.marker}-${name}`, palletType: 'STANDARD', capacityCbm: 2, unitPrice: 0,
|
||||
status: 'IDLE', locationId: sourcePallet.warehouseLocationId, remark: `${report.marker} 异常回仓验收托盘` })
|
||||
const target = await get(`${resourcePath}/pallets/${targetId}`)
|
||||
check(target.status !== 'SCRAPPED' && target.locationId === sourcePallet.warehouseLocationId, `${name}使用当前仓真实有效托盘`)
|
||||
const f = await photo(`${name}-evidence`, `${pp}/evidence`, pda)
|
||||
await write(`${name}-receive`, 'POST', `${pp}/receive`, { expectedVersion: started.order.version, idempotencyKey: key(`${name}-receive`),
|
||||
palletCode: target.palletCode, locationCode: sourcePallet.warehouseLocationCode, evidenceFileIds: [f.fileId],
|
||||
items: started.returnSources.map(source => ({ outboundPalletItemId: source.outboundPalletItemId, inventoryPositionId: source.inventoryPositionId, returnCartonId: source.returnCartonId, packages: Number(source.maxReturnPackages) })) }, pda)
|
||||
}
|
||||
detail = await get(ep + query)
|
||||
check(detail.order.status === 'COMPLETED' && detail.returnReceipts.reduce((sum, receipt) => sum + Number(receipt.receivedPackages), 0) === 1, `${name}PDA实收一箱且异常完结`)
|
||||
const returnedTail = (await get(`${root}/orders?page=1&size=50&orderNo=${detail.returnOrderNo}`)).records.find(item => item.orderNo === detail.returnOrderNo)
|
||||
check(returnedTail.currentStatus === 'INBOUND_COMPLETED' && Number(returnedTail.receivedPackageCount) === 1, `${name}退回尾程已入库签收`)
|
||||
const receiptCommand = report.operations[`${name}-receive`]
|
||||
const replay = await raw(receiptCommand.method, receiptCommand.path, receiptCommand.body, pda)
|
||||
check(String(replay.code) === '200' && replay.data.replayed, `${name}实收同键重放不重复入账`)
|
||||
const inventoryPath = `${root}/inventory`
|
||||
const ledger = await get(`${inventoryPath}/ledger${query}&size=100&businessType=RETURN_STOCK_IN&businessOrderNo=${name}`)
|
||||
const balances = await get(`${inventoryPath}/balances${query}&size=100&inboundCode=${row.inboundCode}`)
|
||||
check(ledger.records.reduce((sum, entry) => sum + Number(entry.deltaPackages), 0) === 1, `${name}退回库存流水仅增加一箱`)
|
||||
check(balances.records.reduce((sum, balance) => sum + Number(balance.availablePackages), 0) === 1, `${name}原单实际可用库存恢复一箱`)
|
||||
report.inventoryVerification ||= {}; report.inventoryVerification[name] = { ledger, balances, receiptReplay: replay.data }
|
||||
report.exceptions = [...report.exceptions.filter(item => item.order.exceptionNo !== name), detail]; await save()
|
||||
}
|
||||
await finishDelivery(7)
|
||||
}
|
||||
/** 签收只记录剩余实际交付量;异常车核平实签实退后通过正式结案入口释放资源。 */
|
||||
async function finishDelivery(expectedSigned) {
|
||||
const path = `${root}/dispatches/${report.dispatchNo}`, query = `?customerId=${report.customerId}`
|
||||
const delivery = report.afterLoading.documents.deliveries[0], dp = `${root}/delivery-orders/${delivery.deliveryNo}`
|
||||
let documents = await get(`${dp}/documents${query}`)
|
||||
if (!report.operations['delivery-pod-save']?.result) {
|
||||
const f = await photo('delivery-pod', `${dp}/documents/RECEIPT/files${query}`)
|
||||
await write('delivery-pod-save', 'PUT', `${dp}/documents${query}`, { expectedVersion: documents.version, idempotencyKey: key('delivery-pod-save'), bolNo: `${report.marker}-TEST-BOL`,
|
||||
documents: documents.categories.map(category => ({ category: category.category, fileIds: category.category === 'RECEIPT' ? [f.fileId] : category.files.map(file => file.fileId) })) })
|
||||
}
|
||||
report.deliveryFinal = await get(dp + query)
|
||||
check(report.deliveryFinal.order.status === 'DELIVERED' && Number(report.deliveryFinal.order.signedPackages) === expectedSigned, `仓内实际签收${expectedSigned}箱凭签收文件完结`)
|
||||
if (scenario === 'normal') {
|
||||
for (let attempt = 0; attempt < 15; attempt++) {
|
||||
report.finalDispatch = await get(path)
|
||||
if (report.finalDispatch.stage === 'DELIVERED') break
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
check(report.finalDispatch.stage === 'DELIVERED', '全正常混排自动进入派送完成')
|
||||
}
|
||||
report.finalDispatch = await get(path); report.finalTrip = await get(`${root}/fleet/trips/${report.fleet.tripNo}/completion`); await save()
|
||||
if (scenario === 'multi-return' && report.finalTrip.canComplete) {
|
||||
const previous = report.operations['exception-trip-complete']
|
||||
if (previous?.code !== 'A0403') {
|
||||
try {
|
||||
await write('exception-trip-complete', 'POST', `${root}/fleet/trips/${report.fleet.tripNo}/complete`, {
|
||||
recordVersion: report.finalTrip.recordVersion, idempotencyKey: key('exception-trip-complete'), reason: `${report.marker} 核实七箱签收两箱实际回仓后结案` })
|
||||
} catch (error) { if (report.operations['exception-trip-complete']?.code !== 'A0403') throw error }
|
||||
}
|
||||
}
|
||||
report.tripDetail = await get(`${root}/fleet/trips/${report.fleet.tripNo}`)
|
||||
if (report.operations['exception-trip-complete']?.code === 'A0403') {
|
||||
report.pendingAction = { action: 'EXCEPTION_TRIP_CLOSE', tripNo: report.fleet.tripNo, reason: '当前验收账号缺少platform:fleet:trip:complete,保留原权限及车次事实,待有权限账号结案。' }
|
||||
check(report.finalTrip.canComplete && report.finalTrip.outcomes.every(row => row.remainingPackages === 0), '异常车实签实退核平,可由有权限账号结案')
|
||||
} else check(report.tripDetail.trip.status === 'COMPLETED' && report.tripDetail.trip.vehicleRelease && report.tripDetail.trip.driverRelease, '车次结案并释放车辆及司机')
|
||||
report.harnessCorrections ||= []
|
||||
for (const previous of report.checks.filter(row => row.label === '仓内签收和上门取货送达后整车完结')) {
|
||||
report.harnessCorrections.push({ ...previous, reason: '现有排车规则仅原实发全部正常签收推进DELIVERED;部分签收及实退通过车次正式结案,不能把实退伪作签收。' })
|
||||
}
|
||||
report.checks = report.checks.filter(row => row.label !== '仓内签收和上门取货送达后整车完结')
|
||||
}
|
||||
try {
|
||||
admin = await login(process.env.ADMIN_TEST_USERNAME, process.env.ADMIN_TEST_PASSWORD)
|
||||
pda = await login(process.env.MIXED_PDA_USERNAME, process.env.MIXED_PDA_PASSWORD || process.env.LOCAL_TRANSFER_PDA_PASSWORD)
|
||||
await prepare()
|
||||
if (process.env.MIXED_ACCEPTANCE_PHASE !== 'prepare') {
|
||||
if (!report.afterLoading) await pickAndLoad()
|
||||
await remoteDocuments()
|
||||
if (scenario === 'multi-return') await exceptionReturns()
|
||||
else await finishDelivery(9)
|
||||
}
|
||||
report.phase = process.env.MIXED_ACCEPTANCE_PHASE === 'prepare' ? 'PREPARED' : report.pendingAction ? 'COMPLETED_WITH_PENDING_ACTION' : 'COMPLETED'; delete report.error; await save()
|
||||
console.log(JSON.stringify({ phase: report.phase, dispatchNo: report.dispatchNo, orders: report.orders.map(({ orderNo, mode }) => ({ orderNo, mode })), output }))
|
||||
} catch (error) { report.error = error.message; await save(); console.error(error.message); process.exitCode = 1 }
|
||||
Reference in New Issue
Block a user