Files
baiyue-firstleg/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs
T

175 lines
11 KiB
JavaScript

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 = ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED']
const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES]
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 source = template.oceanBooking
return { expectedVersion: 0, data: {
customerId: source.customerId, customerName: source.customerName,
channelCode: source.channelCode,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
billOfLadingNo: `BL-${suffix(context, index)}`, vesselName: `人工轮-${suffix(context, index)}`,
voyageNo: `V${suffix(context, index)}`,
shippingCompanyId: source.shippingCompanyId,
shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId,
bookingAgentName: source.bookingAgentName,
departurePortCode: source.departurePortCode, departurePortName: source.departurePortName,
destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName,
placeOfDelivery: source.placeOfDelivery, serviceTerm: source.serviceTerm, freightTerm: source.freightTerm,
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, 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)
context.assert(MODULE, `订舱样本 ${index} 一次创建直接返回 CONFIRMED`, response,
Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'CONFIRMED',
`id=${id(value?.oceanBooking?.id) || 'missing'}, status=${value?.oceanBooking?.status || 'missing'}`, OPTIONS)
if (!value?.oceanBooking?.id || value?.oceanBooking?.status !== 'CONFIRMED') {
throw new Error(`创建订舱样本 ${index} 未直接进入 CONFIRMED`)
}
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 === 'CONFIRMED') return value
if (status === 'CANCELLED') return oceanAction(context, value, 'cancel')
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, oceanBooking, index) {
const value = suffix(context, index)
return { outboundOrderId: null, oceanBookingId: oceanBooking.id,
containerBookingId: null, loadingPlanId: null,
warehouseId: oceanBooking.warehouseId, customerId: oceanBooking.customerId,
customerName: oceanBooking.customerName,
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, oceanBooking, index) {
const response = await context.request('POST', TRUCK_BASE, truckPayload(context, oceanBooking, 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, 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'], ['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_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)
}
const releasedOcean = matrix.pages.oceanBookings.fixtures.RELEASED
if (!releasedOcean.id || !releasedOcean.customerId || !releasedOcean.warehouseId) {
throw new Error('RELEASED 订舱缺少拖车所需的客户或仓库归属')
}
matrix.pages.truckAppointments = { requiredStatuses: TRUCK_STATUSES, fixtures: {} }
for (const [index, status] of TRUCK_STATUSES.entries()) {
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)
}
}