Files
baiyue-firstleg/scripts/admin-test-env-modules/us-truck-fulfillment.mjs
T

122 lines
8.3 KiB
JavaScript

import { mkdir, writeFile } from 'node:fs/promises'
const MODULE = 'phase4-overseas-business'
const OPTIONS = { scenario: 'us-truck-fulfillment', deploymentBlocker: true }
const version = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) ? Number(value) : NaN
/** 沿用本轮独立卡车原单,以正式履约命令完成实收、分配和运输。 */
export async function verifyUsTruckFulfillment(context, proof, { checkpoint = context.checkpoint } = {}) {
if (!proof || proof.marker !== context.marker || !proof.orderNo || !proof.customerId
|| context.createdResources?.phase4OverseasBusiness?.truck !== proof || proof.fulfillment
|| proof.finalStatus !== 'WAITING_INBOUND') throw Error('卡车履约必须使用本轮未执行的待入仓原单')
if (!/^[A-Za-z0-9_-]{1,100}$/.test(context.marker)) throw Error('卡车批次标识不合法')
const evidence = proof.fulfillment = { status: 'RUNNING', requests: [] }
let initialized = false
const directory = new URL('../../reports/us-truck-checkpoints/', import.meta.url)
// 总编排提供检查点时沿用其持久化边界;否则独占创建批次文件,拒绝覆盖旧执行。
const persist = checkpoint ? () => checkpoint.call(context) : 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 base = `/api/platform/v1/last-leg/us-truck-orders/${encodeURIComponent(proof.orderNo)}`
const suffix = `?customerId=${encodeURIComponent(proof.customerId)}`
const check = (name, ok) => {
context.assert(MODULE, name, null, Boolean(ok), `orderNo=${proof.orderNo}`, OPTIONS)
if (!ok) throw Error(name)
}
const request = async (method, action, body, expectedRejection = false) => {
const log = { method, action, body, expectedRejection, status: 'PREPARED' }
evidence.requests.push(log)
await persist()
log.status = 'SENDING'
const response = await context.request(method, `${base}${action}${suffix}`, body)
Object.assign(log, { http: response?.http, code: response?.body?.code,
requestId: response?.requestId, traceId: response?.traceId })
const ok = response?.http === 200 && String(response?.body?.code) === '200'
log.status = ok ? 'SUCCEEDED' : !response?.http || response.http >= 500 || !response.body ? 'UNKNOWN_RESULT' : 'REJECTED'
await persist()
if (expectedRejection) {
check('卡车无效命令按预期错误码拒绝且保留诊断', [200, expectedRejection].includes(response?.http)
&& [String(expectedRejection), `A0${expectedRejection}`].includes(String(response?.body?.code)) && Boolean(response.requestId) && Boolean(response.traceId))
return response.body.data
}
context.apiSuccess(MODULE, `本土卡车${method} ${action || '详情'}`, response,
`HTTP=${response?.http}; requestId=${response?.requestId}; traceId=${response?.traceId}`, OPTIONS)
if (!ok) throw Error(`卡车履约请求失败,不自动重发: ${action}`)
return response.body.data
}
const identity = detail => check('卡车原单客户和版本有效', detail?.summary?.orderNo === proof.orderNo
&& String(detail.summary.customerId) === String(proof.customerId)
&& Number.isSafeInteger(version(detail.summary.version)))
try {
let current = await request('GET', '')
identity(current)
check('卡车实收前仍为本轮待入仓单', current.summary.status === 'WAITING_INBOUND'
&& current.remark?.startsWith(context.marker) && Number(current.summary.receivedPalletCount) === 0)
const packages = Number(current.summary.packageCount)
check('卡车实收箱数来自原订单且为有效正整数', Number.isSafeInteger(packages) && packages > 0)
const packageCounts = packages >= 2 ? [1, packages - 1] : [packages]
const palletCount = packageCounts.length
const stages = [
['receive-pallets', 'WAITING_CONSOLIDATION'],
...(palletCount === 2 ? [['allocate-pallets', 'PARTIALLY_ALLOCATED', 0], ['allocate-pallets', 'ALLOCATED', 1]] : [['allocate-pallets', 'ALLOCATED', 0]]),
['prepare-dispatch', 'PENDING_DISPATCH'], ['dispatch', 'DISPATCHED'],
['start-transit', 'IN_TRANSIT'], ['complete', 'COMPLETED'],
]
let pallets
const allocatedIds = new Set()
for (const [action, state, allocationIndex] of stages) {
if (action === 'allocate-pallets' && allocationIndex === 1) {
await request('POST', '/prepare-dispatch', {
version: version(current.summary.version), idempotencyKey: `${context.marker}-truck-prepare-dispatch-partial`, remark: context.marker,
}, 409)
const unchanged = await request('GET', '')
check('卡车部分分配禁止待派车且原单完全不变', JSON.stringify(unchanged) === JSON.stringify(current))
}
const beforeVersion = version(current.summary.version)
const body = { version: beforeVersion, idempotencyKey: `${context.marker}-truck-${action}`, remark: context.marker }
if (action === 'receive-pallets') Object.assign(body, { actualPalletCount: palletCount, packageCount: packages, palletPackageCounts: packageCounts })
if (action === 'allocate-pallets') {
body.palletIds = [pallets[allocationIndex].id]
body.idempotencyKey += `-${allocationIndex}`
}
// 拒绝分支使用同一原单与独立键,回查整个聚合证明未偷偷收托或分配。
if (action === 'receive-pallets' || action === 'allocate-pallets' && allocationIndex === 0) {
const invalid = { ...body, idempotencyKey: `${body.idempotencyKey}-invalid`,
...(action === 'receive-pallets' ? { palletPackageCounts: packageCounts.map((n, i) => n + (i === 0 ? 1 : 0)) } : { palletIds: [body.palletIds[0], body.palletIds[0]] }) }
await request('POST', `/${action}`, invalid, 400)
const unchanged = await request('GET', '')
check(`卡车${action}拒绝后数量状态历史版本均不变`, JSON.stringify(unchanged) === JSON.stringify(current))
}
const next = await request('POST', `/${action}`, body)
identity(next)
check(`卡车${action}状态和版本正确`, next.summary.status === state && version(next.summary.version) === beforeVersion + 1)
check('卡车原单实收托盘及箱数守恒', next.pallets?.length === palletCount && Number(next.summary.receivedPalletCount) === palletCount
&& new Set(next.pallets.map(row => String(row.id))).size === palletCount
&& next.pallets.every((row, index) => Number(row.packageCount) === packageCounts[index]
&& /^[1-9]\d*$/.test(String(row.id)) && typeof row.palletNo === 'string' && row.palletNo.startsWith(`${proof.orderNo}-P`)))
pallets ??= next.pallets.map(row => ({ id: String(row.id), palletNo: row.palletNo, packageCount: Number(row.packageCount) }))
if (action === 'allocate-pallets') allocatedIds.add(String(body.palletIds[0]))
check('卡车履约复用原实收托盘且只分配本次选择', next.pallets.every((row, index) => String(row.id) === pallets[index].id
&& row.palletNo === pallets[index].palletNo && row.status === (allocatedIds.has(String(row.id)) ? 'ALLOCATED' : 'RECEIVED'))
&& Number(next.summary.allocatedPalletCount) === allocatedIds.size)
check('卡车履约写入对应操作历史', next.histories?.some(row => row.idempotencyKey === body.idempotencyKey
&& row.toStatus === state && row.fromStatus === current.summary.status))
const replay = await request('POST', `/${action}`, body)
check('卡车同键重放不重复收托或推进版本', JSON.stringify(replay) === JSON.stringify(next))
current = await request('GET', '')
check('卡车履约状态已持久化且回查一致', JSON.stringify(current) === JSON.stringify(next))
}
Object.assign(proof, { finalStatus: 'COMPLETED', version: current.summary.version, retained: true })
Object.assign(evidence, { status: 'HTTP_VERIFIED', pallets, packages, partialAllocationVerified: palletCount === 2 })
await persist()
} catch (error) {
for (const log of evidence.requests) if (log.status === 'SENDING') log.status = 'UNKNOWN_RESULT'
evidence.status = 'FAILED'
evidence.failure = error.message
try { await persist() } catch (checkpointError) { evidence.checkpointFailure = checkpointError.message }
throw error
}
}