test(firstleg): cover booking-level truck lifecycle

This commit is contained in:
2026-09-01 16:36:41 +08:00
parent 9098d8dfdf
commit 0ba3df2fce
4 changed files with 247 additions and 196 deletions
@@ -1,18 +1,18 @@
import { ensureMatsonCarrierPartner } from './carrier-partner-fixture.mjs'
import { oceanBookingDraft } from './ocean-booking-fixture.mjs'
import { truckAppointmentFieldGaps } from './fulfillment-list-field-contract.mjs'
const MODULE = 'truck-appointments'
const OPTIONS = { scenario: 'truck-appointment-lifecycle', deploymentBlocker: true }
const OPTIONS = { scenario: 'booking-level-truck-appointment-lifecycle', deploymentBlocker: true }
const TRUCKS = '/api/platform/v1/truck-appointments'
const BOOKINGS = '/api/platform/v1/ocean-bookings'
const CONTAINERS = '/api/platform/v1/containers'
const FORBIDDEN_TRUCK_FIELDS = ['containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo', 'containers']
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function data(response) { return codeOf(response) === '200' ? response.body.data : null }
function failure(response) { return response?.http === 0 || response?.http >= 400 || (response?.http === 200 && codeOf(response) !== '200') }
function records(value) { return Array.isArray(value) ? value : value?.records || [] }
function hasForbiddenTruckFields(value) { return FORBIDDEN_TRUCK_FIELDS.some((field) => Object.hasOwn(value || {}, field)) }
export function minimalPdfBytes(label = 'truck-appointment-fixture') {
const safeLabel = String(label).replace(/[^\x20-\x7E]/g, '_').slice(0, 64)
@@ -63,8 +63,7 @@ export function inspectOceanBookingIdentity(marker, resource, detail = null) {
export function truckAppointmentPayload(context, suffix, identity, version = 0, overrides = {}) {
const supplier = identity.truckSupplier || context.createdResources?.truckSupplier || {}
return { outboundOrderId: null, oceanBookingId: identity.oceanBookingId,
containerBookingId: identity.containerBookingId, loadingPlanId: null, warehouseId: identity.warehouseId,
return { outboundOrderId: null, oceanBookingId: identity.oceanBookingId, warehouseId: identity.warehouseId,
carrierId: id(supplier.supplierId), carrierName: supplier.supplierName,
billOfLadingNo: identity.billOfLadingNo, fleetContact: '车队联系人', fleetPhone: '13800000004',
driverName: '司机验收', driverPhone: '13800000005', vehicleNo: `粤B-${suffix.slice(-6)}`,
@@ -72,7 +71,7 @@ export function truckAppointmentPayload(context, suffix, identity, version = 0,
storeAddress: '深圳市南山区验收地址', storeCity: '深圳', storeZipcode: '518000',
packageCount: 0, grossWeightKg: 0, volumeCbm: 0, palletCount: 0, poNo: `PO-${suffix}`,
plannedLoadingAt: '2026-12-31T10:00:00', transitMinutes: 0, payableAmount: 0,
currency: 'RMB', remark: `拖车平台业务-${suffix}`, expectedVersion: version, ...overrides }
currency: 'RMB', remark: `订舱级拖车-${suffix}`, expectedVersion: version, ...overrides }
}
async function activeSupplier(context, category, suffix) {
@@ -111,11 +110,7 @@ async function bookingMaster(context, suffix) {
if (!warehouseId) continue
const channelsResponse = await request(context, 'GET', `${BOOKINGS}/channels?originWarehouseId=${warehouseId}`)
const channelRows = records(data(channelsResponse)); const channel = channelRows[0]
if (channel?.channelCode) {
context.addStep(MODULE, '完整分页平台仓并解析有效海运渠道', channelsResponse, true,
`warehousePage=${page}, warehouseRecords=${warehouseRows.length}, channelRecords=${channelRows.length}, warehouseId=${warehouseId}`, OPTIONS)
return { ...customer, warehouseId, channelCode: channel.channelCode, channelName: channel.channelName }
}
if (channel?.channelCode) return { ...customer, warehouseId, channelCode: channel.channelCode, channelName: channel.channelName }
}
const total = Number(warehousePage?.total ?? warehouseRows.length)
if (warehouseRows.length === 0 || page * 100 >= total) break
@@ -125,13 +120,10 @@ async function bookingMaster(context, suffix) {
return null
}
function isoContainerNo(marker) {
const values = {}; let value = 10
for (let code = 65; code <= 90; code++) { while (value % 11 === 0) value++; values[String.fromCharCode(code)] = value++ }
const serial = String([...marker].reduce((sum, char) => (sum * 31 + char.charCodeAt(0)) >>> 0, 0) % 1_000_000).padStart(6, '0')
const base = `TSTU${serial}`; let sum = 0
;[...base].forEach((char, index) => { sum += (values[char] ?? Number(char)) * (2 ** index) })
return `${base}${(sum % 11) % 10}`
async function candidate(context, identity) {
const response = await request(context, 'GET', `${TRUCKS}/candidates?keyword=${encodeURIComponent(identity.oceanBookingNo)}&page=1&size=200`)
const page = data(response)
return { response, item: records(page).find((value) => id(value?.oceanBookingId) === identity.oceanBookingId) }
}
async function fixture(context) {
@@ -150,6 +142,12 @@ async function fixture(context) {
{ 'Idempotency-Key': `${context.marker}-truck-booking-create`.slice(0, 128) })
const created = context.apiSuccess(MODULE, '自包含创建 DRAFT 订舱', createdResponse, '', OPTIONS)
const bookingId = id(created?.oceanBooking?.id); if (!bookingId) return null
const identity = { suffix, oceanBookingId: bookingId, oceanBookingNo: created.oceanBooking.bookingNo,
warehouseId: master.warehouseId, billOfLadingNo: created.oceanBooking.billOfLadingNo, truckSupplier }
const draftCandidate = await candidate(context, identity)
context.assert(MODULE, 'DRAFT订舱不进入拖车候选', draftCandidate.response, !draftCandidate.item,
`oceanBookingNo=${identity.oceanBookingNo}`, OPTIONS)
const form = new FormData(); form.append('expectedVersion', String(created.oceanBooking.version))
form.append('documentType', 'BILL_OF_LADING')
form.append('file', new Blob([minimalPdfBytes(`${context.marker}-BILL_OF_LADING`)], { type: 'application/pdf' }), `bill-${suffix}.pdf`)
@@ -160,32 +158,36 @@ async function fixture(context) {
{ expectedVersion: Number(uploaded?.bookingVersion), reason: context.marker },
{ 'Idempotency-Key': `${context.marker}-truck-booking-release`.slice(0, 128) })
const released = context.apiSuccess(MODULE, '放舱自包含订舱', releaseResponse, '', OPTIONS)
const requirement = released?.containerRequirements?.[0]; if (released?.oceanBooking?.status !== 'RELEASED' || !requirement?.id) return null
const simulationResponse = await request(context, 'POST', CONTAINERS, { channelCode: master.channelCode,
warehouseId: master.warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000 })
const simulation = context.apiSuccess(MODULE, '创建自包含模拟柜', simulationResponse, '', OPTIONS)
const containerNo = isoContainerNo(context.marker); if (!simulation?.id) return null
const linkResponse = await request(context, 'POST', `${CONTAINERS}/${simulation.id}/ocean-booking-link`, {
oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo, expectedVersion: Number(simulation.version),
})
const linked = context.apiSuccess(MODULE, '绑定本轮真实 ISO 柜号', linkResponse, '', OPTIONS)
if (linked?.containerNo !== containerNo) return null
return { suffix, oceanBookingId: bookingId, containerBookingId: id(linked.id), warehouseId: master.warehouseId,
billOfLadingNo: released.oceanBooking.billOfLadingNo, truckSupplier, bookingVersion: released.oceanBooking.version }
if (released?.oceanBooking?.status !== 'RELEASED') return null
identity.billOfLadingNo = released.oceanBooking.billOfLadingNo
const releasedCandidate = await candidate(context, identity)
context.assert(MODULE, 'RELEASED且资料有效的订舱进入候选', releasedCandidate.response,
Boolean(releasedCandidate.item) && !hasForbiddenTruckFields(releasedCandidate.item),
`oceanBookingNo=${identity.oceanBookingNo}`, OPTIONS)
return identity
}
async function action(context, path, appointment, name, reason) {
const response = await request(context, 'POST', `${path}/${name.toLowerCase()}`,
{ expectedVersion: Number(appointment?.version), reason },
{ 'Idempotency-Key': `${context.marker}-truck-${name.toLowerCase()}`.slice(0, 128) })
return context.apiSuccess(MODULE, `整单推进${name}`, response, '', OPTIONS)
}
export async function run(context) {
const identity = await fixture(context)
context.assert(MODULE, '拖车模块自包含前置链完整', null, Boolean(identity?.oceanBookingId && identity?.containerBookingId),
`oceanBookingId=${identity?.oceanBookingId || 'missing'}, containerBookingId=${identity?.containerBookingId || 'missing'}`, OPTIONS)
context.assert(MODULE, '订舱级拖车模块自包含前置链完整', null,
Boolean(identity?.oceanBookingId && identity?.oceanBookingNo && identity?.truckSupplier?.supplierId),
`oceanBookingNo=${identity?.oceanBookingNo || 'missing'}`, OPTIONS)
if (!identity) return
context.createdResources.truckSupplier = identity.truckSupplier
const base = truckAppointmentPayload(context, identity.suffix, identity)
context.assert(MODULE, '拖车请求不含客户字段', null,
!Object.hasOwn(base, 'customerId') && !Object.hasOwn(base, 'customerName'), Object.keys(base).join(','), OPTIONS)
context.assert(MODULE, '创建请求完全不含柜次协议字段', null,
!hasForbiddenTruckFields(base) && !Object.hasOwn(base, 'customerId') && !Object.hasOwn(base, 'customerName'),
Object.keys(base).join(','), OPTIONS)
for (const [label, payload] of [
['错误订舱拒绝创建', { ...base, oceanBookingId: (BigInt(identity.oceanBookingId) + 1n).toString() }],
['错误柜号归属拒绝创建', { ...base, containerBookingId: (BigInt(identity.containerBookingId) + 1n).toString() }],
['错误仓库拒绝创建', { ...base, warehouseId: (BigInt(identity.warehouseId) + 1n).toString() }],
['错误提单拒绝创建', { ...base, billOfLadingNo: `WRONG-${identity.suffix}` }],
['错误承运商拒绝创建', { ...base, carrierId: (BigInt(identity.truckSupplier.supplierId) + 1n).toString() }],
['非法拖车币种拒绝创建', { ...base, currency: 'CNY' }],
@@ -193,73 +195,89 @@ export async function run(context) {
const response = await request(context, 'POST', TRUCKS, payload)
context.addStep(MODULE, label, response, failure(response), `code=${codeOf(response)}`, OPTIONS)
}
const createdResponse = await request(context, 'POST', TRUCKS, base)
const appointment = context.apiSuccess(MODULE, '创建合法拖车预约', createdResponse, '', OPTIONS)
let appointment = context.apiSuccess(MODULE, '创建合法订舱级拖车预约', createdResponse, '', OPTIONS)
const appointmentId = id(appointment?.id); if (!appointmentId) return
context.assert(MODULE, 'RMB应付费用响应无客户及应收字段', createdResponse,
appointment?.currency === 'RMB' && Number(appointment?.payableAmount) === 0
&& !Object.hasOwn(appointment, 'receivableAmount')
&& !Object.hasOwn(appointment, 'customerId') && !Object.hasOwn(appointment, 'customerName'), '', OPTIONS)
const path = `${TRUCKS}/${appointmentId}`
context.assert(MODULE, '创建响应完全无柜次协议且费用正确', createdResponse,
!hasForbiddenTruckFields(appointment) && appointment?.currency === 'RMB' && Number(appointment?.payableAmount) === 0,
`appointmentNo=${appointment?.appointmentNo || 'missing'}`, OPTIONS)
const afterCreateCandidate = await candidate(context, identity)
context.assert(MODULE, '创建后订舱立即退出候选', afterCreateCandidate.response, !afterCreateCandidate.item,
`appointmentNo=${appointment.appointmentNo}`, OPTIONS)
const duplicateBefore = await request(context, 'POST', TRUCKS, base)
context.addStep(MODULE, '同订舱第二张非终态拖车单冲突', duplicateBefore, failure(duplicateBefore),
`code=${codeOf(duplicateBefore)}, appointmentNo=${appointment.appointmentNo}`, OPTIONS)
const unchangedResponse = await request(context, 'GET', path)
const unchanged = context.apiSuccess(MODULE, '重复创建冲突后回查零漂移', unchangedResponse, '', OPTIONS)
context.assert(MODULE, '重复创建未改变版本和状态', unchangedResponse,
Number(unchanged?.version) === Number(appointment.version) && unchanged?.status === 'DRAFT', '', OPTIONS)
const attachmentForm = new FormData(); attachmentForm.append('attachmentType', 'DISPATCH_ORDER')
attachmentForm.append('file', new Blob([minimalPdfBytes(`${context.marker}-DISPATCH_ORDER`)], { type: 'application/pdf' }),
`dispatch-order-${identity.suffix}.pdf`)
const uploadAttachmentResponse = await context.requestMultipart('POST', `${path}/attachments/upload`,
attachmentForm, context.token)
const uploadedAttachment = context.apiSuccess(MODULE, '上传并关联DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const uploadedAttachmentId = id(uploadedAttachment?.id); const uploadedFileId = id(uploadedAttachment?.fileId)
context.assert(MODULE, '拖车附件类型仅使用运营附件而非订舱提单', uploadAttachmentResponse,
Boolean(uploadedAttachmentId && uploadedFileId) && uploadedAttachment?.attachmentType === 'DISPATCH_ORDER',
`attachmentId=${uploadedAttachmentId || 'missing'}, fileId=${uploadedFileId || 'missing'}`, OPTIONS)
const deleteUploadedResponse = await request(context, 'DELETE', `${path}/attachments/${uploadedAttachmentId}`)
context.apiSuccess(MODULE, '删除首次上传的拖车附件引用', deleteUploadedResponse, '', OPTIONS)
const referenceResponse = await request(context, 'POST', `${path}/attachments`,
{ fileId: uploadedFileId, attachmentType: 'DISPATCH_ORDER' })
const referencedAttachment = context.apiSuccess(MODULE, '重新引用平台归属拖车附件', referenceResponse, '', OPTIONS)
const referencedAttachmentId = id(referencedAttachment?.id)
const attachment = context.apiSuccess(MODULE, '上传DISPATCH_ORDER拖车附件', uploadAttachmentResponse, '', OPTIONS)
const attachmentListResponse = await request(context, 'GET', `${path}/attachments`)
const attachmentList = context.apiSuccess(MODULE, '查询拖车运营附件列表', attachmentListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含重新引用的DISPATCH_ORDER', attachmentListResponse,
Array.isArray(attachmentList) && attachmentList.some((item) => id(item?.id) === referencedAttachmentId
&& item?.attachmentType === 'DISPATCH_ORDER'), `attachmentId=${referencedAttachmentId || 'missing'}`, OPTIONS)
const deleteReferencedResponse = await request(context, 'DELETE', `${path}/attachments/${referencedAttachmentId}`)
context.apiSuccess(MODULE, '删除重新引用的拖车附件', deleteReferencedResponse, '', OPTIONS)
const usdResponse = await request(context, 'PUT', path,
{ ...base, currency: 'USD', payableAmount: 12.34, expectedVersion: Number(appointment.version) })
const usdAppointment = context.apiSuccess(MODULE, '编辑为USD应付费用', usdResponse, '', OPTIONS)
context.assert(MODULE, 'USD应付费用保存且无应收字段', usdResponse,
usdAppointment?.currency === 'USD' && Number(usdAppointment?.payableAmount) === 12.34
&& !Object.hasOwn(usdAppointment || {}, 'receivableAmount'), '', OPTIONS)
const duplicate = await request(context, 'POST', TRUCKS, base)
context.addStep(MODULE, '同柜第二条非终态预约拒绝且零漂移', duplicate, failure(duplicate), `code=${codeOf(duplicate)}`, OPTIONS)
const attachmentList = context.apiSuccess(MODULE, '查询拖车附件列表', attachmentListResponse, '', OPTIONS)
context.assert(MODULE, '附件列表包含本轮运营附件', attachmentListResponse,
records(attachmentList).some((item) => id(item?.id) === id(attachment?.id) && item?.attachmentType === 'DISPATCH_ORDER'),
`attachmentId=${id(attachment?.id) || 'missing'}`, OPTIONS)
const editedResponse = await request(context, 'PUT', path,
truckAppointmentPayload(context, identity.suffix, identity, Number(appointment.version), { currency: 'USD', payableAmount: 12.34 }))
appointment = context.apiSuccess(MODULE, '编辑订舱级拖车为USD应付费用', editedResponse, '', OPTIONS)
context.assert(MODULE, '编辑响应费用正确且无柜次协议', editedResponse,
appointment?.currency === 'USD' && Number(appointment?.payableAmount) === 12.34 && !hasForbiddenTruckFields(appointment), '', OPTIONS)
const submitKey = `${context.marker}-truck-submit`.slice(0, 128)
const submitBody = { expectedVersion: Number(usdAppointment?.version), reason: '幂等提交' }
const first = await request(context, 'POST', `${path}/submit`, submitBody, { 'Idempotency-Key': submitKey })
const firstData = context.apiSuccess(MODULE, '提交拖车预约', first, '', OPTIONS)
const replay = await request(context, 'POST', `${path}/submit`, submitBody, { 'Idempotency-Key': submitKey })
const replayData = context.apiSuccess(MODULE, '同键同载荷重放', replay, '', OPTIONS)
context.assert(MODULE, '同键重放版本零漂移', replay, Number(firstData?.version) === Number(replayData?.version), '', OPTIONS)
const conflict = await request(context, 'POST', `${path}/submit`, { ...submitBody, reason: '异载荷' }, { 'Idempotency-Key': submitKey })
context.addStep(MODULE, '同键异载荷拒绝', conflict, failure(conflict), `code=${codeOf(conflict)}`, OPTIONS)
const dispatch = await request(context, 'POST', `${path}/dispatch`,
{ expectedVersion: Number(firstData?.version), reason: '派车' },
{ 'Idempotency-Key': `${context.marker}-truck-dispatch`.slice(0, 128) })
const dispatched = context.apiSuccess(MODULE, '有效提单后派车', dispatch, '', OPTIONS)
context.assert(MODULE, '派车命令返回状态身份与版本正确', dispatch,
dispatched?.status === 'DISPATCHED' && id(dispatched?.id) === appointmentId
&& Number(dispatched?.version) > Number(firstData?.version),
`id=${id(dispatched?.id) || 'missing'}, status=${dispatched?.status || 'missing'}, version=${dispatched?.version ?? 'missing'}`, OPTIONS)
const detailResponse = await request(context, 'GET', path); const detail = data(detailResponse)
context.apiSuccess(MODULE, '派车后回查拖车完整详情', detailResponse, '', OPTIONS)
const detailGaps = truckAppointmentFieldGaps(detail)
context.assert(MODULE, '详情字段完整且订舱柜号身份无客户字段漂移', detailResponse,
detail?.status === 'DISPATCHED' && detailGaps.length === 0
&& !Object.hasOwn(detail || {}, 'customerId') && !Object.hasOwn(detail || {}, 'customerName')
&& !Object.hasOwn(detail || {}, 'receivableAmount')
&& detail?.currency === 'USD' && Number(detail?.payableAmount) === 12.34
&& id(detail?.oceanBookingId) === identity.oceanBookingId
&& id(detail?.containerBookingId) === identity.containerBookingId,
`missing=${detailGaps.join(',') || 'none'}, currency=${detail?.currency || 'missing'}, payableAmount=${detail?.payableAmount ?? 'missing'}, oceanBookingId=${id(detail?.oceanBookingId) || 'missing'}, containerBookingId=${id(detail?.containerBookingId) || 'missing'}`, OPTIONS)
context.createdResources.truckAppointment = { id: appointmentId, marker: context.marker, finalStatus: 'DISPATCHED',
oceanBookingId: identity.oceanBookingId, containerBookingId: identity.containerBookingId }
const submitBody = { expectedVersion: Number(appointment.version), reason: '幂等提交' }
const submitResponse = await request(context, 'POST', `${path}/submit`, submitBody, { 'Idempotency-Key': submitKey })
appointment = context.apiSuccess(MODULE, '整单推进SUBMIT', submitResponse, '', OPTIONS)
const replayResponse = await request(context, 'POST', `${path}/submit`, submitBody, { 'Idempotency-Key': submitKey })
const replay = context.apiSuccess(MODULE, 'SUBMIT同键同载荷重放', replayResponse, '', OPTIONS)
context.assert(MODULE, 'SUBMIT重放版本零漂移', replayResponse,
Number(replay?.version) === Number(appointment.version) && replay?.status === appointment.status, '', OPTIONS)
const conflictResponse = await request(context, 'POST', `${path}/submit`, { ...submitBody, reason: '异载荷' },
{ 'Idempotency-Key': submitKey })
context.addStep(MODULE, 'SUBMIT同键异载荷A0409', conflictResponse,
codeOf(conflictResponse) === 'A0409', `code=${codeOf(conflictResponse)}`, OPTIONS)
for (const [name, reason] of [['DISPATCH', '派车'], ['PICKUP', '提柜'], ['ARRIVE', '到仓'],
['LOAD', '装货'], ['DEPART', '离仓'], ['COMPLETE', '完成']]) {
appointment = await action(context, path, appointment, name, reason)
context.assert(MODULE, `${name}响应无柜次协议`, null, Boolean(appointment) && !hasForbiddenTruckFields(appointment),
`status=${appointment?.status || 'missing'}`, OPTIONS)
}
const finalDetailResponse = await request(context, 'GET', path)
const finalDetail = context.apiSuccess(MODULE, '回查COMPLETED订舱级拖车详情', finalDetailResponse, '', OPTIONS)
context.assert(MODULE, '完整生命周期完成且详情无柜次协议', finalDetailResponse,
finalDetail?.status === 'COMPLETED' && !hasForbiddenTruckFields(finalDetail)
&& id(finalDetail?.oceanBookingId) === identity.oceanBookingId,
`appointmentNo=${finalDetail?.appointmentNo || 'missing'}, status=${finalDetail?.status || 'missing'}`, OPTIONS)
const afterCompleteCandidate = await candidate(context, identity)
context.assert(MODULE, 'COMPLETED后订舱恢复候选', afterCompleteCandidate.response, Boolean(afterCompleteCandidate.item),
`oceanBookingNo=${identity.oceanBookingNo}`, OPTIONS)
const rebuiltResponse = await request(context, 'POST', TRUCKS, base)
let rebuilt = context.apiSuccess(MODULE, 'COMPLETED后同订舱可新建下一张拖车单', rebuiltResponse, '', OPTIONS)
context.assert(MODULE, '重建拖车单保持订舱级响应', rebuiltResponse,
rebuilt?.status === 'DRAFT' && !hasForbiddenTruckFields(rebuilt)
&& id(rebuilt?.oceanBookingId) === identity.oceanBookingId,
`appointmentNo=${rebuilt?.appointmentNo || 'missing'}`, OPTIONS)
const cancelled = await action(context, `${TRUCKS}/${id(rebuilt?.id)}`, rebuilt, 'CANCEL', '取消测试单')
const afterCancelCandidate = await candidate(context, identity)
context.assert(MODULE, 'CANCELLED后订舱恢复候选', afterCancelCandidate.response,
cancelled?.status === 'CANCELLED' && Boolean(afterCancelCandidate.item), '', OPTIONS)
const afterCancelResponse = await request(context, 'POST', TRUCKS, base)
rebuilt = context.apiSuccess(MODULE, 'CANCELLED后同订舱可再次新建', afterCancelResponse, '', OPTIONS)
const excepted = await action(context, `${TRUCKS}/${id(rebuilt?.id)}`, rebuilt, 'EXCEPTION', '异常测试单')
const afterExceptionCandidate = await candidate(context, identity)
context.assert(MODULE, 'EXCEPTION后订舱恢复候选', afterExceptionCandidate.response,
excepted?.status === 'EXCEPTION' && Boolean(afterExceptionCandidate.item), '', OPTIONS)
const finalDraftResponse = await request(context, 'POST', TRUCKS, base)
const finalDraft = context.apiSuccess(MODULE, 'EXCEPTION后同订舱可再次新建', finalDraftResponse, '', OPTIONS)
context.createdResources.truckAppointment = { id: id(finalDraft?.id), appointmentNo: finalDraft?.appointmentNo,
marker: context.marker, finalStatus: finalDraft?.status, oceanBookingId: identity.oceanBookingId }
}