2026-08-12 20:36:11 +08:00
|
|
|
const MODULE = 'manual-state-loading-pda'
|
|
|
|
|
const OPTIONS = { scenario: 'manual-loading-pda-state-matrix', deploymentBlocker: true }
|
|
|
|
|
const CONTAINERS = '/api/platform/v1/containers'
|
|
|
|
|
const SMART = '/api/platform/v1/first-leg/smart-loading'
|
|
|
|
|
const LOADING = '/api/platform/v1/first-leg'
|
|
|
|
|
const PDA = `${LOADING}/pda-loading`
|
|
|
|
|
const CONTAINER_STATUSES = ['WAIT_LOADING', 'EDITING', 'LOADING', 'COMPLETED']
|
|
|
|
|
const PRELOAD_STATUSES = ['EDITING', 'LOADING']
|
|
|
|
|
const PDA_STATUSES = ['UNASSIGNED', 'IN_PROGRESS', 'AWAITING_REVIEW', 'COMPLETED']
|
|
|
|
|
|
|
|
|
|
function id(value) { return value == null ? '' : String(value) }
|
|
|
|
|
function records(value) { return Array.isArray(value?.records) ? value.records : [] }
|
|
|
|
|
function suffix(context, label) {
|
|
|
|
|
return `${String(context.marker).replace(/[^A-Za-z0-9]/g, '').slice(-10)}${label}`.slice(-20)
|
|
|
|
|
}
|
|
|
|
|
function operationId(context, label) {
|
|
|
|
|
return `${context.marker}-manual-loading-${label}-${crypto.randomUUID()}`.slice(0, 128)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function post(context, path, body, label) {
|
|
|
|
|
return context.request('POST', path, body, context.token,
|
|
|
|
|
{ 'Idempotency-Key': operationId(context, label) })
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 09:49:55 +08:00
|
|
|
async function uploadDocument(context, path, expectedVersion, operationReason, label) {
|
|
|
|
|
const form = new FormData()
|
|
|
|
|
form.append('file', new Blob([`${context.marker}-${label}`], { type: 'text/plain' }), `${label}.txt`)
|
|
|
|
|
form.append('expectedVersion', String(expectedVersion))
|
|
|
|
|
if (operationReason) form.append('operationReason', operationReason)
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${context.options.gateway}${path}`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/json', Authorization: `Bearer ${context.token}`,
|
|
|
|
|
'Idempotency-Key': operationId(context, `${label}-upload`),
|
|
|
|
|
},
|
|
|
|
|
body: form,
|
|
|
|
|
signal: AbortSignal.timeout(60_000),
|
|
|
|
|
})
|
|
|
|
|
const raw = await response.text()
|
|
|
|
|
let body = null
|
|
|
|
|
try { body = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
|
|
|
|
|
return { method: 'POST', path, http: response.status, body, raw: body ? null : raw.slice(0, 500),
|
|
|
|
|
requestId: body?.requestId || response.headers.get('x-request-id'),
|
|
|
|
|
traceId: body?.traceId || response.headers.get('x-trace-id') }
|
|
|
|
|
} catch (error) {
|
|
|
|
|
return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 20:36:11 +08:00
|
|
|
async function prepareInbound(context, label) {
|
|
|
|
|
const originalMarker = context.marker
|
|
|
|
|
const fixtureMarker = `${originalMarker}-MAN-${label}`
|
|
|
|
|
const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement', 'inboundWarehouseResources']
|
|
|
|
|
const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']
|
|
|
|
|
const createdSnapshot = new Map(createdKeys.map((key) => [key,
|
|
|
|
|
{ present: Object.hasOwn(context.createdResources, key), value: context.createdResources[key] }]))
|
|
|
|
|
const contextSnapshot = new Map(contextKeys.map((key) => [key,
|
|
|
|
|
{ present: Object.hasOwn(context.context, key), value: context.context[key] }]))
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
context.marker = fixtureMarker
|
|
|
|
|
const orders = await import(new URL('./orders.mjs', import.meta.url))
|
|
|
|
|
await orders.run(context)
|
|
|
|
|
const measurement = await import(new URL('./inbound-measurement.mjs', import.meta.url))
|
|
|
|
|
await measurement.run(context)
|
|
|
|
|
const measured = context.createdResources.inboundMeasurement
|
|
|
|
|
const order = context.createdResources.orders
|
2026-08-13 11:57:33 +08:00
|
|
|
if (!measured?.inboundId || !measured?.tenantId) throw new Error(`${label} 未生成独立 WAIT_SPLIT 入库单`)
|
2026-08-12 20:36:11 +08:00
|
|
|
return { marker: fixtureMarker, tenantId: id(measured.tenantId), inboundId: id(measured.inboundId),
|
|
|
|
|
orderId: id(order?.orderId) }
|
|
|
|
|
} finally {
|
|
|
|
|
context.marker = originalMarker
|
|
|
|
|
for (const [key, snapshot] of createdSnapshot) {
|
|
|
|
|
if (snapshot.present) context.createdResources[key] = snapshot.value
|
|
|
|
|
else delete context.createdResources[key]
|
|
|
|
|
}
|
|
|
|
|
for (const [key, snapshot] of contextSnapshot) {
|
|
|
|
|
if (snapshot.present) context.context[key] = snapshot.value
|
|
|
|
|
else delete context.context[key]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function readInbound(context, fixture) {
|
|
|
|
|
const response = await context.request('GET',
|
|
|
|
|
`/api/platform/inbounds/${fixture.inboundId}?tenantId=${encodeURIComponent(fixture.tenantId)}`)
|
|
|
|
|
const value = context.apiSuccess(MODULE, '读取人工装柜独立入库单', response, '', OPTIONS)
|
|
|
|
|
const warehouseId = id(value?.summary?.warehouseId)
|
|
|
|
|
context.assert(MODULE, '人工装柜来源入库单可独立排柜', response,
|
2026-08-13 14:55:50 +08:00
|
|
|
value?.summary?.processStatus === 'WAIT_SPLIT'
|
2026-08-13 11:57:33 +08:00
|
|
|
&& value?.summary?.measureReviewStatus === 'APPROVED'
|
2026-08-12 20:36:11 +08:00
|
|
|
&& Boolean(warehouseId),
|
|
|
|
|
`inboundId=${fixture.inboundId}, status=${value?.summary?.processStatus || 'missing'}`, OPTIONS)
|
|
|
|
|
return { ...fixture, detail: value, warehouseId }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createConfirmedContainer(context, source, label) {
|
|
|
|
|
const value = suffix(context, label)
|
2026-08-17 13:24:48 +08:00
|
|
|
const containerNo = `MAN-${value}`.slice(0, 64)
|
2026-08-12 20:36:11 +08:00
|
|
|
const summary = source.detail.summary
|
|
|
|
|
const draft = {
|
|
|
|
|
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
|
2026-08-14 18:50:07 +08:00
|
|
|
channelCode: summary.channelCode, warehouseId: source.warehouseId,
|
2026-08-17 13:24:48 +08:00
|
|
|
billOfLadingNo: `BL-${value}`.slice(0, 128),
|
2026-08-15 11:05:58 +08:00
|
|
|
shippingCompanyId: null, shippingCompanyName: null,
|
|
|
|
|
bookingAgentId: null, bookingAgentName: null, departurePortCode: 'CNSZX',
|
2026-08-15 09:35:52 +08:00
|
|
|
vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64),
|
2026-08-12 20:36:11 +08:00
|
|
|
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
|
2026-08-15 09:35:52 +08:00
|
|
|
placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
|
|
|
|
|
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00',
|
|
|
|
|
remark: `${context.marker}-${label}`,
|
2026-08-12 20:36:11 +08:00
|
|
|
parties: [{ partyType: 'SHIPPER', partyName: `发货方-${value}` },
|
|
|
|
|
{ partyType: 'CONSIGNEE', partyName: `收货方-${value}` },
|
|
|
|
|
{ partyType: 'NOTIFY_PARTY', partyName: `通知方-${value}` }],
|
2026-08-17 13:24:48 +08:00
|
|
|
containers: [{ containerNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76,
|
|
|
|
|
maxWeightKg: 28000 }],
|
2026-08-12 20:36:11 +08:00
|
|
|
}
|
2026-08-14 21:00:20 +08:00
|
|
|
const simulationResponse = await context.request('POST', CONTAINERS, {
|
2026-08-17 13:24:48 +08:00
|
|
|
channelCode: summary.channelCode, warehouseId: source.warehouseId, containerType: '40HC',
|
|
|
|
|
capacityCbm: 76, maxWeightKg: 28000,
|
2026-08-14 21:00:20 +08:00
|
|
|
})
|
|
|
|
|
const simulation = context.apiSuccess(MODULE, `${label} 新增内部模拟柜`, simulationResponse, '', OPTIONS)
|
|
|
|
|
const containerId = id(simulation?.id)
|
|
|
|
|
if (!containerId) throw new Error(`${label} 未生成内部模拟柜`)
|
2026-08-12 20:36:11 +08:00
|
|
|
const createdResponse = await post(context, '/api/platform/v1/ocean-bookings',
|
|
|
|
|
{ expectedVersion: 0, data: draft }, `${label}-booking-create`)
|
|
|
|
|
const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS)
|
|
|
|
|
const bookingId = id(created?.oceanBooking?.id)
|
2026-08-15 09:35:52 +08:00
|
|
|
const confirmed = created
|
|
|
|
|
context.assert(MODULE, `${label} 一次登记直接返回 CONFIRMED`, createdResponse,
|
|
|
|
|
confirmed?.oceanBooking?.status === 'CONFIRMED',
|
|
|
|
|
`status=${confirmed?.oceanBooking?.status || 'missing'}`, OPTIONS)
|
2026-08-17 13:24:48 +08:00
|
|
|
const requirement = confirmed?.containers?.find?.((item) => item.containerNo === containerNo)
|
|
|
|
|
if (!requirement?.containerBookingId) throw new Error(`${label} 确认订舱未返回真实柜身份`)
|
2026-08-14 21:00:20 +08:00
|
|
|
const linkedResponse = await context.request('POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, {
|
2026-08-17 13:24:48 +08:00
|
|
|
realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(simulation.version),
|
2026-08-14 21:00:20 +08:00
|
|
|
})
|
|
|
|
|
const linked = context.apiSuccess(MODULE, `${label} 关联模拟柜与真实订舱`, linkedResponse, '', OPTIONS)
|
|
|
|
|
if (id(linked?.id) !== containerId || linked?.cabinetNo !== simulation.cabinetNo) {
|
|
|
|
|
throw new Error(`${label} 关联真实订舱后内部柜次身份漂移`)
|
|
|
|
|
}
|
|
|
|
|
const releasedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
|
2026-08-15 11:05:58 +08:00
|
|
|
expectedVersion: Number(confirmed?.oceanBooking?.version),
|
|
|
|
|
billOfLadingNo: confirmed?.oceanBooking?.billOfLadingNo,
|
|
|
|
|
vesselName: confirmed?.oceanBooking?.vesselName, voyageNo: confirmed?.oceanBooking?.voyageNo,
|
|
|
|
|
estimatedSailingAt: confirmed?.oceanBooking?.estimatedSailingAt,
|
|
|
|
|
estimatedArrivalAt: confirmed?.oceanBooking?.estimatedArrivalAt,
|
2026-08-14 21:00:20 +08:00
|
|
|
reason: context.marker,
|
|
|
|
|
}, `${label}-booking-release`)
|
|
|
|
|
context.apiSuccess(MODULE, `${label} 放舱已关联真实订舱`, releasedResponse, '', OPTIONS)
|
2026-08-12 20:36:11 +08:00
|
|
|
return containerId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function createPlanTask(context, label) {
|
|
|
|
|
const source = await readInbound(context, await prepareInbound(context, label))
|
|
|
|
|
const containerId = await createConfirmedContainer(context, source, label)
|
|
|
|
|
const candidateResponse = await context.request('GET',
|
|
|
|
|
`${SMART}/candidates?bookingId=${containerId}&warehouseId=${source.warehouseId}`)
|
|
|
|
|
const candidates = context.apiSuccess(MODULE, `${label} 查询同渠道候选货物`, candidateResponse, '', OPTIONS)
|
|
|
|
|
const candidate = Array.isArray(candidates)
|
|
|
|
|
? candidates.find((item) => id(item.inboundId) === source.inboundId && Number(item.availablePackageCount) > 0)
|
|
|
|
|
: null
|
|
|
|
|
if (!candidate?.inboundDetailId) throw new Error(`${label} 独立入库单未进入同渠道排柜候选`)
|
|
|
|
|
|
|
|
|
|
const planResponse = await context.request('POST', `${SMART}/plans`,
|
|
|
|
|
{ bookingId: containerId, remark: `${context.marker}-${label}` })
|
|
|
|
|
let plan = context.apiSuccess(MODULE, `${label} 创建智能排柜计划`, planResponse, '', OPTIONS)
|
|
|
|
|
const assignResponse = await context.request('POST', `${SMART}/plans/${plan.id}/assignments`, {
|
|
|
|
|
inboundId: source.inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
|
|
|
|
|
expectedVersion: Number(plan.version), operationId: operationId(context, `${label}-assign`),
|
|
|
|
|
})
|
|
|
|
|
plan = context.apiSuccess(MODULE, `${label} 分配独立候选货物`, assignResponse, '', OPTIONS)
|
|
|
|
|
const detailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
|
|
|
|
|
const container = context.apiSuccess(MODULE, `${label} 读取柜次`, detailResponse, '', OPTIONS)
|
|
|
|
|
const containerNo = `MAN-${suffix(context, label)}`.slice(0, 64)
|
|
|
|
|
const transportResponse = await context.request('PATCH', `${CONTAINERS}/${containerId}/transport`, {
|
|
|
|
|
containerNo, voyageNo: `V-${suffix(context, label)}`.slice(0, 64), version: Number(container.version),
|
|
|
|
|
reason: `${context.marker} 人工状态样本`,
|
|
|
|
|
})
|
2026-08-14 22:59:10 +08:00
|
|
|
const transported = context.apiSuccess(MODULE, `${label} 写入真实集装箱号并开放 PDA`, transportResponse, '', OPTIONS)
|
|
|
|
|
if (transported?.containerNo !== containerNo || id(transported?.id) !== containerId
|
|
|
|
|
|| transported?.cabinetNo !== container?.cabinetNo) {
|
|
|
|
|
throw new Error(`${label} 真实集装箱号未绑定到稳定柜次`)
|
|
|
|
|
}
|
|
|
|
|
const pushResponse = await post(context, `${SMART}/plans/${plan.id}/push-preload`,
|
|
|
|
|
{ expectedVersion: Number(plan.version) }, `${label}-push`)
|
|
|
|
|
plan = context.apiSuccess(MODULE, `${label} 补真实柜号后推送预装`, pushResponse, '', OPTIONS)
|
2026-08-15 17:40:01 +08:00
|
|
|
const planId = id(plan?.id)
|
|
|
|
|
const unconfirmedTaskResponse = await context.request('GET',
|
|
|
|
|
`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`)
|
|
|
|
|
context.addStep(MODULE, `${label} 计划确认前不生成 PDA 任务`, unconfirmedTaskResponse,
|
|
|
|
|
unconfirmedTaskResponse.http === 200 && String(unconfirmedTaskResponse?.body?.code) === 'A0404',
|
|
|
|
|
`HTTP=${unconfirmedTaskResponse.http}, code=${unconfirmedTaskResponse?.body?.code ?? 'missing'}, requestId=${unconfirmedTaskResponse.requestId || 'missing'}, traceId=${unconfirmedTaskResponse.traceId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const confirmableResponse = await context.request('GET',
|
|
|
|
|
`${LOADING}/loading-plans/${planId}?warehouseId=${source.warehouseId}`)
|
|
|
|
|
const confirmable = context.apiSuccess(MODULE, `${label} 读取计划级确认即时版本`, confirmableResponse, '', OPTIONS)
|
|
|
|
|
const confirmableVersion = Number(confirmable?.summary?.version)
|
|
|
|
|
context.assert(MODULE, `${label} 整计划允许确认且 PDA 尚未就绪`, confirmableResponse,
|
|
|
|
|
Number.isInteger(confirmableVersion) && confirmable?.summary?.status === 'EDITING'
|
|
|
|
|
&& confirmable?.summary?.readiness?.canConfirm === true
|
|
|
|
|
&& confirmable?.summary?.readiness?.readyForPda === false
|
|
|
|
|
&& confirmable?.summary?.readiness?.pdaTaskId == null,
|
|
|
|
|
`version=${confirmable?.summary?.version ?? 'missing'}, canConfirm=${confirmable?.summary?.readiness?.canConfirm}, pdaTaskId=${id(confirmable?.summary?.readiness?.pdaTaskId) || 'none'}`, OPTIONS)
|
|
|
|
|
if (!Number.isInteger(confirmableVersion) || confirmable?.summary?.readiness?.canConfirm !== true) {
|
|
|
|
|
throw new Error(`${label} 计划尚未满足整计划确认门禁`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmPath = `${LOADING}/loading-plans/${planId}/confirm`
|
|
|
|
|
const confirmBody = Object.freeze({ expectedVersion: confirmableVersion })
|
|
|
|
|
const confirmHeaders = Object.freeze({
|
|
|
|
|
'Idempotency-Key': operationId(context, `${label}-plan-confirm`),
|
|
|
|
|
})
|
|
|
|
|
const confirmResponse = await context.request('POST', confirmPath, confirmBody, context.token, confirmHeaders)
|
|
|
|
|
const confirmed = context.apiSuccess(MODULE, `${label} 计划级确认并生成唯一 PDA 任务`, confirmResponse, '', OPTIONS)
|
|
|
|
|
const confirmedTaskId = id(confirmed?.readiness?.pdaTaskId)
|
|
|
|
|
context.assert(MODULE, `${label} 计划确认返回 CONFIRMED 与 readyForPda`, confirmResponse,
|
|
|
|
|
id(confirmed?.planId) === planId && id(confirmed?.containerBookingId) === containerId
|
|
|
|
|
&& confirmed?.confirmationStatus === 'CONFIRMED'
|
|
|
|
|
&& confirmed?.readiness?.confirmationStatus === 'CONFIRMED'
|
|
|
|
|
&& confirmed?.readiness?.readyForPda === true && Boolean(confirmedTaskId)
|
|
|
|
|
&& Number(confirmed?.version) > confirmableVersion,
|
|
|
|
|
`confirmationStatus=${confirmed?.confirmationStatus || 'missing'}, readyForPda=${confirmed?.readiness?.readyForPda}, pdaTaskId=${confirmedTaskId || 'missing'}`, OPTIONS)
|
|
|
|
|
if (!confirmedTaskId || confirmed?.confirmationStatus !== 'CONFIRMED') {
|
|
|
|
|
throw new Error(`${label} 计划确认未生成唯一 PDA 任务`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const confirmReplay = await context.request('POST', confirmPath, confirmBody, context.token, confirmHeaders)
|
|
|
|
|
context.assert(MODULE, `${label} 计划确认同键同参精确重放`, confirmReplay,
|
|
|
|
|
confirmReplay.http === 200 && String(confirmReplay?.body?.code) === '200'
|
|
|
|
|
&& id(confirmReplay?.body?.data?.readiness?.pdaTaskId) === confirmedTaskId
|
|
|
|
|
&& Number(confirmReplay?.body?.data?.version) === Number(confirmed.version),
|
|
|
|
|
`HTTP=${confirmReplay.http}, code=${confirmReplay?.body?.code ?? 'missing'}, requestId=${confirmReplay.requestId || 'missing'}, traceId=${confirmReplay.traceId || 'missing'}`, OPTIONS)
|
|
|
|
|
|
2026-08-12 20:36:11 +08:00
|
|
|
const taskResponse = await context.request('GET',
|
2026-08-15 17:40:01 +08:00
|
|
|
`${PDA}/tasks?loadingPlanId=${planId}&warehouseId=${source.warehouseId}`)
|
2026-08-12 20:36:11 +08:00
|
|
|
const task = context.apiSuccess(MODULE, `${label} 读取 PDA 任务`, taskResponse, '', OPTIONS)
|
2026-08-15 17:40:01 +08:00
|
|
|
if (id(task?.id) !== confirmedTaskId || !task?.items?.[0]?.id) throw new Error(`${label} PDA 任务未生成或身份漂移`)
|
2026-08-12 20:36:11 +08:00
|
|
|
return { source, containerId, containerNo, plan, task }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function claim(context, chain, label) {
|
|
|
|
|
const response = await context.request('POST', `${PDA}/tasks/${chain.task.id}/claim`, {
|
|
|
|
|
expectedAssignmentVersion: Number(chain.task?.assignment?.version || 0),
|
|
|
|
|
operationId: operationId(context, `${label}-claim`),
|
|
|
|
|
})
|
|
|
|
|
chain.task = context.apiSuccess(MODULE, `${label} 领取 PDA 任务`, response, '', OPTIONS)
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function pickAll(context, chain, label) {
|
|
|
|
|
const snapshotResponse = await context.request('GET',
|
|
|
|
|
`${PDA}/tasks/${chain.task.id}/inventory-snapshot?warehouseId=${chain.source.warehouseId}`)
|
|
|
|
|
const snapshot = context.apiSuccess(MODULE, `${label} 获取 PDA 库存快照`, snapshotResponse, '', OPTIONS)
|
|
|
|
|
const response = await post(context, `${PDA}/tasks/${chain.task.id}/batch`, {
|
|
|
|
|
warehouseId: chain.source.warehouseId, loadingPlanId: id(chain.task.loadingPlanId),
|
2026-08-13 11:57:33 +08:00
|
|
|
containerBookingId: chain.containerId,
|
2026-08-12 20:36:11 +08:00
|
|
|
expectedVersion: Number(chain.task.version), inventorySnapshotToken: snapshot.snapshotToken,
|
|
|
|
|
items: chain.task.items.map((item) => ({ itemId: id(item.id), action: 'PICK',
|
|
|
|
|
pickedPackageCount: Number(item.plannedPackageCount), finalPickedPackageCount: null,
|
|
|
|
|
releaseMode: 'MANUAL', locationIds: [], version: Number(item.version), reason: null })),
|
|
|
|
|
}, `${label}-batch`)
|
|
|
|
|
chain.task = context.apiSuccess(MODULE, `${label} 完成 PDA 拣货`, response, '', OPTIONS)
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function complete(context, chain, label) {
|
|
|
|
|
const response = await context.request('POST', `${PDA}/tasks/${chain.task.id}/complete`, {
|
|
|
|
|
expectedTaskVersion: Number(chain.task.version), operationId: operationId(context, `${label}-complete`),
|
|
|
|
|
remark: `${context.marker} 人工完成状态样本`,
|
|
|
|
|
})
|
|
|
|
|
chain.task = context.apiSuccess(MODULE, `${label} 人工确认装柜完成`, response, '', OPTIONS)
|
|
|
|
|
return chain
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verifyContainer(context, status, fixture) {
|
|
|
|
|
const response = await context.request('GET', `${CONTAINERS}?status=${status}&page=1&size=100`)
|
|
|
|
|
const page = context.apiSuccess(MODULE, `${status} 货柜可按列表状态筛选`, response, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${status} 货柜样本保留`, response,
|
|
|
|
|
records(page).some((item) => id(item.id) === fixture.id && item.planStatus === status),
|
|
|
|
|
`id=${fixture.id}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verifyPlan(context, status, fixture) {
|
|
|
|
|
const response = await context.request('GET',
|
|
|
|
|
`${LOADING}/loading-plans?status=${status}&warehouseId=${fixture.warehouseId}&page=1&size=100`)
|
|
|
|
|
const page = context.apiSuccess(MODULE, `${status} 预装清单可筛选`, response, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${status} 预装清单样本保留`, response,
|
|
|
|
|
records(page).some((item) => id(item.planId) === fixture.id && item.status === status),
|
|
|
|
|
`planId=${fixture.id}`, OPTIONS)
|
|
|
|
|
const detailResponse = await context.request('GET',
|
|
|
|
|
`${LOADING}/loading-plans/${fixture.id}?warehouseId=${fixture.warehouseId}`)
|
|
|
|
|
const detail = context.apiSuccess(MODULE, `${status} 预装清单详情可打开`, detailResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${status} 预装清单详情身份正确`, detailResponse,
|
|
|
|
|
id(detail?.summary?.planId) === fixture.id && detail?.summary?.status === status,
|
|
|
|
|
`planId=${fixture.id}, actual=${detail?.summary?.status || 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verifyTask(context, status, fixture) {
|
|
|
|
|
const response = await context.request('GET',
|
|
|
|
|
`${PDA}/tasks/${fixture.id}?warehouseId=${fixture.warehouseId}`)
|
|
|
|
|
const task = context.apiSuccess(MODULE, `${status} PDA 任务可打开`, response, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, `${status} PDA 任务样本保留`, response,
|
|
|
|
|
id(task?.id) === fixture.id && task?.workflowStatus === status,
|
|
|
|
|
`taskId=${fixture.id}, actual=${task?.workflowStatus || 'missing'}`, OPTIONS)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fixture(value, status, detailPath, extra = {}) {
|
|
|
|
|
return { id: id(value), status, detailPath, ...extra }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-13 09:49:55 +08:00
|
|
|
async function completeSimpleDocuments(context, chain, matrix) {
|
|
|
|
|
const reason = `${context.marker} 人工页面动作验收`
|
|
|
|
|
const clearancePath = `${CONTAINERS}/${chain.containerId}/clearance`
|
|
|
|
|
const clearanceUpload = await uploadDocument(context, `${clearancePath}/document/upload`, 0,
|
|
|
|
|
reason, 'container-clearance')
|
|
|
|
|
const clearanceDocument = context.apiSuccess(MODULE, '货柜公开接口上传目的港清关资料',
|
|
|
|
|
clearanceUpload, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '货柜清关上传返回真实文件事实', clearanceUpload,
|
|
|
|
|
Boolean(clearanceDocument?.id) && Boolean(clearanceDocument?.fileId)
|
|
|
|
|
&& clearanceDocument?.documentType === 'DESTINATION_CLEARANCE'
|
|
|
|
|
&& /^[0-9a-f]{64}$/i.test(String(clearanceDocument?.fileHash || ''))
|
|
|
|
|
&& !clearanceDocument?.completedAt,
|
|
|
|
|
`documentId=${id(clearanceDocument?.id) || 'missing'}, version=${clearanceDocument?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!clearanceDocument?.id || !clearanceDocument?.fileId || !Number.isInteger(Number(clearanceDocument?.version))) return
|
|
|
|
|
const clearanceCompleteResponse = await post(context, `${clearancePath}/complete`, {
|
|
|
|
|
expectedVersion: Number(clearanceDocument?.version), operationReason: reason,
|
|
|
|
|
}, 'container-clearance-complete')
|
|
|
|
|
const clearanceCompleted = context.apiSuccess(MODULE, '货柜点击完成目的港清关',
|
|
|
|
|
clearanceCompleteResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '货柜清关完成事实可回读', clearanceCompleteResponse,
|
|
|
|
|
id(clearanceCompleted?.id) === id(clearanceDocument?.id) && Boolean(clearanceCompleted?.completedAt)
|
|
|
|
|
&& Number(clearanceCompleted?.version) === Number(clearanceDocument?.version) + 1,
|
|
|
|
|
`documentId=${id(clearanceCompleted?.id) || 'missing'}, completedAt=${clearanceCompleted?.completedAt || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
const customsPath = `${LOADING}/loading-lists/${chain.plan.id}/customs`
|
|
|
|
|
const warehouse = `warehouseId=${encodeURIComponent(chain.source.warehouseId)}`
|
|
|
|
|
const customsUpload = await uploadDocument(context, `${customsPath}/document/upload?${warehouse}`, 0,
|
|
|
|
|
reason, 'loading-customs')
|
|
|
|
|
const customsDocument = context.apiSuccess(MODULE, '装柜清单公开接口上传出口报关文档',
|
|
|
|
|
customsUpload, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '装柜清单报关上传返回真实文件事实', customsUpload,
|
|
|
|
|
Boolean(customsDocument?.id) && Boolean(customsDocument?.fileId)
|
|
|
|
|
&& customsDocument?.documentType === 'CUSTOMS_DECLARATION'
|
|
|
|
|
&& /^[0-9a-f]{64}$/i.test(String(customsDocument?.fileHash || ''))
|
|
|
|
|
&& !customsDocument?.completedAt,
|
|
|
|
|
`documentId=${id(customsDocument?.id) || 'missing'}, version=${customsDocument?.version ?? 'missing'}`, OPTIONS)
|
|
|
|
|
if (!customsDocument?.id || !customsDocument?.fileId || !Number.isInteger(Number(customsDocument?.version))) return
|
|
|
|
|
const customsCompleteResponse = await post(context, `${customsPath}/complete?${warehouse}`, {
|
|
|
|
|
expectedVersion: Number(customsDocument?.version), operationReason: reason,
|
|
|
|
|
}, 'loading-customs-complete')
|
|
|
|
|
const customsCompleted = context.apiSuccess(MODULE, '装柜清单点击完成出口报关',
|
|
|
|
|
customsCompleteResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, '装柜清单报关完成事实可回读', customsCompleteResponse,
|
|
|
|
|
id(customsCompleted?.id) === id(customsDocument?.id) && Boolean(customsCompleted?.completedAt)
|
|
|
|
|
&& Number(customsCompleted?.version) === Number(customsDocument?.version) + 1,
|
|
|
|
|
`documentId=${id(customsCompleted?.id) || 'missing'}, completedAt=${customsCompleted?.completedAt || 'missing'}`, OPTIONS)
|
|
|
|
|
|
|
|
|
|
matrix.pages.containerLoading.actions = { clearance: fixture(chain.containerId, 'COMPLETED',
|
|
|
|
|
`/container-loading/${chain.containerId}/smart-loading`, {
|
|
|
|
|
documentId: id(clearanceCompleted?.id), documentVersion: Number(clearanceCompleted?.version),
|
|
|
|
|
completedAt: clearanceCompleted?.completedAt || null,
|
|
|
|
|
}) }
|
|
|
|
|
matrix.pages.loadingLists.actions = { customsDeclaration: fixture(chain.plan.id, 'COMPLETED',
|
|
|
|
|
`/loading-lists?planId=${chain.plan.id}&warehouseId=${chain.source.warehouseId}`, {
|
|
|
|
|
warehouseId: chain.source.warehouseId, documentId: id(customsCompleted?.id),
|
|
|
|
|
documentVersion: Number(customsCompleted?.version), completedAt: customsCompleted?.completedAt || null,
|
|
|
|
|
}) }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-12 20:36:11 +08:00
|
|
|
export async function run(context, matrix = context.createdResources.manualStateMatrix) {
|
2026-08-14 23:14:11 +08:00
|
|
|
if (!matrix?.pages) {
|
|
|
|
|
context.addStep(MODULE, '人工装柜状态模块由人工矩阵统一编排', null, false,
|
|
|
|
|
'missing=manualStateMatrix.pages;请通过 manual-state-matrix 入口执行', OPTIONS)
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-08-13 09:49:55 +08:00
|
|
|
matrix.pages.containerLoading = { requiredStatuses: CONTAINER_STATUSES,
|
|
|
|
|
requiredActions: ['clearance'], fixtures: {}, actions: {} }
|
2026-08-12 20:36:11 +08:00
|
|
|
matrix.pages.preloadPlans = { requiredStatuses: PRELOAD_STATUSES, fixtures: {} }
|
2026-08-13 09:49:55 +08:00
|
|
|
matrix.pages.loadingLists = { requiredStatuses: ['COMPLETED'],
|
|
|
|
|
requiredActions: ['customsDeclaration'], fixtures: {}, actions: {} }
|
2026-08-12 20:36:11 +08:00
|
|
|
matrix.pages.pdaTasks = { requiredStatuses: PDA_STATUSES, fixtures: {} }
|
|
|
|
|
|
2026-08-13 11:57:33 +08:00
|
|
|
const waitingSource = await readInbound(context, await prepareInbound(context, 'WAIT'))
|
|
|
|
|
const waitingContainerId = await createConfirmedContainer(context, waitingSource, 'WAIT')
|
|
|
|
|
matrix.pages.containerLoading.fixtures.WAIT_LOADING = fixture(waitingContainerId, 'WAIT_LOADING',
|
|
|
|
|
`/container-loading?containerId=${waitingContainerId}`)
|
2026-08-12 20:36:11 +08:00
|
|
|
|
|
|
|
|
const editing = await createPlanTask(context, 'EDIT')
|
|
|
|
|
const inProgress = await claim(context, await createPlanTask(context, 'PROGRESS'), 'PROGRESS')
|
|
|
|
|
const awaiting = await pickAll(context,
|
|
|
|
|
await claim(context, await createPlanTask(context, 'REVIEW'), 'REVIEW'), 'REVIEW')
|
|
|
|
|
const completed = await complete(context, await pickAll(context,
|
|
|
|
|
await claim(context, await createPlanTask(context, 'DONE'), 'DONE'), 'DONE'), 'DONE')
|
|
|
|
|
|
|
|
|
|
const chains = { EDITING: editing, LOADING: inProgress, COMPLETED: completed }
|
|
|
|
|
for (const [status, chain] of Object.entries(chains)) {
|
|
|
|
|
matrix.pages.containerLoading.fixtures[status] = fixture(chain.containerId, status,
|
|
|
|
|
`/container-loading/${chain.containerId}/smart-loading`, { planId: id(chain.plan.id) })
|
|
|
|
|
}
|
|
|
|
|
for (const [status, chain] of Object.entries({ EDITING: editing, LOADING: inProgress })) {
|
|
|
|
|
matrix.pages.preloadPlans.fixtures[status] = fixture(chain.plan.id, status,
|
|
|
|
|
`/loading-plans/${chain.plan.id}?warehouseId=${chain.source.warehouseId}`,
|
|
|
|
|
{ warehouseId: chain.source.warehouseId })
|
|
|
|
|
}
|
|
|
|
|
matrix.pages.loadingLists.fixtures.COMPLETED = fixture(completed.plan.id, 'COMPLETED',
|
|
|
|
|
`/loading-lists?planId=${completed.plan.id}&warehouseId=${completed.source.warehouseId}`,
|
|
|
|
|
{ warehouseId: completed.source.warehouseId })
|
|
|
|
|
|
|
|
|
|
const taskChains = { UNASSIGNED: editing, IN_PROGRESS: inProgress, AWAITING_REVIEW: awaiting, COMPLETED: completed }
|
|
|
|
|
for (const [status, chain] of Object.entries(taskChains)) {
|
|
|
|
|
matrix.pages.pdaTasks.fixtures[status] = fixture(chain.task.id, status,
|
|
|
|
|
`/pages/picking/picking?taskId=${chain.task.id}&warehouseId=${chain.source.warehouseId}`,
|
|
|
|
|
{ warehouseId: chain.source.warehouseId, loadingPlanId: id(chain.plan.id) })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const [status, value] of Object.entries(matrix.pages.containerLoading.fixtures))
|
|
|
|
|
await verifyContainer(context, status, value)
|
|
|
|
|
for (const [status, value] of Object.entries(matrix.pages.preloadPlans.fixtures))
|
|
|
|
|
await verifyPlan(context, status, value)
|
|
|
|
|
for (const [status, value] of Object.entries(matrix.pages.pdaTasks.fixtures))
|
|
|
|
|
await verifyTask(context, status, value)
|
|
|
|
|
|
|
|
|
|
const listFixture = matrix.pages.loadingLists.fixtures.COMPLETED
|
|
|
|
|
const listResponse = await context.request('GET',
|
|
|
|
|
`${LOADING}/loading-lists?warehouseId=${listFixture.warehouseId}&page=1&size=100`)
|
|
|
|
|
const list = context.apiSuccess(MODULE, 'COMPLETED 装柜清单可从列表打开', listResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, 'COMPLETED 装柜清单样本保留', listResponse,
|
|
|
|
|
records(list).some((item) => id(item.planId) === listFixture.id && item.status === 'COMPLETED'),
|
|
|
|
|
`planId=${listFixture.id}`, OPTIONS)
|
|
|
|
|
const listDetailResponse = await context.request('GET',
|
|
|
|
|
`${LOADING}/loading-lists/${listFixture.id}?warehouseId=${listFixture.warehouseId}`)
|
|
|
|
|
const listDetail = context.apiSuccess(MODULE, 'COMPLETED 装柜清单详情可打开', listDetailResponse, '', OPTIONS)
|
|
|
|
|
context.assert(MODULE, 'COMPLETED 装柜清单详情身份正确', listDetailResponse,
|
|
|
|
|
id(listDetail?.summary?.planId) === listFixture.id && listDetail?.summary?.status === 'COMPLETED',
|
|
|
|
|
`planId=${listFixture.id}, actual=${listDetail?.summary?.status || 'missing'}`, OPTIONS)
|
2026-08-13 09:49:55 +08:00
|
|
|
|
|
|
|
|
await completeSimpleDocuments(context, completed, matrix)
|
2026-08-12 20:36:11 +08:00
|
|
|
}
|