test(firstleg): cover phase3 container loading flow

This commit is contained in:
2026-08-08 21:34:39 +08:00
parent 2dae8b2029
commit da85e30f33
8 changed files with 644 additions and 15 deletions
@@ -0,0 +1,248 @@
const MODULE = 'phase3-container-loading'
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers'
const SMART = '/api/platform/v1/first-leg/smart-loading'
const PDA = '/api/platform/v1/first-leg/pda-loading'
const FIRST_LEG = '/api/platform/v1/first-leg'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function failure(response) {
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|| (response?.http === 200 && codeOf(response) !== '200')
}
function records(page) { return Array.isArray(page?.records) ? page.records : [] }
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method,
headers: {
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers,
},
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method, path, http: response.status, body: payload,
raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
}
}
function fixture(context) {
return context.createdResources?.orchestrationFixtures?.phase3Container
|| context.createdResources?.inboundMeasurement
|| context.createdResources?.orders
}
export async function run(context) {
const source = fixture(context)
const tenantId = id(source?.tenantId)
const inboundId = id(source?.inboundId)
if (!tenantId || !inboundId || source?.marker !== context.marker) {
context.addStep(MODULE, '取得独立柜次级 INBOUNDED fixture', null, false,
`fixture marker=${source?.marker || 'missing'}, current marker=${context.marker}`, OPTIONS)
return
}
const inboundResponse = await context.request('GET',
`/api/platform/inbounds/${inboundId}?tenantId=${encodeURIComponent(tenantId)}`)
const inbound = context.apiSuccess(MODULE, '读取柜次级独立入库单', inboundResponse,
`tenantId=${tenantId}, inboundId=${inboundId}`, OPTIONS)
const warehouseId = id(inbound?.summary?.warehouseId)
context.assert(MODULE, '柜次候选入库单状态、测量和仓库门禁有效', inboundResponse,
inbound?.summary?.processStatus === 'INBOUNDED' && inbound?.summary?.measureReviewStatus === 'APPROVED'
&& Boolean(warehouseId),
`status=${inbound?.summary?.processStatus || 'missing'}, review=${inbound?.summary?.measureReviewStatus || 'missing'}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
if (!warehouseId) return
const candidateResponse = await context.request('GET', `${SMART}/candidates?warehouseId=${encodeURIComponent(warehouseId)}`)
const candidates = context.apiSuccess(MODULE, '查询智能装柜真实候选货物', candidateResponse, '', OPTIONS)
const candidate = Array.isArray(candidates)
? candidates.find((item) => id(item.inboundId) === inboundId && Number(item.availablePackageCount) > 0)
: null
context.assert(MODULE, '候选货物归属本轮入库单且剩余箱数有效', candidateResponse,
Boolean(candidate?.inboundDetailId) && id(candidate?.warehouseId) === warehouseId
&& id(candidate?.accountId) === tenantId,
`candidateFound=${Boolean(candidate)}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
if (!candidate?.inboundDetailId) return
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
const containerNo = `P3C-${suffix}`.slice(0, 64)
const cabinetNo = `P3I-${suffix}`.slice(0, 64)
const containerPayload = {
containerNo, cabinetNo, warehouseId, warehouseName: inbound?.summary?.warehouseName || null,
billOfLadingNo: `BL-${suffix}`, vesselName: `P3-VESSEL-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64),
containerType: '40HQ', shippingCompanyId: null, shippingCompanyName: `P3-SHIPPING-${suffix}`.slice(0, 128),
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
destinationWarehouseCode: candidate.destinationWarehouseCode || null,
destinationWarehouseName: candidate.destinationWarehouseName || null,
capacityCbm: 100, maxWeightKg: 100000, terminalTruckingCompany: `P3-TRUCK-${suffix}`.slice(0, 128),
loadingAt: '2026-09-02T08:00:00', estimatedSailingAt: '2026-09-03T08:00:00',
estimatedPortArrivalAt: '2026-09-20T08:00:00', estimatedWarehouseArrivalAt: '2026-09-21T08:00:00',
}
const createContainerResponse = await request(context, 'POST', CONTAINERS, containerPayload)
const container = context.apiSuccess(MODULE, '创建 marker 隔离货柜', createContainerResponse,
`containerNo=${containerNo}`, OPTIONS)
const containerId = id(container?.id)
context.assert(MODULE, '货柜归属仓库、路线、版本和编号正确', createContainerResponse,
Boolean(containerId) && container?.containerNo === containerNo && container?.cabinetNo === cabinetNo
&& id(container?.warehouseId) === warehouseId && Number(container?.version) === 0,
`containerId=${containerId || 'missing'}, version=${container?.version ?? 'missing'}`, OPTIONS)
if (!containerId) return
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
const containerDetail = context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS)
context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse,
containerDetail?.destinationWarehouseCode === (candidate.destinationWarehouseCode || null)
&& containerDetail?.planStatus === 'EDITING',
`planStatus=${containerDetail?.planStatus || 'missing'}`, OPTIONS)
const planResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
const plan = context.apiSuccess(MODULE, '创建智能装柜计划', planResponse, '', OPTIONS)
const planId = id(plan?.id)
context.assert(MODULE, '装柜计划与货柜关联且处于 EDITING', planResponse,
Boolean(planId) && id(plan?.bookingId) === containerId && plan?.status === 'EDITING'
&& Number(plan?.version) === 0 && Array.isArray(plan?.items) && plan.items.length === 0,
`planId=${planId || 'missing'}, status=${plan?.status || 'missing'}`, OPTIONS)
if (!planId) return
const planReplayResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: 'replay' })
const planReplay = context.apiSuccess(MODULE, '重放货柜装柜计划创建', planReplayResponse, '', OPTIONS)
context.assert(MODULE, '同一货柜重放返回同一计划', planReplayResponse,
id(planReplay?.id) === planId, `first=${planId}, replay=${id(planReplay?.id) || 'missing'}`, OPTIONS)
const assignPayload = {
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(plan.version), operationId: `${context.marker}-container-assign`.slice(0, 128),
}
const assignResponse = await request(context, 'POST', `${SMART}/plans/${planId}/assignments`, assignPayload)
const assigned = context.apiSuccess(MODULE, '人工分配本轮候选货物', assignResponse, '', OPTIONS)
const planItem = assigned?.items?.find?.((item) => id(item.inboundId) === inboundId)
context.assert(MODULE, '分配明细归属、箱数和计划版本正确', assignResponse,
Boolean(planItem?.id) && Number(planItem?.packageCount) === 1 && Number(assigned?.version) === 1,
`itemId=${id(planItem?.id) || 'missing'}, version=${assigned?.version ?? 'missing'}`, OPTIONS)
if (!planItem?.id) return
const assignReplayResponse = await request(context, 'POST', `${SMART}/plans/${planId}/assignments`, assignPayload)
const assignReplay = context.apiSuccess(MODULE, '分配 operationId 幂等重放', assignReplayResponse, '', OPTIONS)
context.assert(MODULE, '重放不重复增加装柜箱数', assignReplayResponse,
assignReplay?.items?.length === 1 && Number(assignReplay.items[0]?.packageCount) === 1
&& Number(assignReplay?.version) === Number(assigned?.version),
`items=${assignReplay?.items?.length ?? 'n/a'}, version=${assignReplay?.version ?? 'missing'}`, OPTIONS)
const staleAssignResponse = await request(context, 'POST', `${SMART}/plans/${planId}/assignments`, {
...assignPayload, expectedVersion: 0, operationId: `${context.marker}-container-stale`.slice(0, 128),
})
context.addStep(MODULE, '旧计划版本分配返回冲突', staleAssignResponse,
staleAssignResponse.http === 200 && ['A0409', '409'].includes(codeOf(staleAssignResponse)),
`HTTP=${staleAssignResponse.http}, code=${codeOf(staleAssignResponse)}`, OPTIONS)
const pushKey = `${context.marker}-container-push`.slice(0, 128)
const pushResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey })
const pushed = context.apiSuccess(MODULE, '推送预装并创建 PDA 任务', pushResponse, '', OPTIONS)
context.assert(MODULE, '推送后计划进入 LOADING 且版本递增', pushResponse,
pushed?.status === 'LOADING' && Number(pushed?.version) === Number(assigned.version) + 1,
`status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS)
if (pushed?.status !== 'LOADING') return
const pushReplayResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey })
const pushReplay = context.apiSuccess(MODULE, '推送预装 Idempotency-Key 重放', pushReplayResponse, '', OPTIONS)
context.assert(MODULE, '推送重放保持同一 LOADING 计划', pushReplayResponse,
id(pushReplay?.id) === planId && pushReplay?.status === 'LOADING'
&& Number(pushReplay?.version) === Number(pushed.version),
`planId=${id(pushReplay?.id) || 'missing'}, version=${pushReplay?.version ?? 'missing'}`, OPTIONS)
const preloadPageResponse = await context.request('GET',
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`)
const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS)
context.assert(MODULE, '预装清单包含本轮 LOADING 计划', preloadPageResponse,
records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'LOADING'),
`records=${records(preloadPage).length}`, OPTIONS)
const taskResponse = await context.request('GET',
`${PDA}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const task = context.apiSuccess(MODULE, '查询本轮柜次 PDA 拣货任务', taskResponse, '', OPTIONS)
const taskId = id(task?.id)
const pickItem = task?.items?.find?.((item) => id(item.loadingPlanItemId) === id(planItem.id))
context.assert(MODULE, 'PDA 任务、计划明细和待拣箱数关联正确', taskResponse,
Boolean(taskId) && id(task?.loadingPlanId) === planId && task?.taskStatus === 'LOADING'
&& Boolean(pickItem?.id) && Number(pickItem?.plannedPackageCount) === 1,
`taskId=${taskId || 'missing'}, itemId=${id(pickItem?.id) || 'missing'}`, OPTIONS)
if (!taskId || !pickItem?.id) return
const wrongWarehouseResponse = await context.request('GET',
`${PDA}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=1`)
context.addStep(MODULE, 'PDA 任务仓库隔离拒绝', wrongWarehouseResponse,
failure(wrongWarehouseResponse), `HTTP=${wrongWarehouseResponse.http}, code=${codeOf(wrongWarehouseResponse)}`, OPTIONS)
const stalePickResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(pickItem.version) + 1,
operationId: `${context.marker}-pda-stale`.slice(0, 128),
})
context.addStep(MODULE, 'PDA 旧明细版本拣货返回冲突', stalePickResponse,
stalePickResponse.http === 200 && ['A0409', '409'].includes(codeOf(stalePickResponse)),
`HTTP=${stalePickResponse.http}, code=${codeOf(stalePickResponse)}`, OPTIONS)
const pickPayload = {
warehouseId, pickedPackageCount: 1, locationIds: [], version: Number(pickItem.version),
operationId: `${context.marker}-pda-pick`.slice(0, 128),
}
const pickResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, pickPayload)
const picked = context.apiSuccess(MODULE, '执行本轮 PDA 全量拣货', pickResponse, '', OPTIONS)
context.assert(MODULE, '最后一箱拣货后 PDA 任务进入 COMPLETED', pickResponse,
picked?.taskStatus === 'COMPLETED'
&& picked?.items?.every?.((item) => Number(item.pickedPackageCount) === Number(item.plannedPackageCount)),
`taskStatus=${picked?.taskStatus || 'missing'}`, OPTIONS)
const pickReplayResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/items/${id(pickItem.id)}/pick`, pickPayload)
const pickReplay = context.apiSuccess(MODULE, 'PDA operationId 幂等重放', pickReplayResponse, '', OPTIONS)
context.assert(MODULE, 'PDA 重放保持已完成任务和拣货数', pickReplayResponse,
pickReplay?.taskStatus === 'COMPLETED'
&& pickReplay?.items?.every?.((item) => Number(item.pickedPackageCount) === Number(item.plannedPackageCount)),
`taskStatus=${pickReplay?.taskStatus || 'missing'}`, OPTIONS)
const finalPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
const finalPlan = context.apiSuccess(MODULE, '回查柜次装柜计划终态', finalPlanResponse, '', OPTIONS)
context.assert(MODULE, '装柜计划进入 COMPLETED 且明细已冻结', finalPlanResponse,
finalPlan?.status === 'COMPLETED' && finalPlan?.items?.length === 1 && Boolean(finalPlan.items[0]?.frozenAt),
`status=${finalPlan?.status || 'missing'}, frozen=${Boolean(finalPlan?.items?.[0]?.frozenAt)}`, OPTIONS)
const confirmedPageResponse = await context.request('GET',
`${FIRST_LEG}/loading-lists?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`)
const confirmedPage = context.apiSuccess(MODULE, '查询完成装柜清单', confirmedPageResponse, '', OPTIONS)
context.assert(MODULE, '完成清单分页包含本轮计划', confirmedPageResponse,
records(confirmedPage).some((item) => id(item.planId) === planId && item.status === 'COMPLETED'),
`records=${records(confirmedPage).length}`, OPTIONS)
const confirmedDetailResponse = await context.request('GET',
`${FIRST_LEG}/loading-lists/${planId}?warehouseId=${encodeURIComponent(warehouseId)}`)
const confirmedDetail = context.apiSuccess(MODULE, '查询完成装柜清单详情', confirmedDetailResponse, '', OPTIONS)
context.assert(MODULE, '装柜清单明细、客户归属、冻结和 PDA 历史完整', confirmedDetailResponse,
id(confirmedDetail?.summary?.planId) === planId && confirmedDetail?.summary?.status === 'COMPLETED'
&& confirmedDetail?.items?.some?.((item) => id(item.inboundId) === inboundId
&& id(item.accountId) === tenantId && Boolean(item.frozenAt))
&& confirmedDetail?.histories?.some?.((item) => item.actionType === 'PDA_COMPLETE_LOADING'
&& Boolean(item.operatorName) && Boolean(item.occurredAt)),
`items=${confirmedDetail?.items?.length ?? 'n/a'}, histories=${confirmedDetail?.histories?.length ?? 'n/a'}`, OPTIONS)
const immutableResponse = await request(context, 'PATCH',
`${FIRST_LEG}/loading-plans/${planId}/transport?warehouseId=${encodeURIComponent(warehouseId)}`,
{ containerNo: `MUTATE-${suffix}`.slice(0, 64), voyageNo: 'MUTATE', expectedVersion: Number(finalPlan?.version), operationReason: context.marker },
{ 'Idempotency-Key': `${context.marker}-completed-mutate`.slice(0, 128) })
context.addStep(MODULE, '完成装柜后运输信息不可变', immutableResponse,
immutableResponse.http === 200 && ['A0409', '409'].includes(codeOf(immutableResponse)),
`HTTP=${immutableResponse.http}, code=${codeOf(immutableResponse)}`, OPTIONS)
context.createdResources.phase3ContainerLoading = {
marker: context.marker, tenantId, inboundId, warehouseId, containerBookingId: containerId,
loadingPlanId: planId, pdaTaskId: taskId, finalStatus: 'COMPLETED',
}
}
@@ -5,9 +5,31 @@ const TRUCK = '/api/platform/v1/truck-appointments'
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method,
headers: {
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers,
},
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method, path, http: response.status, body: payload,
raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
}
}
export async function run(context) {
const ocean = context.createdResources?.oceanBooking
const containerFlow = context.createdResources?.phase3ContainerLoading
const containerFixture = context.createdResources?.orchestrationFixtures?.phase3Container
if (!ocean?.id || ocean.finalStatus !== 'RELEASED') {
context.addStep(MODULE, '跨服务链路前置订舱终态可用', null, false,
'本轮海运订舱模块未返回 RELEASED 隔离记录,未伪造关联拖车预约', OPTIONS)
@@ -20,9 +42,39 @@ export async function run(context) {
const customerId = id(oceanDetail?.oceanBooking?.customerId)
context.assert(MODULE, '跨服务链路订舱客户归属有效', oceanResponse, Boolean(customerId), `customerId=${customerId || 'missing'}`, OPTIONS)
if (!customerId) return
const linkContainer = containerFlow?.finalStatus === 'COMPLETED'
&& id(containerFlow?.tenantId) === customerId
&& Boolean(containerFlow?.containerBookingId)
&& Boolean(containerFlow?.loadingPlanId)
let oceanVersion = Number(oceanDetail?.oceanBooking?.version)
if (containerFlow && !linkContainer) {
context.addStep(MODULE, '本轮柜次与订舱客户归属可串联', oceanResponse, false,
`oceanCustomerId=${customerId}, containerTenantId=${id(containerFlow?.tenantId) || 'missing'}, containerStatus=${containerFlow?.finalStatus || 'missing'}`, OPTIONS)
return
}
if (linkContainer) {
const requirement = oceanDetail?.containerRequirements?.find?.((item) => item?.containerType === '40HQ')
context.assert(MODULE, '订舱存在匹配本轮柜型的箱型需求', oceanResponse,
Boolean(requirement?.id), `requirementId=${id(requirement?.id) || 'missing'}`, OPTIONS)
if (!requirement?.id || !Number.isInteger(oceanVersion)) return
const allocationResponse = await request(context, 'POST', `${OCEAN}/${ocean.id}/allocations`, {
requirementId: id(requirement.id), containerBookingId: id(containerFlow.containerBookingId), expectedVersion: oceanVersion,
}, { 'Idempotency-Key': `${context.marker}-flow-container-allocation`.slice(0, 128) })
const allocated = context.apiSuccess(MODULE, '将本轮柜次分配至已放舱订舱', allocationResponse, '', OPTIONS)
oceanVersion = Number(allocated?.oceanBooking?.version)
context.assert(MODULE, '订舱分配保留柜次关联并递增版本', allocationResponse,
allocated?.containerAllocations?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId))
&& Number.isInteger(oceanVersion),
`containerBookingId=${id(containerFlow.containerBookingId)}, version=${oceanVersion}`, OPTIONS)
}
const payload = {
outboundOrderId: null, oceanBookingId: ocean.id, containerBookingId: null, loadingPlanId: null,
warehouseId: id(context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
outboundOrderId: linkContainer ? id(containerFixture?.orderId) || null : null,
oceanBookingId: ocean.id,
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
warehouseId: linkContainer
? id(containerFlow.warehouseId)
: id(context.createdResources?.warehouse?.id || context.context?.warehouseId || '193000000000000001'),
customerId, customerName: `服务中心链路客户-${suffix}`, carrierId: null, carrierName: `链路承运商-${suffix}`,
fleetContact: '链路车队联系人', fleetPhone: '13800000007', driverName: '链路司机', driverPhone: '13800000008',
driverIdCard: null, vehicleNo: `粤D-${suffix.slice(-6)}`, storeName: `链路门店-${suffix}`, storeContact: '链路联系人',
@@ -31,23 +83,40 @@ export async function run(context) {
plannedLoadingAt: '2026-09-03T08:00:00', transitMinutes: 60, payableAmount: 1, receivableAmount: 2,
currency: 'RMB', remark: context.marker, expectedVersion: 0,
}
if (linkContainer) {
const missingContainerResponse = await context.request('POST', TRUCK, {
...payload, containerBookingId: null, remark: `${context.marker}-missing-container`,
})
context.addStep(MODULE, '拖车关联装柜计划时缺失柜次被拒绝', missingContainerResponse,
missingContainerResponse.http === 200 && codeOf(missingContainerResponse) !== '200',
`HTTP=${missingContainerResponse.http}, code=${codeOf(missingContainerResponse)}`, OPTIONS)
}
const response = await context.request('POST', TRUCK, payload)
const appointment = context.apiSuccess(MODULE, '创建关联海运订舱的拖车预约', response, `oceanBookingId=${ocean.id}`, OPTIONS)
const appointmentId = id(appointment?.id)
context.assert(MODULE, '拖车预约关联订舱 ID、版本和审计字段完整', response,
context.assert(MODULE, '拖车预约关联订舱、柜次、装柜计划、版本和审计字段完整', response,
Boolean(appointmentId) && id(appointment?.oceanBookingId) === id(ocean.id) && Number.isFinite(Number(appointment?.version))
&& (!linkContainer || id(appointment?.containerBookingId) === id(containerFlow.containerBookingId)
&& id(appointment?.loadingPlanId) === id(containerFlow.loadingPlanId))
&& Boolean(appointment?.createdAt || appointment?.createdBy || appointment?.operatorName),
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, containerBookingId=${appointment?.containerBookingId || 'n/a'}, loadingPlanId=${appointment?.loadingPlanId || 'n/a'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
if (!appointmentId) return
context.createdResources.serviceCenterFlow = { appointmentId, oceanBookingId: ocean.id, marker: context.marker, finalStatus: 'CANCELLED' }
context.createdResources.serviceCenterFlow = {
appointmentId, oceanBookingId: ocean.id,
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
marker: context.marker, finalStatus: 'CANCELLED',
}
const action = await context.request('POST', `${TRUCK}/${appointmentId}/cancel`, { expectedVersion: Number(appointment.version), reason: context.marker })
const cancelled = context.apiSuccess(MODULE, '取消跨服务链路拖车预约', action, '', OPTIONS)
context.assert(MODULE, '跨服务链路安全终态为 CANCELLED', action, cancelled?.status === 'CANCELLED', `status=${cancelled?.status || 'missing'}`, OPTIONS)
const detail = await context.request('GET', `${TRUCK}/${appointmentId}`)
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
final?.status === 'CANCELLED' && id(final?.oceanBookingId) === id(ocean.id),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}`, OPTIONS)
final?.status === 'CANCELLED' && id(final?.oceanBookingId) === id(ocean.id)
&& (!linkContainer || id(final?.containerBookingId) === id(containerFlow.containerBookingId)
&& id(final?.loadingPlanId) === id(containerFlow.loadingPlanId)),
`status=${final?.status || 'missing'}, oceanBookingId=${final?.oceanBookingId || 'missing'}, containerBookingId=${final?.containerBookingId || 'n/a'}, loadingPlanId=${final?.loadingPlanId || 'n/a'}`, OPTIONS)
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
const history = context.apiSuccess(MODULE, '查询跨服务链路拖车预约状态历史', historyResponse, '', OPTIONS)