216 lines
13 KiB
JavaScript
216 lines
13 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', '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)
|
|
}
|
|
}
|