284 lines
22 KiB
JavaScript
284 lines
22 KiB
JavaScript
import { fingerprint } from './prod-release-order-stage.mjs'
|
||
import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
|
||
|
||
const BASE = '/api/platform/v1/last-leg'
|
||
const PDA = '/api/pda/v1/last-leg'
|
||
const check = (ok, message) => { if (!ok) throw new Error(message) }
|
||
const id = value => value == null ? '' : String(value)
|
||
const query = (path, values) => `${path}?${new URLSearchParams(values)}`
|
||
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
|
||
|
||
/** Each successful command is persisted; an interrupted command must be investigated, never resent. */
|
||
export function overseasCommands(context, checkpoint) {
|
||
const { state, save } = checkpoint
|
||
const unwrap = response => {
|
||
check(response?.http === 200 && String(response?.body?.code) === '200',
|
||
`海外 HTTP=${response?.http}; code=${response?.body?.code}; requestId=${response?.body?.requestId}; traceId=${response?.body?.traceId}`)
|
||
return response.body.data
|
||
}
|
||
async function command(name, method, path, body, token = context.token, headers = {}, file = null) {
|
||
const binding = fingerprint({ method, path, body: body ?? null, headers, file })
|
||
const previous = state.commands?.[name]
|
||
check(!previous || previous.binding === binding, `海外命令 ${name} 与检查点不符`)
|
||
if (previous?.status === 'DONE') return previous.result
|
||
check(!previous, `海外命令 ${name} 结果未知;停止重发并保留检查点`)
|
||
const persist = async value => { const commands = { ...state.commands, [name]: value }; await save({ commands }); state.commands = commands }
|
||
await persist({ binding, status: 'INTENT' })
|
||
let response
|
||
if (file) {
|
||
const form = new FormData()
|
||
form.append('file', new Blob([PNG], { type: 'image/png' }), file)
|
||
response = await context.requestWithBody(method, path, form, token)
|
||
} else response = await context.request(method, path, body, token, headers)
|
||
const result = unwrap(response)
|
||
await persist({ binding, status: 'DONE', result })
|
||
return result
|
||
}
|
||
return { command, read: async (path, token = context.token) => unwrap(await context.request('GET', path, undefined, token)),
|
||
readCurrentReceivingSession: async token => {
|
||
const response = await context.request('GET', `${PDA}/receiving-containers/current`, undefined, token)
|
||
// Only the initial current-session probe treats the service's absent-session business code as empty.
|
||
// Subsequent mandatory session reads and every other resource remain strict.
|
||
if (response?.http === 200 && String(response?.body?.code) === 'A0404') return null
|
||
return unwrap(response)
|
||
},
|
||
lookup: async (path, body) => unwrap(await context.request('POST', path, body)) }
|
||
}
|
||
|
||
export function validateOverseasInput(input, domestic) {
|
||
check(/^[A-Za-z0-9][A-Za-z0-9_-]{7,47}$/.test(input.batch || ''), '无效验收批次')
|
||
check([1, 2].includes(input.packageCount), '海外仅支持1~2箱单票')
|
||
for (const key of ['customerId', 'warehouseId']) check(/^[1-9]\d*$/.test(input[key] || ''), `缺少 ${key}`)
|
||
for (const key of ['vehicleId', 'driverId']) check(/^[1-9]\d*$/.test(input.resources?.[key] || ''), `缺少既有 ${key}`)
|
||
check(Boolean(input.resources.carrierName), '缺少既有车队名称')
|
||
for (const key of ['containerBookingId', 'sourceInboundId', 'orderId']) check(/^[1-9]\d*$/.test(domestic[key] || ''), `缺少 ${key}`)
|
||
for (const key of ['containerNo', 'inboundCode', 'orderNo']) check(Boolean(domestic[key]), `缺少 ${key}`)
|
||
if (domestic.cartonMarks != null) check(Array.isArray(domestic.cartonMarks)
|
||
&& domestic.cartonMarks.length === input.packageCount && new Set(domestic.cartonMarks).size === input.packageCount,
|
||
'本票冻结箱码数量不符')
|
||
}
|
||
|
||
/** Normal single-ticket path only. No fixture runners, account creation, or master-data mutation. */
|
||
export async function runOverseasStage({ input, domestic, context, checkpoint }) {
|
||
validateOverseasInput(input, domestic)
|
||
check(context.overseasPdaToken && context.merchantToken, '缺少既有海外 PDA 或本客户会话')
|
||
const { state, save } = checkpoint
|
||
check(!Object.values(state.commands || {}).some(row => row.status !== 'DONE'), '海外命令结果未知;停止重发并保留检查点')
|
||
const { read, lookup, command, readCurrentReceivingSession } = overseasCommands(context, checkpoint)
|
||
const pdaToken = context.overseasPdaToken
|
||
const remark = `RELEASE-ACCEPTANCE:${input.batch}`
|
||
const key = name => `${input.batch}-${name}`
|
||
if (!state.schedule) {
|
||
const schedule = { plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(),
|
||
expectedDriverEndAt: new Date(Date.now() + 48 * 3600000).toISOString(),
|
||
arrival: new Date(Date.now() + 86400000).toISOString().slice(0, 19) }
|
||
await save({ schedule }); state.schedule = schedule
|
||
}
|
||
const schedule = state.schedule
|
||
const vehicle = await read(`${BASE}/master-data/vehicles/${input.resources.vehicleId}`)
|
||
const driver = await read(query(`${BASE}/fleet/drivers/${input.resources.driverId}`, { expectedEnd: schedule.expectedDriverEndAt }))
|
||
check(id(vehicle.id) === input.resources.vehicleId && vehicle.region === 'OVERSEAS' && vehicle.status === 'IN_SERVICE', '既有海外车辆不可用')
|
||
check(id(driver.id) === input.resources.driverId && driver.qualification?.canDispatch === true, '既有司机证照/审核不满足本次出车')
|
||
let sources
|
||
for (let attempt = 0; attempt < 60; attempt++) {
|
||
const page = await read(query(`${BASE}/orders`, { containerNo: domestic.containerNo, page: '1', size: '200' }))
|
||
sources = (page.records || []).filter(row => id(row.sourceContainerBookingId) === domestic.containerBookingId)
|
||
if (sources.length) break
|
||
if (attempt < 59) await (context.wait || (ms => new Promise(resolve => setTimeout(resolve, ms))))(1000)
|
||
}
|
||
check(sources?.length === 1 && id(sources[0].customerId) === input.customerId
|
||
&& id(sources[0].sourceInboundId) === domestic.sourceInboundId && sources[0].inboundCode === domestic.inboundCode
|
||
&& id(sources[0].overseasWarehouseId) === input.warehouseId, '柜内尾程来源不属于本次唯一客户/单票/接柜仓')
|
||
const tailOrderId = id(sources[0].id)
|
||
const arrivalRows = await lookup(`${BASE}/orders/container-arrival-times/query`, { sourceContainerBookingIds: [domestic.containerBookingId] })
|
||
const arrival = arrivalRows.find(row => id(row.sourceContainerBookingId) === domestic.containerBookingId)
|
||
check(arrival && Number.isSafeInteger(Number(arrival.version)), '柜级到仓版本缺失')
|
||
if (!arrival.estimatedWarehouseArrivalAt) await command('arrival', 'PUT', `${BASE}/orders/container-summaries/${domestic.containerBookingId}/expected-arrival`,
|
||
{ estimatedWarehouseArrivalAt: schedule.arrival, expectedVersion: Number(arrival.version), remark }, undefined, { 'Idempotency-Key': key('arrival') })
|
||
let plans = state.commands?.convert?.result
|
||
if (!plans) {
|
||
const preview = await read(query(`${BASE}/inbound-plans/container-preview`, { sourceContainerBookingId: domestic.containerBookingId }))
|
||
check(preview.ready === true && Number(preview.totalOrderCount) === 1 && Number(preview.pendingOrderCount) === 1, '本票计划不可转换或存在未登记计划')
|
||
plans = await command('convert', 'POST', `${BASE}/inbound-plans/convert-container`, {
|
||
sourceContainerBookingId: domestic.containerBookingId, snapshotVersion: Number(preview.snapshotVersion), snapshotHash: preview.snapshotHash,
|
||
}, undefined, { 'Idempotency-Key': key('convert') })
|
||
}
|
||
check(plans?.length === 1 && plans[0].lines?.length > 0 && plans[0].lines.every(line => line.inboundCode === domestic.inboundCode)
|
||
&& plans[0].pallets?.length === 1, '本票入仓计划/实体托盘不唯一')
|
||
const pallet = plans[0].pallets[0]
|
||
check(pallet.palletNo && pallet.warehousePalletId, '计划未生成实体托盘')
|
||
const receiving = `${PDA}/receiving-containers`
|
||
if (!state.commands?.receiveComplete) {
|
||
const current = await readCurrentReceivingSession(pdaToken)
|
||
check(!current || current.containerNo === domestic.containerNo && current.inbounds?.length === 1
|
||
&& current.inbounds[0].inboundCode === domestic.inboundCode, '海外 PDA 正在操作其他柜')
|
||
const detailPath = query(`${receiving}/${encodeURIComponent(domestic.containerNo)}`, { sourceContainerBookingId: domestic.containerBookingId })
|
||
const detail = await read(detailPath, pdaToken)
|
||
check(id(detail.overseasWarehouseId) === input.warehouseId && detail.inbounds?.length === 1
|
||
&& detail.inbounds[0].inboundCode === domestic.inboundCode && Number(detail.totalPackages) === input.packageCount, '收货柜身份或箱数不符')
|
||
const destinationType = detail.inbounds[0].destinationType
|
||
check(['OVERSEAS', 'FBA'].includes(destinationType), '单票不支持此收货类型')
|
||
if (destinationType === 'OVERSEAS') check(domestic.cartonMarks?.length === input.packageCount,
|
||
'OVERSEAS逐箱收货必须提供本票正式冻结箱码')
|
||
await command('prepare', 'POST', query(`${receiving}/${encodeURIComponent(domestic.containerNo)}/prepare`, { sourceContainerBookingId: domestic.containerBookingId }), { idempotencyKey: key('prepare') }, pdaToken)
|
||
const photos = []
|
||
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) {
|
||
const uploaded = await command(`photo-${role}`, 'POST', `${receiving}/current/unload-photos/files`, undefined, pdaToken, {}, `${input.batch}-SYNTHETIC-${role}.png`)
|
||
check(/^[1-9]\d*$/.test(id(uploaded.fileId)), '验收图片未返回文件ID')
|
||
photos.push({ role, fileId: id(uploaded.fileId) })
|
||
}
|
||
await command('sign', 'POST', `${receiving}/current/sign`, { photos, idempotencyKey: key('sign') }, pdaToken)
|
||
const resolved = await command('resolve-pallet', 'POST', `${receiving}/current/pallets/resolve`, { palletCode: pallet.palletNo, inboundCode: domestic.inboundCode }, pdaToken)
|
||
check(resolved.compatible === true, '计划托盘不兼容')
|
||
// FBA receives cumulative physical counts on its source pallet; carton resolution is not a receiving prerequisite.
|
||
for (const [index, cartonMark] of (destinationType === 'OVERSEAS' ? domestic.cartonMarks : []).entries()) {
|
||
const carton = await command(`resolve-carton-${index}`, 'POST', `${receiving}/current/cartons/resolve`, { cartonMark }, pdaToken)
|
||
check(carton.inboundCode === domestic.inboundCode, '箱码未归属本票')
|
||
await command(`receive-${index}`, 'POST', `${receiving}/current/receipts/carton`, { cartonMark, palletCode: pallet.palletNo, idempotencyKey: key(`receive-${index}`) }, pdaToken)
|
||
}
|
||
if (destinationType === 'FBA') await command('receive-bulk', 'POST', `${receiving}/current/receipts/bulk`, {
|
||
inboundCode: domestic.inboundCode, palletCode: pallet.palletNo, packageCount: input.packageCount, idempotencyKey: key('receive-bulk'),
|
||
}, pdaToken)
|
||
const received = await read(`${receiving}/current`, pdaToken)
|
||
check(Number(received.pendingPackages) === 0 && Number(received.totalPackages) === input.packageCount, '本票收货数量未核平')
|
||
await command('receiveComplete', 'POST', `${receiving}/current/complete`, { idempotencyKey: key('receive-complete') }, pdaToken)
|
||
}
|
||
if (!state.commands?.emptyReturn) {
|
||
const drayagePath = `/api/platform/v1/terminal-drayages/${domestic.containerBookingId}`
|
||
let drayage
|
||
for (let attempt = 0; attempt < 60; attempt++) {
|
||
drayage = await read(drayagePath)
|
||
if (drayage.status === 'UNLOADED' && drayage.unloadingCompletedAt) break
|
||
if (attempt < 59) await (context.wait || (ms => new Promise(resolve => setTimeout(resolve, ms))))(1000)
|
||
}
|
||
check(id(drayage.containerBookingId) === domestic.containerBookingId && drayage.status === 'UNLOADED'
|
||
&& drayage.allowedActions?.includes('RECORD_EMPTY_RETURN'), '海外卸柜事实尚未回传国内码头拖车')
|
||
const unloaded = Date.parse(`${drayage.unloadingCompletedAt}Z`)
|
||
check(Number.isFinite(unloaded), '卸柜时间无效')
|
||
const returned = await command('emptyReturn', 'POST', `${drayagePath}/empty-return`, {
|
||
expectedVersion: Number(drayage.version), actualAt: new Date(unloaded + 3600000).toISOString().slice(0, 19), remark,
|
||
}, undefined, { 'Idempotency-Key': key('empty-return') })
|
||
check(returned.status === 'EMPTY_RETURNED', '实际还柜未完成')
|
||
}
|
||
let dispatch = state.commands?.dispatch?.result
|
||
if (!dispatch) {
|
||
const candidates = await read(`${BASE}/dispatches/candidates?sourceStage=RECEIVED`)
|
||
const matches = candidates.filter(row => row.sourceStage === 'RECEIVED' && row.inboundCode === domestic.inboundCode
|
||
&& id(row.customerId) === input.customerId && id(row.overseasWarehouseId) === input.warehouseId)
|
||
check(matches.length === 1 && Number(matches[0].packageCount) === input.packageCount, '本票排车候选不唯一或数量不符')
|
||
dispatch = await command('dispatch', 'POST', `${BASE}/dispatches/pre-arrange`, { planPalletIds: matches.map(row => id(row.planPalletId)) })
|
||
}
|
||
const dispatchNo = dispatch.dispatchNo
|
||
check(Boolean(dispatchNo), '排车单号缺失')
|
||
const transport = { vehiclePlate: vehicle.plateNumber, driverName: driver.details?.name,
|
||
driverPhone: driver.details?.phone, carrierName: input.resources.carrierName, estimatedArrivalAt: schedule.arrival }
|
||
await command('dispatch-vehicle', 'PUT', `${BASE}/dispatches/${dispatchNo}`, transport)
|
||
const transfer = await command('transfer', 'POST', query('/api/merchant/v1/last-leg/delivery-orders', { dispatchNo }), {
|
||
idempotencyKey: key('transfer'), plannedDeliveryAt: schedule.arrival, deliveryMethod: 'TRUCK_PALLET',
|
||
waybillNo: key('WAYBILL'), carrierName: transport.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET',
|
||
vehicleType: 'TRUCK', vehicleSize: '15托', driverName: transport.driverName, driverPhone: transport.driverPhone,
|
||
vehiclePlate: transport.vehiclePlate, appointmentAccount: 'SELF', sellerName: null, remark,
|
||
fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE', currency: 'USD', amount: 10, remark }],
|
||
}, context.merchantToken)
|
||
check(transfer.deliveries?.length === 1 && transfer.outboundNo, '本票未形成唯一出库/派送单')
|
||
let trip = await command('trip', 'POST', `${BASE}/fleet/trips`, { warehouseId: input.warehouseId,
|
||
plannedDepartureAt: schedule.plannedDepartureAt, expectedDriverEndAt: schedule.expectedDriverEndAt,
|
||
stops: [{ name: '验收起运仓' }, { name: '待匹配本票目的地' }], remark, idempotencyKey: key('trip') })
|
||
const tripNo = trip.trip.tripNo, tripPath = `${BASE}/fleet/trips/${tripNo}`
|
||
trip = await command('trip-assign', 'POST', `${tripPath}/assignment`, { recordVersion: trip.trip.recordVersion,
|
||
vehicleId: input.resources.vehicleId, driverId: input.resources.driverId, reason: remark })
|
||
trip = await command('trip-bind', 'POST', `${tripPath}/dispatches/bind`, { recordVersion: trip.trip.recordVersion, dispatchNo, reason: remark })
|
||
let stops = state.stops
|
||
if (!stops) {
|
||
const loading = await read(`${tripPath}/loading`)
|
||
check(loading.cargo?.length > 0 && loading.cargo.every(row => row.destinationKey), '本票装载缺少真实目的地')
|
||
stops = [{ name: '验收起运仓' }, ...new Map(loading.cargo.map(row => [row.destinationKey, { name: row.destinationLabel, destinationKey: row.destinationKey }])).values()]
|
||
await save({ stops }); state.stops = stops
|
||
}
|
||
trip = await command('trip-plan', 'PUT', `${tripPath}/plan`, { recordVersion: trip.trip.recordVersion,
|
||
plannedDepartureAt: schedule.plannedDepartureAt, expectedDriverEndAt: schedule.expectedDriverEndAt, stops, reason: remark })
|
||
trip = await command('trip-review', 'POST', `${tripPath}/dispatches/${dispatchNo}/review`, { recordVersion: trip.trip.recordVersion, decision: 'APPROVED' })
|
||
const pdaDispatch = `${PDA}/dispatches/${dispatchNo}`
|
||
if (!state.commands?.pickComplete) {
|
||
const picking = await read(pdaDispatch, pdaToken)
|
||
const exactDispatch = await read(`${BASE}/dispatches/${dispatchNo}`)
|
||
check(exactDispatch.items?.length > 0 && exactDispatch.items.every(row => row.inboundCode === domestic.inboundCode)
|
||
&& picking.items?.length === exactDispatch.items.length
|
||
&& picking.items.every(row => exactDispatch.items.some(item => id(item.id) === id(row.dispatchItemId))), 'PDA拣货包含其他票')
|
||
for (const item of picking.items) {
|
||
let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages)
|
||
for (const position of item.positions || []) {
|
||
if (remaining <= 0) break
|
||
const packages = Math.min(remaining, Number(position.availablePackages))
|
||
if (!(packages > 0)) continue
|
||
await command(`pick-${item.dispatchItemId}-${position.id}`, 'POST', `${pdaDispatch}/items/${item.dispatchItemId}/pick`, {
|
||
positionId: id(position.id), packages, productQuantity: packages, idempotencyKey: key(`pick-${position.id}`), remark,
|
||
}, pdaToken)
|
||
remaining -= packages
|
||
}
|
||
check(remaining === 0, '本票库存不足以完成拣货')
|
||
}
|
||
await command('pickComplete', 'POST', `${pdaDispatch}/complete`, { idempotencyKey: key('pick-complete') }, pdaToken)
|
||
}
|
||
let loadingResult = state.commands?.loading?.result
|
||
if (!loadingResult) {
|
||
const loading = await read(`${tripPath}/loading`)
|
||
check(!loading.blockReason, '整车装载未就绪')
|
||
loadingResult = await command('loading', 'POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion,
|
||
manifestHash: loading.manifestHash, pallets: controlledPalletMeasurements(loading.cargo, stops), reason: `${remark} 合成验收测量` })
|
||
}
|
||
const reviewed = await command('loading-review', 'POST', `${tripPath}/loading-review`, { recordVersion: loadingResult.recordVersion, decision: 'APPROVED' })
|
||
if (!state.commands?.depart) {
|
||
const ready = await read(pdaDispatch, pdaToken)
|
||
check(ready.status === 'COMPLETED' && ready.canDepart === true, 'PDA发车门禁未通过')
|
||
}
|
||
await command('depart', 'POST', `${pdaDispatch}/depart`, { recordVersion: reviewed.trip.recordVersion, idempotencyKey: key('depart'), remark }, pdaToken)
|
||
const outbound = await read(`${BASE}/outbound-orders/${transfer.outboundNo}`)
|
||
check(outbound.outbound?.status === 'OUTBOUNDED' && outbound.pallets?.length > 0
|
||
&& outbound.pallets.every(row => row.inboundCode === domestic.inboundCode), '本票未真实完成出库')
|
||
const result = { ...domestic, batch: input.batch, packageCount: input.packageCount, warehouseId: input.warehouseId,
|
||
tailOrderId, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: transfer.deliveries[0].deliveryNo,
|
||
tripNo, status: 'OVERSEAS_OUTBOUNDED', fullFlowVerified: false, photos: 'SYNTHETIC_ACCEPTANCE_ONLY' }
|
||
await save({ result }); return result
|
||
}
|
||
|
||
/** Close only this ticket's trip after delivery; release its two resource claims without creating probe trips. */
|
||
export async function finishSingleOrderTrip({ input, overseas, context, checkpoint }) {
|
||
check(overseas?.tripNo && overseas?.dispatchNo && overseas?.deliveryNo, '缺少本票车次与派送身份')
|
||
check(!Object.values(checkpoint.state.commands || {}).some(row => row.status !== 'DONE'), '车次结案命令结果未知;停止重发')
|
||
const { read, command } = overseasCommands(context, checkpoint)
|
||
const path = `${BASE}/fleet/trips/${encodeURIComponent(overseas.tripNo)}`
|
||
const delivery = await read(query(`${BASE}/delivery-orders/${encodeURIComponent(overseas.deliveryNo)}`, { customerId: input.customerId }))
|
||
check(delivery.order?.status === 'DELIVERED' && id(delivery.order.customerId) === input.customerId
|
||
&& delivery.order.dispatchNo === overseas.dispatchNo, '本票尚未完成签收,不能结案车次')
|
||
const detail = await read(path)
|
||
check(detail.trip?.tripNo === overseas.tripNo && id(detail.trip.vehicleId) === input.resources.vehicleId
|
||
&& id(detail.trip.driverId) === input.resources.driverId && detail.dispatches?.length === 1
|
||
&& detail.dispatches[0].dispatchNo === overseas.dispatchNo, '车次含其他票或资源身份不符')
|
||
let completed = checkpoint.state.commands?.tripComplete?.result
|
||
if (!completed) {
|
||
const completion = await read(`${path}/completion`)
|
||
check(completion.canComplete === true && completion.outcomes?.length > 0, '车次结案条件未通过')
|
||
completed = await command('tripComplete', 'POST', `${path}/complete`, { recordVersion: completion.recordVersion,
|
||
idempotencyKey: `${input.batch}-trip-complete`, reason: '单票验收实际签收完成' })
|
||
}
|
||
check(completed.trip?.status === 'COMPLETED' && completed.trip.completedAt, '车次货物未结案')
|
||
const endLocation = detail.trip.stops?.at(-1)?.name
|
||
check(Boolean(endLocation), '本车次缺少结束地点')
|
||
const vehicle = await command('releaseVehicle', 'POST', `${path}/resources/release`, {
|
||
recordVersion: completed.trip.recordVersion, resourceKind: 'VEHICLE', actualEndAt: completed.trip.completedAt,
|
||
endLocation, vehicleStatus: 'IN_SERVICE', idempotencyKey: `${input.batch}-vehicle-release`,
|
||
reason: '单票受控验收车辆在签收末站结束,无返程安排',
|
||
})
|
||
const driver = await command('releaseDriver', 'POST', `${path}/resources/release`, {
|
||
recordVersion: vehicle.trip.recordVersion, resourceKind: 'DRIVER', actualEndAt: completed.trip.completedAt,
|
||
endLocation, vehicleStatus: null, idempotencyKey: `${input.batch}-driver-release`,
|
||
reason: '单票受控验收司机在签收末站结束,无返程安排',
|
||
})
|
||
check(driver.trip?.vehicleRelease && driver.trip?.driverRelease, '本车次车辆或司机未释放')
|
||
const final = await read(path)
|
||
check(final.trip?.status === 'COMPLETED' && final.trip.vehicleRelease && final.trip.driverRelease, '车次结案回查未通过')
|
||
const result = { tripNo: overseas.tripNo, status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' }
|
||
await checkpoint.save({ result }); return result
|
||
}
|