const MODULE = 'manual-state-ocean-trucking' const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBlocker: true } const OCEAN_BASE = '/api/platform/v1/first-leg/ocean-bookings' const TRUCK_BASE = '/api/platform/v1/truck-appointments' const OCEAN_STATUSES = ['DRAFT', 'SUBMITTED', 'CONFIRMED', 'RELEASED', 'FAILED', 'CHANGE_PENDING', 'CANCELLED'] const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE', 'LOADING', 'DEPARTED', 'COMPLETED', '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) }) } function oceanPayload(context, template, index) { const value = suffix(context, index); const source = template.oceanBooking return { expectedVersion: 0, data: { customerId: source.customerId, customerName: source.customerName, channelCode: source.channelCode, channelName: source.channelName, warehouseId: source.warehouseId, warehouseName: source.warehouseName, billOfLadingNo: `MAN-BL-${value}`, shippingCompanyId: source.shippingCompanyId, shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId, bookingAgentName: source.bookingAgentName, vesselName: `人工验收轮-${value}`, voyageNo: `V${value}`, departurePortCode: source.departurePortCode, departurePortName: source.departurePortName, destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName, placeOfDelivery: source.placeOfDelivery, serviceTerm: source.serviceTerm, freightTerm: source.freightTerm, estimatedSailingAt: '2026-09-15T08:00:00', estimatedArrivalAt: '2026-10-15T08: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, capacityCbm, maxWeightKg, quantity }) => ({ containerType, containerKind, capacityCbm, maxWeightKg, quantity })), } } } 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) if (!value?.oceanBooking?.id) throw new Error(`创建订舱样本 ${index} 失败`) 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 buildOcean(context, template, status, index) { let value = await createOcean(context, template, index) if (status === 'DRAFT') return value value = await oceanAction(context, value, 'submit') if (status === 'SUBMITTED') return value if (status === 'FAILED') return oceanAction(context, value, 'fail') if (status === 'CANCELLED') return oceanAction(context, value, 'cancel') value = await oceanAction(context, value, 'confirm') if (status === 'CONFIRMED') return value if (status === 'CHANGE_PENDING') return oceanAction(context, value, 'change-request', { vesselName: `变更轮-${suffix(context, index)}`, voyageNo: `CV${suffix(context, index)}`, estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00' }) if (status === 'RELEASED') return oceanAction(context, value, 'release', { billOfLadingNo: `REL-BL-${suffix(context, index)}`, vesselName: `放舱轮-${suffix(context, index)}`, voyageNo: `RV${suffix(context, index)}`, estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00' }) throw new Error(`unsupported ocean status ${status}`) } function truckPayload(context, oceanBookingId, index) { const value = suffix(context, index) return { outboundOrderId: null, oceanBookingId, containerBookingId: null, loadingPlanId: null, warehouseId: '193000000000200001', customerId: '10001', customerName: 'IT 测试客户', carrierName: `人工验收车队-${value}`, 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, receivableAmount: 150, currency: 'RMB', remark: `${context.marker} 人工列表状态 ${index}`, expectedVersion: 0 } } async function createTruck(context, oceanBookingId, index) { const response = await context.request('POST', TRUCK_BASE, truckPayload(context, oceanBookingId, index)) const value = context.apiSuccess(MODULE, `创建拖车状态样本 ${index}`, response, '', OPTIONS) if (!value?.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, oceanBookingId, status, index) { let value = await createTruck(context, oceanBookingId, 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'], ['depart', 'DEPARTED'], ['complete', 'COMPLETED']] 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_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 matrix.pages.oceanBookings.fixtures[status] = { id: resourceId, status: actual, detailPath: `/ocean-bookings/${resourceId}` } context.assert(MODULE, `订舱保留 ${status}`, null, resourceId && actual === status, `id=${resourceId || 'missing'}, actual=${actual || 'missing'}`, OPTIONS) await verify(context, OCEAN_BASE, status, resourceId) } const releasedOceanId = matrix.pages.oceanBookings.fixtures.RELEASED.id matrix.pages.truckAppointments = { requiredStatuses: TRUCK_STATUSES, fixtures: {} } for (const [index, status] of TRUCK_STATUSES.entries()) { const value = await buildTruck(context, releasedOceanId, 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) } }