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