2026-08-26 16:50:22 +08:00
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
|
import { resolve } from 'node:path'
|
|
|
|
|
|
|
|
|
|
const MODULE = 'manual-pda-ready-fixture'
|
|
|
|
|
const OPTIONS = { scenario: 'manual-pda-ready-fixture', deploymentBlocker: true }
|
|
|
|
|
const SMART = '/api/platform/v1/first-leg/smart-loading'
|
|
|
|
|
const FIRST_LEG = '/api/platform/v1/first-leg'
|
|
|
|
|
const CONTAINERS = '/api/platform/v1/containers'
|
|
|
|
|
const PDA = `${FIRST_LEG}/pda-loading`
|
|
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
function rows(value) { return Array.isArray(value) ? value : [] }
|
|
|
|
|
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
|
|
|
|
|
function items(value) {
|
|
|
|
|
return rows(value).map((item) => ({ id: id(item.id), inboundId: id(item.inboundId),
|
|
|
|
|
packageCount: Number(item.packageCount) }))
|
|
|
|
|
.sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadSource(context) {
|
|
|
|
|
const report = JSON.parse(await readFile(resolve(context.options.manualPdaSourceReport), 'utf8'))
|
|
|
|
|
const source = report?.createdResources?.manualPreloadPushVerification
|
|
|
|
|
const sourceGateway = String(report?.gateway || '').replace(/\/$/, '')
|
|
|
|
|
const valid = sourceGateway === context.options.gateway && source?.stage === 'PRELOAD_EDITING'
|
|
|
|
|
&& source?.loadingPlanId && source?.containerBookingId && source?.pdaTaskCreated === false
|
|
|
|
|
&& Array.isArray(source?.expectedAssignments) && source.expectedAssignments.length > 0
|
|
|
|
|
context.assert(MODULE, '读取同环境推送预装核验报告', null, valid,
|
|
|
|
|
`sourceMarker=${report?.marker || 'missing'}, sameGateway=${sourceGateway === context.options.gateway}, loadingPlanId=${id(source?.loadingPlanId) || 'missing'}`,
|
|
|
|
|
OPTIONS)
|
|
|
|
|
return valid ? source : null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
const source = await loadSource(context)
|
|
|
|
|
if (!source) return
|
|
|
|
|
const planId = id(source.loadingPlanId)
|
|
|
|
|
const containerId = id(source.containerBookingId)
|
|
|
|
|
|
|
|
|
|
const planResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(planId)}`)
|
|
|
|
|
const plan = context.apiSuccess(MODULE, 'PDA 就绪操作前读取预装计划', planResponse, '', OPTIONS)
|
|
|
|
|
const beforeItems = items(plan?.items)
|
|
|
|
|
context.assert(MODULE, '操作前计划仍为无号已推送 EDITING', planResponse,
|
|
|
|
|
id(plan?.id) === planId && plan?.status === 'EDITING' && plan?.containerNo == null
|
|
|
|
|
&& Number(plan?.version) === Number(source.planVersion) && beforeItems.length > 0,
|
|
|
|
|
`status=${plan?.status || 'missing'}, expectedVersion=${source.planVersion}, actualVersion=${plan?.version ?? 'missing'}, items=${beforeItems.length}`,
|
|
|
|
|
OPTIONS)
|
|
|
|
|
if (!plan || beforeItems.length === 0) return
|
|
|
|
|
|
2026-09-09 12:19:16 +08:00
|
|
|
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(containerId)}`)
|
|
|
|
|
const container = context.apiSuccess(MODULE, 'PDA 就绪操作前读取内部模拟柜', containerResponse, '', OPTIONS)?.container
|
2026-08-26 16:50:22 +08:00
|
|
|
context.assert(MODULE, '操作前柜次仍无真实柜号和订舱关联', containerResponse,
|
|
|
|
|
id(container?.id) === containerId && container?.cabinetNo === source.cabinetNo
|
|
|
|
|
&& container?.containerNo == null && container?.oceanBookingId == null
|
|
|
|
|
&& Number.isInteger(Number(container?.version)),
|
|
|
|
|
`cabinetNo=${container?.cabinetNo || 'missing'}, containerNo=${container?.containerNo || 'null'}, oceanBookingId=${id(container?.oceanBookingId) || 'null'}, version=${container?.version ?? 'missing'}`,
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
|
|
|
|
const detailResponse = await context.request('GET',
|
|
|
|
|
`${FIRST_LEG}/loading-plans/${encodeURIComponent(planId)}?warehouseId=${encodeURIComponent(source.warehouseId)}`)
|
|
|
|
|
const detail = context.apiSuccess(MODULE, '操作前读取预装确认门禁', detailResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '缺少真实柜号与 RELEASED 订舱时不能确认预装', detailResponse,
|
|
|
|
|
detail?.summary?.status === 'EDITING' && detail?.summary?.readiness?.canConfirm === false
|
|
|
|
|
&& detail?.summary?.readiness?.readyForPda === false
|
|
|
|
|
&& detail?.summary?.readiness?.pdaTaskId == null,
|
|
|
|
|
`canConfirm=${detail?.summary?.readiness?.canConfirm}, readyForPda=${detail?.summary?.readiness?.readyForPda}, blockers=${rows(detail?.summary?.readiness?.blockers).join(',')}`,
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
|
|
|
|
const taskResponse = await context.request('GET',
|
|
|
|
|
`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(source.warehouseId)}`)
|
|
|
|
|
context.addStep(MODULE, '操作前 PDA 任务仍不存在', taskResponse,
|
|
|
|
|
taskResponse.http === 200 && codeOf(taskResponse) === 'A0404',
|
|
|
|
|
`HTTP=${taskResponse.http}, code=${codeOf(taskResponse)}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
context.createdResources.manualPdaReadyFixture = {
|
|
|
|
|
schemaVersion: 1, sourceMarker: source.sourceMarker, preparedAt: new Date().toISOString(),
|
|
|
|
|
stage: 'MANUAL_BOOKING_LINK_CONFIRM_READY', loadingPlanId: planId, containerBookingId: containerId,
|
|
|
|
|
cabinetNo: source.cabinetNo, tenantId: id(source.tenantId), warehouseId: id(source.warehouseId),
|
|
|
|
|
channelCode: source.channelCode, beforePlanVersion: Number(plan.version),
|
|
|
|
|
beforeContainerVersion: Number(container?.version), beforeItems,
|
|
|
|
|
expectedAssignments: source.expectedAssignments || [],
|
|
|
|
|
instructions: [
|
2026-08-26 17:09:02 +08:00
|
|
|
'在订舱页面创建与本清单客户、起运仓、渠道和柜型一致的 DRAFT 订舱,并填写承运船司代码 carrierCode 及完整运输资料。',
|
2026-08-26 16:50:22 +08:00
|
|
|
'确认放舱,使订舱进入 RELEASED;放舱本身不得创建货柜。',
|
|
|
|
|
'在原 containerBookingId 上选择该订舱未占用的柜型需求,录入唯一真实 containerNo 完成关联。',
|
|
|
|
|
'回到预装详情,确认 canConfirm=true 后执行整份计划确认;不要领取 PDA 任务。',
|
|
|
|
|
'记录 bookingNo、containerNo,并立即运行 manual-pda-ready-verify。',
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
}
|