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) } async function actionRequest(context, path, endpoint, version) { try { const response = await fetch(`${context.options.gateway}${path}/${endpoint}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`, 'Content-Type': 'application/json; charset=utf-8', 'Idempotency-Key': `${context.marker}-flow-${endpoint}`.slice(0, 128), }, body: JSON.stringify({ expectedVersion: version, reason: context.marker }), 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: 'POST', path: `${path}/${endpoint}`, 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: 'POST', path: `${path}/${endpoint}`, http: 0, body: null, raw: String(error), requestId: null, traceId: null } } } async function uploadBillOfLading(context, appointmentId) { const path = `${TRUCK}/${appointmentId}/attachments/upload?attachmentType=BILL_OF_LADING` const form = new FormData() const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130]) form.append('file', new Blob([png], { type: 'image/png' }), `${context.marker}-bill.png`) const response = await fetch(`${context.options.gateway}${path}`, { method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` }, body: form, signal: AbortSignal.timeout(60_000), }) const body = await response.json() return { method: 'POST', path, http: response.status, body, requestId: body?.requestId || response.headers.get('x-request-id'), traceId: body?.traceId || response.headers.get('x-trace-id') } } export async function run(context) { const containerFlow = context.createdResources?.phase3Loading || context.createdResources?.phase3ContainerLoading const ocean = containerFlow?.oceanBookingId ? { id: id(containerFlow.oceanBookingId), finalStatus: 'RELEASED' } : context.createdResources?.oceanBooking const containerFixture = [ context.createdResources?.orders, context.createdResources?.orchestrationFixtures?.inboundMeasurement, context.createdResources?.orchestrationFixtures?.phase3Container, ].find((item) => id(item?.inboundId) === id(containerFlow?.inboundId)) if (!ocean?.id || ocean.finalStatus !== 'RELEASED') { context.addStep(MODULE, '跨服务链路前置订舱终态可用', null, false, '本轮海运订舱模块未返回 RELEASED 隔离记录,未伪造关联拖车预约', OPTIONS) return } const suffix = context.marker.replace(/\D/g, '').slice(-10) const oceanResponse = await context.request('GET', `${OCEAN}/${ocean.id}`) const oceanDetail = context.apiSuccess(MODULE, '读取跨服务链路已放舱订舱', oceanResponse, `oceanBookingId=${ocean.id}`, OPTIONS) const linkContainer = ['AWAITING_REVIEW', 'LOADED'].includes(containerFlow?.finalStatus) && Boolean(containerFlow?.containerBookingId) && Boolean(containerFlow?.loadingPlanId) if (!linkContainer) { context.addStep(MODULE, '本轮已放舱订舱必须关联真实柜和装柜计划', oceanResponse, false, `containerStatus=${containerFlow?.finalStatus || 'missing'}, containerBookingId=${id(containerFlow?.containerBookingId) || 'missing'}`, OPTIONS) return } const allocationMatches = oceanDetail?.containers?.some?.((item) => id(item?.containerBookingId) === id(containerFlow.containerBookingId)) context.assert(MODULE, '已放舱订舱保留本轮柜次分配', oceanResponse, allocationMatches, `containerBookingId=${id(containerFlow.containerBookingId)}, containers=${oceanDetail?.containers?.length ?? 'n/a'}`, OPTIONS) if (!allocationMatches) return const carrierResponse = await context.request('GET', '/api/platform/v1/suppliers?tenantId=0&category=TRUCKING&status=ACTIVE&page=1&size=20') const carrierPage = context.apiSuccess(MODULE, '读取 ACTIVE TRUCKING 承运商事实源', carrierResponse, '', OPTIONS) const carrier = carrierPage?.records?.find?.((item) => item?.id && item?.name) context.assert(MODULE, '跨服务拖车使用正式 ACTIVE TRUCKING 承运商', carrierResponse, Boolean(carrier?.id && carrier?.name), `carrierId=${id(carrier?.id) || 'missing'}`, OPTIONS) if (!carrier) return const payload = { 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'), carrierId: id(carrier.id), carrierName: carrier.name, 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, } 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, '拖车预约关联订舱、柜次、装柜计划、版本和审计字段完整', 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'}, containerBookingId=${appointment?.containerBookingId || 'n/a'}, loadingPlanId=${appointment?.loadingPlanId || 'n/a'}, version=${appointment?.version ?? 'missing'}`, OPTIONS) if (!appointmentId) return const path = `${TRUCK}/${appointmentId}` const billUpload = await uploadBillOfLading(context, appointmentId) const billAttachment = context.apiSuccess(MODULE, '跨服务拖车上传有效提单附件', billUpload, '', OPTIONS) if (!billAttachment?.id) return let version = Number(appointment.version) for (const [name, endpoint, status] of [ ['提交跨链路预约', 'submit', 'PENDING_DISPATCH'], ['派发跨链路拖车', 'dispatch', 'DISPATCHED'], ['跨链路开始提货', 'pickup', 'PICKUP_IN_PROGRESS'], ['跨链路拖车到仓', 'arrive', 'ARRIVED_WAREHOUSE'], ['跨链路开始装货', 'load', 'LOADING'], ['跨链路拖车离仓', 'depart', 'DEPARTED'], ['完成跨链路拖车', 'complete', 'COMPLETED'], ]) { const action = await actionRequest(context, path, endpoint, version) const changed = context.apiSuccess(MODULE, name, action, '', OPTIONS) context.assert(MODULE, `${name}状态正确`, action, changed?.status === status, `status=${changed?.status || 'missing'}, version=${changed?.version ?? 'missing'}`, OPTIONS) if (!changed) return version = Number(changed.version) } const detail = await context.request('GET', `${TRUCK}/${appointmentId}`) const final = context.apiSuccess(MODULE, '回查跨服务链路安全终态', detail, '', OPTIONS) context.assert(MODULE, '跨服务链路终态持久化且关联订舱未漂移', detail, final?.status === 'COMPLETED' && 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) const actions = Array.isArray(history) ? history.filter((item) => item?.action !== 'CREATE') : [] const expectedActions = ['SUBMIT', 'DISPATCH', 'PICKUP', 'ARRIVE', 'LOAD', 'DEPART', 'COMPLETE'] context.assert(MODULE, '跨服务链路历史包含完整状态链、操作者、时间和备注', historyResponse, expectedActions.every((action) => actions.some((item) => item.action === action && id(item.appointmentId) === appointmentId && Boolean(item.operatorId) && Boolean(item.operatorName) && Boolean(item.createdAt) && item.reason === context.marker)), `history=${history?.length ?? 'n/a'}, actions=${actions.map((item) => item.action).join(',')}`, OPTIONS) if (final?.status === 'COMPLETED') { context.createdResources.serviceCenterFlow = { appointmentId, oceanBookingId: ocean.id, containerBookingId: linkContainer ? id(containerFlow.containerBookingId) : null, loadingPlanId: linkContainer ? id(containerFlow.loadingPlanId) : null, marker: context.marker, finalStatus: 'COMPLETED', } } }