2026-08-13 14:03:54 +08:00
|
|
|
const MODULE = 'phase3-container-overview'
|
|
|
|
|
const OPTIONS = { scenario: 'phase3-container-overview-read-only', deploymentBlocker: true }
|
|
|
|
|
const CONTAINERS = '/api/platform/v1/containers'
|
|
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
|
2026-08-14 14:26:56 +08:00
|
|
|
export function controlledPhotoDownload(downloadPath, response) {
|
|
|
|
|
const controlledPath = typeof downloadPath === 'string'
|
2026-08-14 14:50:39 +08:00
|
|
|
&& /^\/api\/platform\/v1\/containers\/[1-9]\d*\/pda\/photos\/[1-9]\d*\/download$/.test(downloadPath)
|
2026-08-14 14:26:56 +08:00
|
|
|
const contentType = String(response?.contentType || '').toLowerCase()
|
|
|
|
|
return controlledPath
|
|
|
|
|
&& response?.http === 200
|
|
|
|
|
&& response?.bytes?.length > 0
|
|
|
|
|
&& Boolean(contentType)
|
|
|
|
|
&& !contentType.includes('json')
|
|
|
|
|
&& !contentType.includes('text/html')
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 14:03:54 +08:00
|
|
|
export async function run(context) {
|
2026-08-13 20:00:30 +08:00
|
|
|
const loadingRequested = context.options?.modules?.includes('phase3-loading') === true
|
|
|
|
|
const fixture = loadingRequested
|
|
|
|
|
? context.createdResources?.phase3Loading
|
|
|
|
|
: context.createdResources?.phase3Loading || context.createdResources?.phase3ContainerLoading
|
2026-08-13 14:03:54 +08:00
|
|
|
const containerId = id(fixture?.containerBookingId)
|
|
|
|
|
const planId = id(fixture?.loadingPlanId)
|
|
|
|
|
const warehouseId = id(fixture?.warehouseId)
|
2026-08-13 20:00:30 +08:00
|
|
|
const markerMatches = fixture?.marker === context.marker
|
|
|
|
|
|| fixture?.marker === `${context.marker}-CAB`
|
|
|
|
|
|| fixture?.sourceMarker === `${context.marker}-CAB`
|
|
|
|
|
if (!containerId || !planId || !warehouseId || !markerMatches) {
|
2026-08-13 14:03:54 +08:00
|
|
|
context.addStep(MODULE, '复用智能装柜产生的货柜 overview fixture', null, false,
|
2026-08-13 20:00:30 +08:00
|
|
|
`source=${loadingRequested ? 'phase3Loading-required' : 'available-container-fixture'}, containerBookingId=${containerId || 'missing'}, loadingPlanId=${planId || 'missing'}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
|
2026-08-13 14:03:54 +08:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const overviewResponse = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/overview`)
|
|
|
|
|
const overview = context.apiSuccess(MODULE, '查询货柜 overview 聚合详情', overviewResponse,
|
|
|
|
|
`containerBookingId=${containerId}`, OPTIONS)
|
|
|
|
|
context.assert(MODULE, 'overview 返回货柜身份、版本和时间线', overviewResponse,
|
|
|
|
|
id(overview?.container?.id) === containerId
|
|
|
|
|
&& Number.isFinite(Number(overview?.container?.version))
|
|
|
|
|
&& Array.isArray(overview?.documents)
|
|
|
|
|
&& Array.isArray(overview?.exceptionHistory)
|
|
|
|
|
&& Array.isArray(overview?.timeline),
|
|
|
|
|
`documents=${overview?.documents?.length ?? 'missing'}, timeline=${overview?.timeline?.length ?? 'missing'}`, OPTIONS)
|
|
|
|
|
const timeline = Array.isArray(overview?.timeline) ? overview.timeline : []
|
|
|
|
|
context.assert(MODULE, 'overview timeline 按 occurredAt 升序返回', overviewResponse,
|
|
|
|
|
timeline.every((item, index) => index === 0 || new Date(timeline[index - 1]?.occurredAt) <= new Date(item?.occurredAt)),
|
|
|
|
|
`events=${timeline.length}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
if (overview?.pda) {
|
2026-08-13 20:00:30 +08:00
|
|
|
const expectsPhotos = fixture === context.createdResources?.phase3Loading
|
|
|
|
|
&& Array.isArray(fixture?.evidenceTypes) && fixture.evidenceTypes.length > 0
|
2026-08-14 14:26:56 +08:00
|
|
|
const requiredFields = ['taskId', 'taskStatus', 'workflowStatus', 'taskVersion', 'itemCount', 'completedItemCount', 'unresolvedItemCount']
|
2026-08-14 10:09:46 +08:00
|
|
|
const missingFields = requiredFields.filter((field) => overview.pda[field] === undefined)
|
2026-08-13 14:03:54 +08:00
|
|
|
context.assert(MODULE, 'overview PDA 摘要字段完整', overviewResponse,
|
2026-08-14 10:09:46 +08:00
|
|
|
missingFields.length === 0
|
2026-08-13 14:03:54 +08:00
|
|
|
&& Array.isArray(overview.pda.photoCounts)
|
|
|
|
|
&& Array.isArray(overview.pda.photos)
|
2026-08-13 20:00:30 +08:00
|
|
|
&& Array.isArray(overview.pda.exceptionReasons)
|
|
|
|
|
&& (!expectsPhotos || overview.pda.photos.length > 0),
|
2026-08-14 10:09:46 +08:00
|
|
|
`taskId=${id(overview.pda.taskId)}, photos=${overview.pda.photos?.length ?? 'not-array'}, missingFields=${missingFields.join('|') || 'none'}, photoCountsArray=${Array.isArray(overview.pda.photoCounts)}, photosArray=${Array.isArray(overview.pda.photos)}, exceptionReasonsArray=${Array.isArray(overview.pda.exceptionReasons)}`, OPTIONS)
|
2026-08-14 14:26:56 +08:00
|
|
|
const progressIsComplete = Number(overview.pda.itemCount) > 0
|
|
|
|
|
&& Number(overview.pda.completedItemCount) === Number(overview.pda.itemCount)
|
|
|
|
|
&& Number(overview.pda.unresolvedItemCount) === 0
|
|
|
|
|
context.assert(MODULE, 'overview PDA 100%进度区分作业中与待复核', overviewResponse,
|
|
|
|
|
!progressIsComplete || overview.pda.taskStatus !== 'LOADING'
|
|
|
|
|
|| overview.pda.workflowStatus === 'AWAITING_REVIEW',
|
|
|
|
|
`taskStatus=${overview.pda.taskStatus || 'missing'}, workflowStatus=${overview.pda.workflowStatus || 'missing'}, completed=${overview.pda.completedItemCount ?? 'missing'}/${overview.pda.itemCount ?? 'missing'}`, OPTIONS)
|
2026-08-13 14:03:54 +08:00
|
|
|
for (const [index, photo] of overview.pda.photos.entries()) {
|
|
|
|
|
context.assert(MODULE, `overview PDA 照片摘要 ${index + 1} 字段完整`, overviewResponse,
|
2026-08-14 22:43:07 +08:00
|
|
|
['evidenceId', 'fileId', 'status'].every((field) => photo[field] !== undefined),
|
2026-08-13 14:03:54 +08:00
|
|
|
`fileId=${id(photo.fileId)}`, OPTIONS)
|
2026-08-14 14:26:56 +08:00
|
|
|
const downloadPath = String(photo.downloadPath || '')
|
2026-08-14 22:30:28 +08:00
|
|
|
if (photo.status === 'VOID') {
|
|
|
|
|
context.assert(MODULE, `overview PDA 作废照片 ${index + 1} 无下载入口`, overviewResponse,
|
|
|
|
|
!downloadPath,
|
|
|
|
|
`fileId=${id(photo.fileId)}, status=${photo.status}, downloadPath=${downloadPath || 'none'}`, OPTIONS)
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-08-14 14:26:56 +08:00
|
|
|
const controlledPath = downloadPath.startsWith('/api/')
|
|
|
|
|
&& !downloadPath.startsWith('//') && !downloadPath.includes('://')
|
2026-08-14 22:30:28 +08:00
|
|
|
if (photo.status !== 'ACTIVE' || !controlledPath) {
|
2026-08-14 14:26:56 +08:00
|
|
|
context.assert(MODULE, `overview PDA 照片 ${index + 1} 下载路径受控`, overviewResponse, false,
|
2026-08-14 22:30:28 +08:00
|
|
|
`fileId=${id(photo.fileId)}, status=${photo.status || 'missing'}, downloadPath=${downloadPath || 'missing'}`, OPTIONS)
|
2026-08-14 14:26:56 +08:00
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const download = await context.requestBinary('GET', downloadPath)
|
|
|
|
|
context.assert(MODULE, `overview PDA 照片 ${index + 1} 可鉴权下载`, download,
|
|
|
|
|
controlledPhotoDownload(downloadPath, download),
|
|
|
|
|
`fileId=${id(photo.fileId)}, bytes=${download?.bytes?.length ?? 0}, contentType=${download?.contentType || 'missing'}`, OPTIONS)
|
2026-08-13 14:03:54 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const documentType of ['DELIVERY_ORDER', 'PICKUP_ORDER']) {
|
|
|
|
|
const response = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/documents/${documentType}`)
|
|
|
|
|
const data = context.apiSuccess(MODULE, `查询 ${documentType} 单证`, response, `documentType=${documentType}`, OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${documentType} 单证归属货柜`, response,
|
|
|
|
|
data == null || id(data.containerId || containerId) === containerId,
|
|
|
|
|
`containerBookingId=${containerId}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clearance = await context.request('GET', `${CONTAINERS}/${encodeURIComponent(containerId)}/clearance`)
|
|
|
|
|
context.apiSuccess(MODULE, '查询目的港清关资料', clearance, `containerBookingId=${containerId}`, OPTIONS)
|
|
|
|
|
const customs = await context.request('GET', `/api/platform/v1/first-leg/loading-lists/${encodeURIComponent(planId)}/customs?warehouseId=${encodeURIComponent(warehouseId)}`)
|
2026-08-24 13:48:16 +08:00
|
|
|
if (fixture?.finalStatus === 'LOADED') {
|
|
|
|
|
context.apiSuccess(MODULE, '查询出口报关资料', customs, `loadingPlanId=${planId}, warehouseId=${warehouseId}`, OPTIONS)
|
|
|
|
|
} else {
|
|
|
|
|
context.assert(MODULE, '运行中计划尚未生成完成装柜清单,出口报关资料拒绝读取', customs,
|
|
|
|
|
customs.http === 200 && String(customs.body?.code) === 'A0409',
|
|
|
|
|
`loadingPlanId=${planId}, status=${fixture?.finalStatus || 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
2026-08-13 14:03:54 +08:00
|
|
|
|
|
|
|
|
context.addStep(MODULE, '异常标记/取消、单证上传和完成锁定保持写入阻断', null, true,
|
|
|
|
|
'默认全量脚本只读;写场景需显式独立授权和一次性测试柜次', OPTIONS)
|
|
|
|
|
}
|