2026-08-26 16:50:22 +08:00
|
|
|
|
import { readFile } from 'node:fs/promises'
|
|
|
|
|
|
import { resolve } from 'node:path'
|
|
|
|
|
|
|
|
|
|
|
|
const MODULE = 'manual-loading-complete-fixture'
|
|
|
|
|
|
const OPTIONS = { scenario: 'manual-loading-complete-fixture', deploymentBlocker: true }
|
|
|
|
|
|
const PDA = '/api/platform/v1/first-leg/pda-loading'
|
|
|
|
|
|
const SMART = '/api/platform/v1/first-leg/smart-loading'
|
|
|
|
|
|
const CONTAINERS = '/api/platform/v1/containers'
|
|
|
|
|
|
const REQUIRED_EVIDENCE_TYPES = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
|
|
|
|
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
|
function rows(value) { return Array.isArray(value) ? value : [] }
|
|
|
|
|
|
|
|
|
|
|
|
async function loadSource(context) {
|
|
|
|
|
|
const report = JSON.parse(await readFile(resolve(context.options.manualLoadingCompleteSourceReport), 'utf8'))
|
|
|
|
|
|
const source = report?.createdResources?.manualPdaBatchVerification
|
|
|
|
|
|
const sourceGateway = String(report?.gateway || '').replace(/\/$/, '')
|
|
|
|
|
|
const valid = sourceGateway === context.options.gateway && source?.stage === 'PDA_AWAITING_REVIEW'
|
|
|
|
|
|
&& source?.pdaTaskId && source?.loadingPlanId && source?.containerBookingId
|
|
|
|
|
|
context.assert(MODULE, '读取同环境 PDA 待复核核验报告', null, valid,
|
|
|
|
|
|
`sourceMarker=${report?.marker || 'missing'}, sameGateway=${sourceGateway === context.options.gateway}, pdaTaskId=${id(source?.pdaTaskId) || 'missing'}`,
|
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
return valid ? source : null
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function taskItems(value) {
|
|
|
|
|
|
return rows(value).map((item) => ({ id: id(item.id), tenantId: id(item.tenantId),
|
|
|
|
|
|
inboundId: id(item.inboundId), inboundDetailId: id(item.inboundDetailId),
|
|
|
|
|
|
plannedPackageCount: Number(item.plannedPackageCount), pickedPackageCount: Number(item.pickedPackageCount),
|
|
|
|
|
|
itemStatus: item.itemStatus, version: Number(item.version) }))
|
|
|
|
|
|
.sort((left, right) => left.inboundId.localeCompare(right.inboundId) || left.id.localeCompare(right.id))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function sourceInboundSnapshots(items) {
|
|
|
|
|
|
const values = new Map()
|
|
|
|
|
|
for (const item of rows(items)) {
|
|
|
|
|
|
const inboundId = id(item.inboundId)
|
|
|
|
|
|
const current = values.get(inboundId) || { tenantId: id(item.tenantId), inboundId, pickedPackageCount: 0 }
|
|
|
|
|
|
current.pickedPackageCount += Number(item.pickedPackageCount || 0)
|
|
|
|
|
|
values.set(inboundId, current)
|
|
|
|
|
|
}
|
|
|
|
|
|
return [...values.values()]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export async function run(context) {
|
|
|
|
|
|
const source = await loadSource(context)
|
|
|
|
|
|
if (!source) return
|
|
|
|
|
|
const taskId = id(source.pdaTaskId)
|
|
|
|
|
|
const taskResponse = await context.request('GET',
|
|
|
|
|
|
`${PDA}/tasks/${encodeURIComponent(taskId)}?warehouseId=${encodeURIComponent(source.warehouseId)}`)
|
|
|
|
|
|
const task = context.apiSuccess(MODULE, '最终完成前读取 PDA 待复核任务', taskResponse, '', OPTIONS)
|
|
|
|
|
|
const beforeItems = taskItems(task?.items)
|
|
|
|
|
|
const evidenceOwner = rows(task?.items).find((item) => id(item.inboundId) === id(task?.inboundId))
|
|
|
|
|
|
|| rows(task?.items)[0]
|
|
|
|
|
|
context.assert(MODULE, '任务全部拣齐、待复核且证据归属入库单明确', taskResponse,
|
|
|
|
|
|
id(task?.id) === taskId && task?.taskStatus === 'LOADING'
|
|
|
|
|
|
&& task?.workflowStatus === 'AWAITING_REVIEW' && task?.assignment?.assigneeId
|
|
|
|
|
|
&& beforeItems.length > 0 && beforeItems.every((item) => item.itemStatus === 'PICKED'
|
|
|
|
|
|
&& item.pickedPackageCount === item.plannedPackageCount)
|
|
|
|
|
|
&& evidenceOwner?.tenantId && evidenceOwner?.inboundId,
|
|
|
|
|
|
`workflowStatus=${task?.workflowStatus || 'missing'}, items=${beforeItems.length}, evidenceTenantId=${id(evidenceOwner?.tenantId) || 'missing'}, evidenceInboundId=${id(evidenceOwner?.inboundId) || 'missing'}`,
|
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
if (!evidenceOwner?.tenantId || !evidenceOwner?.inboundId) return
|
|
|
|
|
|
|
|
|
|
|
|
const evidencePath = `/api/platform/v1/inbounds/${encodeURIComponent(evidenceOwner.inboundId)}/loading/evidence?tenantId=${encodeURIComponent(evidenceOwner.tenantId)}`
|
|
|
|
|
|
const evidenceResponse = await context.request('GET', evidencePath)
|
|
|
|
|
|
const evidence = context.apiSuccess(MODULE, '上传五类证据前读取空基线', evidenceResponse, '', OPTIONS)
|
|
|
|
|
|
const activeEvidence = rows(evidence?.evidences).filter((item) => item.status !== 'VOID')
|
|
|
|
|
|
context.assert(MODULE, '本轮最终完成前尚未上传 ACTIVE 装柜证据', evidenceResponse,
|
|
|
|
|
|
activeEvidence.length === 0,
|
|
|
|
|
|
`activeEvidence=${activeEvidence.map((item) => item.evidenceType).join(',') || 'none'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
|
|
const planResponse = await context.request('GET', `${SMART}/plans/${encodeURIComponent(source.loadingPlanId)}`)
|
|
|
|
|
|
const plan = context.apiSuccess(MODULE, '最终完成前读取运行中装柜计划', planResponse, '', OPTIONS)
|
|
|
|
|
|
context.assert(MODULE, '计划保持 LOADING 且尚未最终完成', planResponse,
|
|
|
|
|
|
id(plan?.id) === id(source.loadingPlanId) && plan?.status === 'LOADING'
|
|
|
|
|
|
&& plan?.finalCompletionStatus === 'IN_PROGRESS',
|
|
|
|
|
|
`status=${plan?.status || 'missing'}, finalCompletionStatus=${plan?.finalCompletionStatus || 'missing'}, version=${plan?.version ?? 'missing'}`,
|
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
|
|
|
|
|
|
const operationsResponse = await context.request('GET', `${PDA}/tasks/${encodeURIComponent(taskId)}/operations`)
|
|
|
|
|
|
const operations = context.apiSuccess(MODULE, '最终完成前读取任务审计', operationsResponse, '', OPTIONS)
|
|
|
|
|
|
context.assert(MODULE, '完成前只有一次 CLAIM/BATCH 且无 COMPLETE_LOADING', operationsResponse,
|
|
|
|
|
|
rows(operations).filter((item) => item.actionType === 'CLAIM').length === 1
|
|
|
|
|
|
&& rows(operations).filter((item) => item.actionType === 'BATCH').length === 1
|
|
|
|
|
|
&& !rows(operations).some((item) => ['REVIEW', 'COMPLETE_LOADING'].includes(item.actionType)),
|
|
|
|
|
|
`actions=${rows(operations).map((item) => item.actionType).join(',') || 'none'}`, OPTIONS)
|
|
|
|
|
|
|
2026-09-09 12:19:16 +08:00
|
|
|
|
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(source.containerBookingId)}`)
|
|
|
|
|
|
const container = context.apiSuccess(MODULE, '最终完成前回查 RELEASED 订舱柜次', containerResponse, '', OPTIONS)?.container
|
2026-08-26 16:50:22 +08:00
|
|
|
|
const bookingResponse = container?.oceanBookingId
|
|
|
|
|
|
? await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(container.oceanBookingId)}`)
|
|
|
|
|
|
: containerResponse
|
|
|
|
|
|
const booking = context.apiSuccess(MODULE, '最终完成前回查订舱门禁', bookingResponse, '', OPTIONS)
|
|
|
|
|
|
context.assert(MODULE, '完成前真实柜号与 RELEASED 订舱仍有效', bookingResponse,
|
|
|
|
|
|
id(container?.id) === id(source.containerBookingId) && container?.containerNo === source.containerNo
|
|
|
|
|
|
&& booking?.oceanBooking?.status === 'RELEASED',
|
|
|
|
|
|
`containerNo=${container?.containerNo || 'missing'}, oceanBookingId=${id(container?.oceanBookingId) || 'missing'}, bookingStatus=${booking?.oceanBooking?.status || 'missing'}`,
|
|
|
|
|
|
OPTIONS)
|
|
|
|
|
|
|
|
|
|
|
|
context.createdResources.manualLoadingCompleteFixture = {
|
|
|
|
|
|
schemaVersion: 1, sourceMarker: source.sourceMarker, preparedAt: new Date().toISOString(),
|
|
|
|
|
|
stage: 'MANUAL_EVIDENCE_AND_COMPLETE_READY', pdaTaskId: taskId,
|
|
|
|
|
|
loadingPlanId: id(source.loadingPlanId), containerBookingId: id(source.containerBookingId),
|
|
|
|
|
|
cabinetNo: source.cabinetNo, containerNo: source.containerNo, warehouseId: id(source.warehouseId),
|
|
|
|
|
|
evidenceTenantId: id(evidenceOwner.tenantId), evidenceInboundId: id(evidenceOwner.inboundId),
|
|
|
|
|
|
requiredEvidenceTypes: REQUIRED_EVIDENCE_TYPES, beforeTaskVersion: Number(task.version),
|
|
|
|
|
|
beforePlanVersion: Number(plan?.version), beforeItems,
|
|
|
|
|
|
sourceInbounds: sourceInboundSnapshots(task.items),
|
|
|
|
|
|
instructions: [
|
|
|
|
|
|
'在 PDA 当前任务中依次现场拍摄空仓、半仓、满仓、锁仓和带锁码照五类证据。',
|
|
|
|
|
|
'五类分别对应 PHOTO_STEP_1、PHOTO_STEP_2、PHOTO_STEP_3、PHOTO_STEP_4、SEAL;SEAL 必须填写非空锁码。',
|
|
|
|
|
|
'确认五类证据均为 ACTIVE 后,只点击一次最终完成;不要调用旧入库单兼容完成入口。',
|
|
|
|
|
|
'完成成功后任务、计划和所有来源入库单应进入唯一终态,立即运行 manual-loading-complete-verify。',
|
|
|
|
|
|
],
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|