Files

118 lines
7.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
const containerResponse = await context.request('GET', `${CONTAINERS}/domestic/${encodeURIComponent(source.containerBookingId)}`)
const container = context.apiSuccess(MODULE, '最终完成前回查 RELEASED 订舱柜次', containerResponse, '', OPTIONS)?.container
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。',
],
}
}