170 lines
10 KiB
JavaScript
170 lines
10 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 = ['DRAFT', 'RELEASED']
|
|
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,
|
|
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, 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, 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 buildOcean(context, template, status, index) {
|
|
let value = await createOcean(context, template, index)
|
|
if (status === 'DRAFT') return value
|
|
if (status === 'RELEASED') return oceanAction(context, value, 'release')
|
|
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,
|
|
carrierId: null, carrierName: null, 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)
|
|
}
|
|
}
|