Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-carrier-schedule-queries.mjs
2026-08-26 14:56:33 +08:00

120 lines
7.1 KiB
JavaScript

const MODULE = 'phase3-carrier-schedule-queries'
const OPTIONS = { scenario: 'carrier-schedule-query-policy', deploymentBlocker: true }
const BASE = '/api/platform/v1/carrier-schedule-queries'
const AUTOMATED_CARRIERS = Object.freeze(['MATSON', 'SML', 'ZIM', 'EMC', 'HEDE'])
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function data(response) { return codeOf(response) === '200' ? response.body.data : null }
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method,
headers: {
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers,
},
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
}
}
export function currentOutboundFixture(context) {
const loading = context?.createdResources?.phase3Loading
const outbound = context?.createdResources?.phase3OutboundDispatch
const marker = context?.marker
const matches = loading?.marker === marker && outbound?.marker === marker
&& id(loading?.containerBookingId) && id(loading?.containerBookingId) === id(outbound?.containerBookingId)
&& id(loading?.oceanBookingId) && id(loading?.containerNo)
&& outbound?.confirmedStatus === 'WAITING_FOR_LOADING'
return matches ? {
marker, customerId: id(outbound.tenantId || loading.tenantId), oceanBookingId: id(loading.oceanBookingId),
containerBookingId: id(loading.containerBookingId), trackingNo: id(loading.containerNo),
} : null
}
export function ownsCarrierQuery(resource, marker) {
return Boolean(resource?.ownedByModule && resource?.queryId && resource?.marker === marker
&& resource?.triggerSource === 'OUTBOUND_CONFIRM')
}
function blocker(context, name, evidence) {
context.addStep(MODULE, name, null, false, evidence, OPTIONS)
}
function validLongId(value) { return typeof value === 'string' && /^\d{16,}$/.test(value) }
export async function run(context) {
context.addCoverageGap(MODULE, OPTIONS.scenario, 'COSCO官方API自动查询',
'COSCO Shipping Open Platform 待正式凭据;当前返回MANUAL_REQUIRED,不逆向网页WAF',
'external-third-party')
const carriersResponse = await request(context, 'GET', `${BASE}/carriers`)
const carriers = context.apiSuccess(MODULE, '查询六家船司自动能力', carriersResponse, '', OPTIONS)
const byCode = new Map((Array.isArray(carriers) ? carriers : []).map((item) => [item.carrierCode, item]))
context.assert(MODULE, '五家自动Provider与COSCO覆盖缺口精确', carriersResponse,
['MATSON', 'SML', 'ZIM', 'COSCO', 'EMC', 'HEDE'].every((code) => byCode.has(code))
&& AUTOMATED_CARRIERS.every((code) => byCode.get(code)?.automated === true
&& byCode.get(code)?.coverageGap === false)
&& byCode.get('COSCO')?.automated === false && byCode.get('COSCO')?.coverageGap === true
&& byCode.get('COSCO')?.coverageGapCode === 'OFFICIAL_API_CREDENTIALS_REQUIRED',
`automated=${AUTOMATED_CARRIERS.filter((code) => byCode.get(code)?.automated).join(',') || 'none'}`, OPTIONS)
const fixture = currentOutboundFixture(context)
if (!fixture) {
blocker(context, '缺少本轮phase3出仓fixture,禁止借历史Query验收',
`marker=${context.marker}, loading=${context.createdResources?.phase3Loading?.marker || 'missing'}, outbound=${context.createdResources?.phase3OutboundDispatch?.marker || 'missing'}`)
return
}
const selector = new URLSearchParams({
customerId: fixture.customerId, oceanBookingId: fixture.oceanBookingId,
containerBookingId: fixture.containerBookingId, keyword: fixture.trackingNo, page: '1', size: '20',
})
const pageResponse = await request(context, 'GET', `${BASE}?${selector}`)
const page = context.apiSuccess(MODULE, '查询本轮出仓自动创建的船司任务', pageResponse, '', OPTIONS)
const matches = (Array.isArray(page?.records) ? page.records : []).filter((item) => {
const task = item?.query
return id(task?.customerId) === fixture.customerId && id(task?.oceanBookingId) === fixture.oceanBookingId
&& id(task?.containerBookingId) === fixture.containerBookingId && task?.trackingNo === fixture.trackingNo
&& task?.triggerSource === 'OUTBOUND_CONFIRM'
})
context.assert(MODULE, '本轮出仓范围唯一命中CarrierScheduleQuery', pageResponse,
matches.length === 1, `matches=${matches.length}, total=${page?.total ?? 'missing'}`, OPTIONS)
if (matches.length !== 1) return
const task = matches[0].query
const queryId = id(task.id)
const automatedCarrier = AUTOMATED_CARRIERS.includes(task?.carrierCode)
const manualCarrier = task?.carrierCode === 'OTHER' || !AUTOMATED_CARRIERS.includes(task?.carrierCode)
context.assert(MODULE, '出仓自动建任务兼容自动船司与未知船司人工处理', pageResponse,
(automatedCarrier && ['READY', 'MANUAL_REQUIRED'].includes(task?.status))
|| (manualCarrier && task?.status === 'MANUAL_REQUIRED' && Boolean(task?.manualReason)),
`carrierCode=${task?.carrierCode || 'missing'}, status=${task?.status || 'missing'}, manualReason=${task?.manualReason || 'none'}`, OPTIONS)
const detailResponse = await request(context, 'GET', `${BASE}/${encodeURIComponent(queryId)}`)
const detail = context.apiSuccess(MODULE, '回查本轮出仓船节点详情', detailResponse, `queryId=${queryId}`, OPTIONS)
const departed = (detail?.nodes || []).filter((item) => item.standardNode === 'DEPARTED_WAREHOUSE'
&& item.actualAt != null && item.estimatedAt == null)
context.assert(MODULE, '出仓自动任务携带实际DEPARTED_WAREHOUSE事实与长ID', detailResponse,
id(detail?.query?.query?.id) === queryId && validLongId(detail?.query?.query?.id)
&& departed.length >= 1 && departed.every((item) => validLongId(item.id)
&& item.providerCode === 'FIRSTLEG' && item.providerEventId && item.providerNodeCode
&& typeof item.rawPayloadHash === 'string' && /^[0-9a-f]{64}$/.test(item.rawPayloadHash)),
`queryId=${queryId}, departed=${departed.length}`, OPTIONS)
context.createdResources.phase3CarrierScheduleQuery = {
queryId, marker: context.marker, ownedByModule: true, triggerSource: 'OUTBOUND_CONFIRM',
customerId: fixture.customerId, oceanBookingId: fixture.oceanBookingId,
containerBookingId: fixture.containerBookingId, trackingNo: fixture.trackingNo,
carrierCode: task?.carrierCode, initialStatus: task?.status,
version: Number(data(detailResponse)?.query?.query?.version),
}
}