const MODULE = 'manual-state-ocean-trucking' const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBlocker: true } const OCEAN_BASE = '/api/platform/v1/ocean-bookings' const TRUCK_BASE = '/api/platform/v1/truck-appointments' const OCEAN_LIST_STATUSES = ['DRAFT', 'RELEASED'] const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES] const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE', 'LOADING', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED'] function id(value) { return value == null ? '' : String(value) } function code(response) { return String(response?.body?.code ?? 'missing') } function data(response) { return response?.http === 200 && code(response) === '200' ? response.body.data : null } function key(context, value) { return `${context.marker}-manual-${value}-${crypto.randomUUID()}`.slice(0, 128) } function suffix(context, index) { return `${context.marker.replace(/\D/g, '').slice(-8)}${String(index).padStart(2, '0')}` } async function post(context, path, body, operation) { return context.request('POST', path, body, context.token, { 'Idempotency-Key': key(context, operation) }) } async function uploadDispatchOrder(context, appointmentId, index) { const path = `${TRUCK_BASE}/${appointmentId}/attachments/upload?attachmentType=DISPATCH_ORDER` 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}-truck-${index}.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') } } function oceanPayload(context, template, index) { const source = template.oceanBooking return { expectedVersion: 0, data: { customerId: source.customerId, customerName: source.customerName, channelCode: source.channelCode, warehouseId: source.warehouseId, billOfLadingNo: `BL-${suffix(context, index)}`, vesselName: `人工轮-${suffix(context, index)}`, voyageNo: `V${suffix(context, index)}`, shippingCompanyId: source.shippingCompanyId, shippingCompanyName: source.shippingCompanyId ? source.shippingCompanyName : null, bookingAgentId: source.bookingAgentId, bookingAgentName: source.bookingAgentId ? source.bookingAgentName : null, departurePortCode: source.departurePortCode, departurePortName: source.departurePortName, destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName, placeOfDelivery: source.placeOfDelivery, estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00', remark: `${context.marker} 人工列表状态 ${index}`, parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) => ({ partyType, partyName, address, contactName, contactPhone })), containerRequirements: template.containerRequirements.map(({ containerType, containerKind, quantity, capacityCbm, maxWeightKg }) => ({ containerType, containerKind, quantity, capacityCbm, maxWeightKg })), } } } async function createOcean(context, template, index) { const response = await post(context, OCEAN_BASE, oceanPayload(context, template, index), `ocean-create-${index}`) const value = context.apiSuccess(MODULE, `创建订舱状态样本 ${index}`, response, '', OPTIONS) context.assert(MODULE, `订舱样本 ${index} 创建为 DRAFT 且无真实柜`, response, Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'DRAFT' && (!value?.containers || value.containers.length === 0), `id=${id(value?.oceanBooking?.id) || 'missing'}, status=${value?.oceanBooking?.status || 'missing'}`, OPTIONS) if (!value?.oceanBooking?.id || value?.oceanBooking?.status !== 'DRAFT') { throw new Error(`创建订舱样本 ${index} 未进入 DRAFT`) } return value } async function oceanAction(context, value, endpoint, body = {}) { const response = await post(context, `${OCEAN_BASE}/${id(value.oceanBooking.id)}/${endpoint}`, { expectedVersion: Number(value.oceanBooking.version), reason: `${context.marker}-${endpoint}`, ...body }, `ocean-${endpoint}`) const next = context.apiSuccess(MODULE, `订舱推进 ${endpoint}`, response, `id=${id(value.oceanBooking.id)}`, OPTIONS) if (!next?.oceanBooking?.id) throw new Error(`订舱 ${id(value.oceanBooking.id)} 推进 ${endpoint} 失败`) return next } async function uploadBookingConfirmation(context, value, index) { const bookingId = id(value?.oceanBooking?.id) const form = new FormData() form.append('expectedVersion', String(Number(value?.oceanBooking?.version))) form.append('documentType', 'BOOKING_CONFIRMATION') form.append('file', new Blob([`${context.marker}-BOOKING_CONFIRMATION-${index}`], { type: 'text/plain' }), `booking-confirmation-${suffix(context, index)}.txt`) const response = await context.requestMultipart('POST', `${OCEAN_BASE}/${bookingId}/documents/upload`, form, context.token, { 'Idempotency-Key': key(context, `ocean-booking-document-${index}`) }) const document = context.apiSuccess(MODULE, `订舱样本 ${index} 上传订舱确认文件`, response, '', OPTIONS) if (!Number.isInteger(Number(document?.bookingVersion))) { throw new Error(`订舱样本 ${index} 未返回有效文件版本`) } return { ...value, oceanBooking: { ...value.oceanBooking, version: Number(document.bookingVersion) } } } async function buildOcean(context, template, status, index) { let value = await createOcean(context, template, index) if (status === 'DRAFT') return value if (status === 'RELEASED') { value = await uploadBookingConfirmation(context, value, index) return oceanAction(context, value, 'release') } throw new Error(`unsupported ocean status ${status}`) } function truckPayload(context, oceanBooking, carrier, index) { const value = suffix(context, index) return { outboundOrderId: null, oceanBookingId: oceanBooking.id, containerBookingId: null, loadingPlanId: null, warehouseId: oceanBooking.warehouseId, carrierId: id(carrier.id), carrierName: carrier.name, fleetContact: '人工验收联系人', fleetPhone: '13800000004', driverName: `人工司机-${value}`, driverPhone: '13800000005', vehicleNo: `粤B${value.slice(-6)}`, storeName: '人工验收仓', storeContact: '仓库联系人', storePhone: '13800000006', storeAddress: '深圳市南山区人工验收地址', storeCity: '深圳市', storeZipcode: '518000', packageCount: 10, grossWeightKg: 100, volumeCbm: 2.5, palletCount: 1, poNo: `PO-${value}`, plannedLoadingAt: '2026-09-16T08:00:00', transitMinutes: 120, payableAmount: 100, currency: 'RMB', remark: `${context.marker} 人工列表状态 ${index}`, expectedVersion: 0 } } async function createTruck(context, oceanBooking, index) { const carrierResponse = await context.request('GET', '/api/platform/v1/suppliers?tenantId=0&category=TRUCKING&status=ACTIVE&truckingScope=DOMESTIC&page=1&size=20') const carrierPage = context.apiSuccess(MODULE, `读取拖车状态样本 ${index} 的 ACTIVE 承运商`, carrierResponse, '', OPTIONS) const carrier = carrierPage?.records?.find?.((item) => item?.id && item?.name && item?.truckingScope === 'DOMESTIC') if (!carrier) throw new Error(`创建拖车样本 ${index} 缺少 ACTIVE 承运商`) const response = await context.request('POST', TRUCK_BASE, truckPayload(context, oceanBooking, carrier, index)) const value = context.apiSuccess(MODULE, `创建拖车状态样本 ${index}`, response, '', OPTIONS) if (!value?.id) throw new Error(`创建拖车样本 ${index} 失败`) const billUpload = await uploadDispatchOrder(context, id(value.id), index) const billAttachment = context.apiSuccess(MODULE, `拖车状态样本 ${index} 上传提单附件`, billUpload, '', OPTIONS) if (!billAttachment?.id) throw new Error(`拖车样本 ${index} 未上传有效提单附件`) return value } async function truckAction(context, value, endpoint) { const response = await post(context, `${TRUCK_BASE}/${id(value.id)}/${endpoint}`, { expectedVersion: Number(value.version), reason: `${context.marker}-${endpoint}` }, `truck-${endpoint}`) const next = context.apiSuccess(MODULE, `拖车推进 ${endpoint}`, response, `id=${id(value.id)}`, OPTIONS) if (!next?.id) throw new Error(`拖车 ${id(value.id)} 推进 ${endpoint} 失败`) return next } async function buildTruck(context, oceanBooking, status, index) { let value = await createTruck(context, oceanBooking, index) if (status === 'DRAFT') return value if (status === 'RESCHEDULED') { const response = await post(context, `${TRUCK_BASE}/${id(value.id)}/reschedule?plannedLoadingAt=2026-09-18T08%3A00%3A00`, { expectedVersion: Number(value.version), reason: `${context.marker}-reschedule` }, 'truck-reschedule') return context.apiSuccess(MODULE, '拖车推进 reschedule', response, '', OPTIONS) } if (status === 'EXCEPTION') return truckAction(context, value, 'exception') if (status === 'CANCELLED') return truckAction(context, value, 'cancel') const chain = [['submit', 'PENDING_DISPATCH'], ['dispatch', 'DISPATCHED'], ['pickup', 'PICKUP_IN_PROGRESS'], ['arrive', 'ARRIVED_WAREHOUSE'], ['load', 'LOADING']] for (const [endpoint, target] of chain) { value = await truckAction(context, value, endpoint) if (target === status) return value } throw new Error(`unsupported truck status ${status}`) } async function verify(context, base, status, resourceId) { const response = await context.request('GET', `${base}?page=1&size=100&status=${status}`) const page = data(response) context.assert(MODULE, `${status} 样本可从列表筛选并打开`, response, Array.isArray(page?.records) && page.records.some((item) => id(item.id) === resourceId), `status=${status}, id=${resourceId}`, OPTIONS) } export async function run(context, matrix = context.createdResources.manualStateMatrix) { const sourceOceanId = id(context.createdResources?.oceanBooking?.id) if (!sourceOceanId) throw new Error('缺少现有订舱生命周期模块生成的模板资源') const templateResponse = await context.request('GET', `${OCEAN_BASE}/${sourceOceanId}`) const template = context.apiSuccess(MODULE, '读取订舱状态矩阵模板', templateResponse, '', OPTIONS) if (!template?.oceanBooking || !template?.parties?.length || !template?.containerRequirements?.length) { throw new Error('订舱模板缺少渠道、仓库、参与方或柜型需求') } matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {} } for (const [index, status] of OCEAN_STATUSES.entries()) { const value = await buildOcean(context, template, status, index) const resourceId = id(value?.oceanBooking?.id); const actual = value?.oceanBooking?.status context.assert(MODULE, `订舱保留 ${status}`, null, resourceId && actual === status, `id=${resourceId || 'missing'}, actual=${actual || 'missing'}`, OPTIONS) if (!resourceId || actual !== status) continue const fixtureValue = { id: resourceId, status: actual, customerId: id(value?.oceanBooking?.customerId), customerName: value?.oceanBooking?.customerName, warehouseId: id(value?.oceanBooking?.warehouseId), } matrix.pages.oceanBookings.fixtures[status] = { ...fixtureValue, detailPath: `/ocean-bookings/${resourceId}`, } await verify(context, OCEAN_BASE, status, resourceId) } matrix.pages.truckAppointments = { requiredStatuses: TRUCK_STATUSES, fixtures: {} } for (const [index, status] of TRUCK_STATUSES.entries()) { const released = await buildOcean(context, template, 'RELEASED', index + 100) const releasedOcean = { id: id(released?.oceanBooking?.id), warehouseId: id(released?.oceanBooking?.warehouseId), } if (!releasedOcean.id || !releasedOcean.warehouseId) { throw new Error(`拖车状态样本 ${index} 缺少独立 RELEASED 订舱`) } const value = await buildTruck(context, releasedOcean, status, index) const resourceId = id(value?.id) matrix.pages.truckAppointments.fixtures[status] = { id: resourceId, status: value?.status, detailPath: `/truck-appointments/${resourceId}` } context.assert(MODULE, `拖车保留 ${status}`, null, resourceId && value?.status === status, `id=${resourceId || 'missing'}, actual=${value?.status || 'missing'}`, OPTIONS) await verify(context, TRUCK_BASE, status, resourceId) } }