2026-08-08 15:25:46 +08:00
|
|
|
const MODULE = 'service-center-flow'
|
|
|
|
|
const OPTIONS = { scenario: 'service-center-cross-flow', deploymentBlocker: true }
|
|
|
|
|
const OCEAN = '/api/platform/v1/ocean-bookings'
|
|
|
|
|
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) }
|
2026-08-08 21:34:39 +08:00
|
|
|
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 }
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-08 15:25:46 +08:00
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
const ocean = context.createdResources?.oceanBooking
|
2026-08-08 21:34:39 +08:00
|
|
|
const containerFlow = context.createdResources?.phase3ContainerLoading
|
|
|
|
|
const containerFixture = context.createdResources?.orchestrationFixtures?.phase3Container
|
2026-08-08 15:25:46 +08:00
|
|
|
if (!ocean?.id || ocean.finalStatus !== 'RELEASED') {
|
|
|
|
|
context.addStep(MODULE, '跨服务链路前置订舱终态可用', null, false,
|
|
|
|
|
'本轮海运订舱模块未返回 RELEASED 隔离记录,未伪造关联拖车预约', OPTIONS)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const suffix = context.marker.replace(/\D/g, '').slice(-10)
|
2026-08-08 20:47:26 +08:00
|
|
|
const oceanResponse = await context.request('GET', `${OCEAN}/${ocean.id}`)
|
|
|
|
|
const oceanDetail = context.apiSuccess(MODULE, '读取跨服务链路订舱真实客户归属', oceanResponse, `oceanBookingId=${ocean.id}`, OPTIONS)
|
|
|
|
|
const customerId = id(oceanDetail?.oceanBooking?.customerId)
|
|
|
|
|
context.assert(MODULE, '跨服务链路订舱客户归属有效', oceanResponse, Boolean(customerId), `customerId=${customerId || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!customerId) return
|
2026-08-08 21:34:39 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-08 15:25:46 +08:00
|
|
|
const payload = {
|
2026-08-08 21:34:39 +08:00
|
|
|
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'),
|
2026-08-08 15:25:46 +08:00
|
|
|
customerId, customerName: `服务中心链路客户-${suffix}`, carrierId: null, carrierName: `链路承运商-${suffix}`,
|
|
|
|
|
fleetContact: '链路车队联系人', fleetPhone: '13800000007', driverName: '链路司机', driverPhone: '13800000008',
|
|
|
|
|
driverIdCard: null, vehicleNo: `粤D-${suffix.slice(-6)}`, storeName: `链路门店-${suffix}`, storeContact: '链路联系人',
|
|
|
|
|
storePhone: '13800000009', storeAddress: '深圳市南山区链路地址', storeCity: '深圳市', storeZipcode: '518000',
|
|
|
|
|
packageCount: 1, grossWeightKg: 1, volumeCbm: 0.1, palletCount: 0, poNo: `FLOW-PO-${suffix}`,
|
|
|
|
|
plannedLoadingAt: '2026-09-03T08:00:00', transitMinutes: 60, payableAmount: 1, receivableAmount: 2,
|
|
|
|
|
currency: 'RMB', remark: context.marker, expectedVersion: 0,
|
|
|
|
|
}
|
2026-08-08 21:34:39 +08:00
|
|
|
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)
|
|
|
|
|
}
|
2026-08-08 15:25:46 +08:00
|
|
|
const response = await context.request('POST', TRUCK, payload)
|
|
|
|
|
const appointment = context.apiSuccess(MODULE, '创建关联海运订舱的拖车预约', response, `oceanBookingId=${ocean.id}`, OPTIONS)
|
|
|
|
|
const appointmentId = id(appointment?.id)
|
2026-08-08 21:34:39 +08:00
|
|
|
context.assert(MODULE, '拖车预约关联订舱、柜次、装柜计划、版本和审计字段完整', response,
|
2026-08-08 15:25:46 +08:00
|
|
|
Boolean(appointmentId) && id(appointment?.oceanBookingId) === id(ocean.id) && Number.isFinite(Number(appointment?.version))
|
2026-08-08 21:34:39 +08:00
|
|
|
&& (!linkContainer || id(appointment?.containerBookingId) === id(containerFlow.containerBookingId)
|
|
|
|
|
&& id(appointment?.loadingPlanId) === id(containerFlow.loadingPlanId))
|
2026-08-08 15:25:46 +08:00
|
|
|
&& Boolean(appointment?.createdAt || appointment?.createdBy || appointment?.operatorName),
|
2026-08-08 21:34:39 +08:00
|
|
|
`appointmentId=${appointmentId || 'missing'}, oceanBookingId=${appointment?.oceanBookingId || 'missing'}, containerBookingId=${appointment?.containerBookingId || 'n/a'}, loadingPlanId=${appointment?.loadingPlanId || 'n/a'}, version=${appointment?.version ?? 'missing'}`, OPTIONS)
|
2026-08-08 15:25:46 +08:00
|
|
|
if (!appointmentId) return
|
2026-08-08 21:34:39 +08:00
|
|
|
context.createdResources.serviceCenterFlow = {
|
|
|
|
|
appointmentId, oceanBookingId: ocean.id,
|
|
|
|
|
containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null,
|
|
|
|
|
loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null,
|
|
|
|
|
marker: context.marker, finalStatus: 'CANCELLED',
|
|
|
|
|
}
|
2026-08-08 15:25:46 +08:00
|
|
|
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}`)
|
2026-08-08 20:36:23 +08:00
|
|
|
const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail,
|
2026-08-08 21:34:39 +08:00
|
|
|
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)
|
2026-08-08 20:36:23 +08:00
|
|
|
|
|
|
|
|
const historyResponse = await context.request('GET', `${TRUCK}/${appointmentId}/history`)
|
|
|
|
|
const history = context.apiSuccess(MODULE, '查询跨服务链路拖车预约状态历史', historyResponse, '', OPTIONS)
|
|
|
|
|
const cancel = Array.isArray(history) ? history.find((item) => item?.action === 'CANCEL') : null
|
|
|
|
|
context.assert(MODULE, '跨服务链路历史包含归属、状态、操作者、时间和备注', historyResponse,
|
|
|
|
|
Boolean(cancel) && id(cancel?.appointmentId) === appointmentId
|
|
|
|
|
&& cancel?.fromStatus === 'DRAFT' && cancel?.toStatus === 'CANCELLED'
|
|
|
|
|
&& Boolean(cancel?.operatorId) && Boolean(cancel?.operatorName) && Boolean(cancel?.createdAt)
|
|
|
|
|
&& cancel?.reason === context.marker,
|
|
|
|
|
`history=${history?.length ?? 'n/a'}, cancelFrom=${cancel?.fromStatus || 'missing'}, cancelTo=${cancel?.toStatus || 'missing'}`,
|
|
|
|
|
OPTIONS)
|
2026-08-08 15:25:46 +08:00
|
|
|
}
|