Files
baiyue-firstleg/scripts/admin-test-env-modules/phase3-loading.mjs
T

891 lines
64 KiB
JavaScript
Raw Normal View History

2026-08-08 21:14:39 +08:00
const MODULE = 'phase3-loading'
const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true }
2026-08-11 11:41:41 +08:00
const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin'
const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!'
2026-08-08 21:14:39 +08:00
function id(value) { return value == null ? '' : String(value) }
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function failure(response) {
return [400, 401, 403, 404, 409, 503].includes(response?.http)
|| (response?.http === 200 && codeOf(response) !== '200')
}
async function request(context, method, path, body, headers = {}) {
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method,
headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers },
body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'), traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } }
}
async function uploadEvidence(context, tenantId, inboundId, evidenceType) {
const path = `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}?evidenceType=${encodeURIComponent(evidenceType)}`
2026-08-08 21:14:39 +08:00
const form = new FormData()
const png = Uint8Array.from([137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,0,0,0,13,73,68,65,84,8,215,99,248,207,192,240,31,0,5,0,1,255,137,153,61,29,0,0,0,0,73,69,78,68,174,66,96,130])
const fileName = `${context.marker}-${evidenceType.toLowerCase()}.png`
form.append('file', new Blob([png], { type: 'image/png' }), fileName)
2026-08-08 21:14:39 +08:00
try {
const response = await fetch(`${context.options.gateway}${path}`, {
method: 'POST', headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}` },
body: form, signal: AbortSignal.timeout(60_000),
})
const raw = await response.text(); let payload = null
try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ }
return { method: 'POST', path, http: response.status, body: payload,
raw: payload === null ? raw.slice(0, 500) : null,
requestId: payload?.requestId || response.headers.get('x-request-id'),
traceId: payload?.traceId || response.headers.get('x-trace-id') }
} catch (error) {
return { method: 'POST', path, http: 0, body: null, raw: String(error), requestId: null, traceId: null }
}
}
async function bindEvidence(context, base, tenantId, evidenceType, fileId, suffix, keySuffix = evidenceType) {
return request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0,
evidenceType,
fileId,
fileName: 'client-must-not-override.png',
sealNo: evidenceType === 'SEAL' ? `SEAL-${suffix}` : null,
remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${keySuffix}`.slice(0, 128) })
}
2026-08-08 21:14:39 +08:00
function fixture(context) {
2026-08-12 10:59:39 +08:00
return context.createdResources?.orchestrationFixtures?.phase3Loading
|| context.createdResources?.orchestrationFixtures?.inboundMeasurement
2026-08-08 21:14:39 +08:00
|| context.createdResources?.inboundMeasurement
|| context.createdResources?.orders
}
2026-08-11 20:10:24 +08:00
function inventorySignature(snapshot) {
return JSON.stringify((snapshot?.inbounds || []).map((inbound) => ({
inboundId: id(inbound?.inboundId),
availablePackageCount: Number(inbound?.availablePackageCount),
resources: (inbound?.resources || []).map((resource) => ({
reservationId: id(resource?.reservationId), occupancyId: id(resource?.occupancyId),
palletId: id(resource?.palletId), locationId: id(resource?.locationId),
sharedOverflow: resource?.sharedOverflow === true,
availablePackageCount: Number(resource?.availablePackageCount),
})),
})))
}
function operationsSignature(operations) {
return JSON.stringify((operations || []).map((operation) => ({
id: id(operation?.id), operationId: operation?.operationId || null,
actionType: operation?.actionType || null, operatorId: id(operation?.operatorId),
createdAt: operation?.createdAt || null,
})))
}
2026-08-12 13:24:17 +08:00
function taskItemsSignature(task) {
return JSON.stringify((task?.items || []).map((item) => ({
id: id(item?.id), status: item?.itemStatus || null,
picked: Number(item?.pickedPackageCount), version: Number(item?.version),
})))
}
2026-08-08 21:14:39 +08:00
export async function run(context) {
const source = fixture(context)
const tenantId = id(source?.tenantId)
const inboundId = id(source?.inboundId)
if (!tenantId || !inboundId) {
context.addStep(MODULE, '取得本轮 WAIT_SPLIT 隔离入库单', null, false,
'未找到 orders/inbound-measurement 本轮隔离 fixture,未使用历史入库单', OPTIONS)
return
}
const detailPath = `/api/platform/inbounds/${inboundId}?tenantId=${encodeURIComponent(tenantId)}`
const initialResponse = await context.request('GET', detailPath)
const initial = context.apiSuccess(MODULE, '读取三期隔离入库单状态与版本', initialResponse, `tenantId=${tenantId}, inboundId=${inboundId}`, OPTIONS)
const initialStatus = initial?.summary?.processStatus
const initialVersion = Number(initial?.summary?.version)
context.assert(MODULE, '隔离入库单处于智能分柜门禁状态', initialResponse,
2026-08-13 14:55:50 +08:00
initialStatus === 'WAIT_SPLIT' && Number.isInteger(initialVersion),
2026-08-08 21:14:39 +08:00
`status=${initialStatus || 'missing'}, version=${initial?.summary?.version ?? 'missing'}`, OPTIONS)
2026-08-13 14:55:50 +08:00
if (initialStatus !== 'WAIT_SPLIT' || !Number.isInteger(initialVersion)) return
2026-08-08 21:14:39 +08:00
const base = `/api/platform/v1/inbounds/${inboundId}`
const suffix = context.marker.replace(/\D/g, '').slice(-10)
const warehouseId = id(initial?.summary?.warehouseId)
const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading'
const pdaBase = '/api/platform/v1/first-leg/pda-loading'
2026-08-13 19:19:29 +08:00
const bookingDraft = {
customerId: tenantId, customerName: initial?.summary?.customerName,
warehouseId, warehouseName: initial?.summary?.warehouseName,
2026-08-14 18:50:07 +08:00
channelCode: initial?.summary?.channelCode,
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128),
2026-08-13 19:19:29 +08:00
shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`,
vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64),
2026-08-13 19:19:29 +08:00
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO',
estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00',
2026-08-13 19:19:29 +08:00
remark: context.marker,
parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` },
{ partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` },
{ partyType: 'NOTIFY_PARTY', partyName: `P3-NOTIFY-${suffix}` }],
containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', capacityCbm: 100,
maxWeightKg: 100000, quantity: 1 }],
}
const simulationResponse = await request(context, 'POST', '/api/platform/v1/containers', {
channelCode: initial?.summary?.channelCode, warehouseId, containerType: '40HQ',
capacityCbm: 100, maxWeightKg: 100000,
})
const simulation = context.apiSuccess(MODULE, '新增三期内部模拟柜', simulationResponse, '', OPTIONS)
const containerBookingId = id(simulation?.id)
context.assert(MODULE, '三期模拟柜不要求真实柜号或订舱', simulationResponse,
Boolean(containerBookingId) && Boolean(simulation?.cabinetNo) && simulation?.containerNo == null
&& simulation?.oceanBookingId == null,
`containerBookingId=${containerBookingId || 'missing'}, cabinetNo=${simulation?.cabinetNo || 'missing'}`, OPTIONS)
if (!containerBookingId) return
2026-08-13 19:19:29 +08:00
const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings',
{ expectedVersion: 0, data: bookingDraft },
{ 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) })
const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS)
const bookingId = id(bookingCreated?.oceanBooking?.id)
if (!bookingId) return
const bookingConfirmed = bookingCreated
const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ')
context.assert(MODULE, '一次登记直接返回 CONFIRMED 且不自动生成三期内部柜', bookingCreateResponse,
bookingConfirmed?.oceanBooking?.status === 'CONFIRMED' && Boolean(requirement?.id)
&& bookingConfirmed?.containerAllocations?.length === 0,
`status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, requirementId=${id(requirement?.id) || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
if (!requirement?.id) return
const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, {
oceanBookingId: bookingId, requirementId: id(requirement.id), expectedVersion: Number(simulation.version),
})
const linked = context.apiSuccess(MODULE, '关联三期模拟柜与真实订舱', linkResponse, '', OPTIONS)
context.assert(MODULE, '三期订舱关联保持内部柜次身份', linkResponse,
id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo
&& id(linked?.oceanBookingId) === bookingId,
`containerBookingId=${containerBookingId}, cabinetNo=${linked?.cabinetNo || 'missing'}`, OPTIONS)
if (id(linked?.id) !== containerBookingId) return
2026-08-13 19:19:29 +08:00
const candidateResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const candidates = context.apiSuccess(MODULE, '查询本轮入库单柜次装柜候选', candidateResponse, '', OPTIONS)
const candidate = Array.isArray(candidates) ? candidates.find((item) => id(item.inboundId) === inboundId
&& Number(item.availablePackageCount) > 0) : null
context.assert(MODULE, '柜次装柜候选与入库单、客户和仓库一致', candidateResponse,
Boolean(candidate?.inboundDetailId) && id(candidate?.accountId) === tenantId
&& id(candidate?.warehouseId) === warehouseId,
`candidate=${Boolean(candidate)}, warehouseId=${warehouseId || 'missing'}`, OPTIONS)
if (!candidate?.inboundDetailId || !warehouseId) return
const containerNo = `P3B-${suffix}`
const loadingPlanResponse = await request(context, 'POST', `${smartLoadingBase}/plans`, {
bookingId: containerBookingId, remark: context.marker,
})
const loadingPlan = context.apiSuccess(MODULE, '创建柜次级智能装柜计划', loadingPlanResponse, '', OPTIONS)
const loadingPlanId = id(loadingPlan?.id)
if (!loadingPlanId) return
const assignmentResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, {
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(loadingPlan.version), operationId: `${context.marker}-bridge-assign`.slice(0, 128),
})
const assignedLoadingPlan = context.apiSuccess(MODULE, '将本轮入库货物分配到柜次计划', assignmentResponse, '', OPTIONS)
context.assert(MODULE, '柜次计划明细和版本真实持久化', assignmentResponse,
assignedLoadingPlan?.items?.some?.((item) => id(item.inboundId) === inboundId && Number(item.packageCount) === 1)
&& Number(assignedLoadingPlan?.version) === 1,
`loadingPlanId=${loadingPlanId}, version=${assignedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
2026-08-08 21:14:39 +08:00
const smart = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0,
items: [{ containerCode: `P3-${suffix}`, cargoReference: `REF-${suffix}`, packageCount: 1, volumeCbm: 0.01, remark: context.marker }],
remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
const smartData = context.apiSuccess(MODULE, '智能分柜并生成预装清单', smart, `inboundId=${inboundId}`, OPTIONS)
const planId = id(smartData?.preloadPlan?.id)
context.assert(MODULE, '智能分柜预装计划 PENDING、版本和明细正确', smart,
2026-08-13 14:55:50 +08:00
smartData?.mainStatus === 'WAIT_SPLIT' && Boolean(planId) && smartData?.preloadPlan?.status === 'PENDING'
2026-08-08 21:14:39 +08:00
&& Number(smartData?.preloadPlan?.version) === 0 && smartData?.preloadPlan?.items?.length === 1,
`mainStatus=${smartData?.mainStatus || 'missing'}, planId=${planId || 'missing'}, status=${smartData?.preloadPlan?.status || 'missing'}`, OPTIONS)
if (!planId) return
const replay = await request(context, 'POST', `${base}/split/smart?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0, items: [], remark: 'replay must not replace',
}, { 'Idempotency-Key': `${context.marker}-p3-smart`.slice(0, 128) })
context.assert(MODULE, '智能分柜幂等重放返回同一预装计划', replay,
codeOf(replay) === '200' && id(replay.body?.data?.preloadPlan?.id) === planId
&& replay.body?.data?.preloadPlan?.items?.length === 1,
`HTTP=${replay.http}, code=${codeOf(replay)}, planId=${id(replay.body?.data?.preloadPlan?.id) || 'missing'}`, OPTIONS)
const bindResponse = await request(context, 'POST',
`${base}/preload/loading-plans/${loadingPlanId}?tenantId=${encodeURIComponent(tenantId)}`, {
expectedPreloadVersion: Number(smartData.preloadPlan.version),
expectedLoadingPlanVersion: Number(assignedLoadingPlan.version),
}, { 'Idempotency-Key': `${context.marker}-bridge-bind`.slice(0, 128) })
const link = context.apiSuccess(MODULE, '原子关联入库预装与柜次装柜计划', bindResponse, '', OPTIONS)
context.assert(MODULE, '桥接双版本递增且关联归属唯一', bindResponse,
id(link?.inboundId) === inboundId && id(link?.loadingPlanId) === loadingPlanId
&& id(link?.preloadPlanId) === planId && Number(link?.allocatedPackageCount) === 1
&& Number(link?.preloadVersion) === 1 && Number(link?.loadingPlanVersion) === 2,
`linkId=${id(link?.id) || 'missing'}, preloadVersion=${link?.preloadVersion ?? 'missing'}, loadingVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS)
if (!link?.id) return
const missingContainerPushResponse = await request(context, 'POST',
`${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
{ expectedVersion: Number(link.loadingPlanVersion) },
{ 'Idempotency-Key': `${context.marker}-bridge-push-without-container-no`.slice(0, 128) })
const missingContainerPushRejected = failure(missingContainerPushResponse)
context.addStep(MODULE, '无真实柜号拒绝推送柜次计划', missingContainerPushResponse,
missingContainerPushRejected,
`HTTP=${missingContainerPushResponse.http}, code=${codeOf(missingContainerPushResponse)}, requestId=${missingContainerPushResponse.requestId || 'missing'}, traceId=${missingContainerPushResponse.traceId || 'missing'}`, OPTIONS)
if (!missingContainerPushRejected) return
const missingContainerTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const missingContainerTaskAbsent = failure(missingContainerTaskResponse)
|| !id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id)
context.addStep(MODULE, '无真实柜号推送失败后不生成 PDA 任务', missingContainerTaskResponse,
missingContainerTaskAbsent,
`HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, task=${id(missingContainerTaskResponse?.body?.id || missingContainerTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS)
if (!missingContainerTaskAbsent) return
const containerBeforeTransportResponse = await context.request('GET',
`/api/platform/v1/containers/${containerBookingId}`)
const containerBeforeTransport = context.apiSuccess(MODULE, '读取补真实柜号前柜次最新版本',
containerBeforeTransportResponse, '', OPTIONS)
const transportVersion = Number(containerBeforeTransport?.version)
context.assert(MODULE, '补真实柜号使用订舱关联后的最新柜次版本', containerBeforeTransportResponse,
id(containerBeforeTransport?.id) === containerBookingId && Number.isInteger(transportVersion),
`containerBookingId=${id(containerBeforeTransport?.id) || 'missing'}, version=${containerBeforeTransport?.version ?? 'missing'}`, OPTIONS)
if (!Number.isInteger(transportVersion)) return
const transportResponse = await request(context, 'PATCH',
`/api/platform/v1/containers/${containerBookingId}/transport`, {
containerNo, voyageNo: `P3V-${suffix}`, version: transportVersion,
reason: `${context.marker}-bind-container-no`,
})
const transported = context.apiSuccess(MODULE, '绑定真实柜号后开放三期 PDA 推送门禁', transportResponse, '', OPTIONS)
context.assert(MODULE, '真实柜号绑定到稳定柜次且计划身份不变', transportResponse,
transported?.containerNo === containerNo && id(transported?.id) === containerBookingId
&& transported?.cabinetNo === linked.cabinetNo,
`containerNo=${transported?.containerNo || 'missing'}, containerBookingId=${id(transported?.id) || 'missing'}, loadingPlanId=${loadingPlanId}`, OPTIONS)
if (transported?.containerNo !== containerNo) return
2026-08-14 22:21:24 +08:00
const confirmedBookingTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const confirmedBookingTaskAbsent = failure(confirmedBookingTaskResponse)
|| !id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id)
context.addStep(MODULE, '订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务', confirmedBookingTaskResponse,
confirmedBookingTaskAbsent,
`HTTP=${confirmedBookingTaskResponse.http}, code=${codeOf(confirmedBookingTaskResponse)}, task=${id(confirmedBookingTaskResponse?.body?.id || confirmedBookingTaskResponse?.body?.data?.id) || 'none'}`, OPTIONS)
if (!confirmedBookingTaskAbsent) return
const sailingAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString()
const arrivalAt = new Date(Date.now() + 20 * 24 * 60 * 60 * 1000).toISOString()
const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, {
expectedVersion: Number(bookingConfirmed?.oceanBooking?.version),
billOfLadingNo: `P3BL-${suffix}`.slice(0, 128), vesselName: `P3VESSEL-${suffix}`.slice(0, 128),
voyageNo: `P3V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt,
estimatedArrivalAt: arrivalAt, reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) })
const bookingReleased = context.apiSuccess(MODULE, '三期订舱携带已冻结运输快照并放舱', releaseResponse, '', OPTIONS)
2026-08-14 22:21:24 +08:00
context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse,
bookingReleased?.oceanBooking?.status === 'RELEASED'
&& bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId),
`status=${bookingReleased?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS)
if (bookingReleased?.oceanBooking?.status !== 'RELEASED') return
const pushResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`,
{ expectedVersion: Number(link.loadingPlanVersion) },
{ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) })
2026-08-14 22:21:24 +08:00
const pushedLoadingPlan = context.apiSuccess(MODULE, '真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务', pushResponse, '', OPTIONS)
context.assert(MODULE, '推送后柜次计划保持 EDITING 且版本递增', pushResponse,
pushedLoadingPlan?.status === 'EDITING'
&& Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1,
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
2026-08-08 21:14:39 +08:00
const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: initialVersion, remark: 'must confirm preload first' },
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) })
context.addStep(MODULE, '预装未确认时拒绝完成装柜', blockedBeforePreloadConfirm,
failure(blockedBeforePreloadConfirm),
`HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS)
2026-08-08 21:14:39 +08:00
const canonicalTaskResponse = await context.request('GET',
2026-08-13 19:19:29 +08:00
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS)
const canonicalTaskId = id(canonicalTask?.id)
const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId)
context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse,
Boolean(canonicalTaskId) && id(canonicalTask?.loadingPlanId) === loadingPlanId
&& canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id),
`taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS)
if (!canonicalTaskId || !canonicalItem?.id) return
const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, {
expectedAssignmentVersion: Number(canonicalTask?.assignment?.version || 0),
operationId: `${context.marker}-bridge-claim`.slice(0, 128),
})
const claimed = context.apiSuccess(MODULE, '领取柜次 PDA 任务', claim, '', OPTIONS)
context.assert(MODULE, '柜次 PDA 任务绑定当前作业员', claim,
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
const claimedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划', claimedPlanResponse, '', OPTIONS)
context.assert(MODULE, '领取 PDA 任务后计划进入 LOADING 且版本递增', claimedPlanResponse,
claimedPlan?.status === 'LOADING' && Number(claimedPlan?.version) === Number(pushedLoadingPlan?.version) + 1,
`status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS)
2026-08-11 18:43:50 +08:00
const inventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const inventory = context.apiSuccess(MODULE, '读取 PDA 提交前实时库存快照', inventoryResponse, '', OPTIONS)
2026-08-11 20:10:24 +08:00
const inventoryInbound = inventory?.inbounds?.find?.((item) => id(item.inboundId) === inboundId)
const inventoryIdentityVerified = id(inventory?.taskId) === canonicalTaskId
2026-08-13 20:00:30 +08:00
&& id(inventory?.loadingPlanId) === loadingPlanId
&& id(inventory?.containerBookingId) === containerBookingId
2026-08-11 20:10:24 +08:00
&& Number(inventory?.taskVersion) === Number(claimed?.version)
2026-08-13 20:00:30 +08:00
&& id(inventory?.cabinetNo) && inventory?.currentContainerNo === containerNo
&& id(inventory?.warehouseId) === warehouseId
2026-08-11 20:10:24 +08:00
&& Boolean(inventory?.capturedAt) && typeof inventory?.snapshotToken === 'string'
&& inventory.snapshotToken.length === 64 && Number(inventoryInbound?.availablePackageCount) >= 1
&& (inventoryInbound?.resources || []).every((resource) => id(resource?.reservationId)
&& id(resource?.occupancyId) && id(resource?.palletId) && id(resource?.locationId)
&& Number(resource?.availablePackageCount) >= 0)
&& Number(inventoryInbound?.availablePackageCount) === (inventoryInbound?.resources || [])
.reduce((total, resource) => total + Number(resource?.availablePackageCount || 0), 0)
context.assert(MODULE, '实时库存快照归属、版本、资源数量和稳定令牌完整', inventoryResponse,
inventoryIdentityVerified,
`taskId=${id(inventory?.taskId) || 'missing'}, taskVersion=${inventory?.taskVersion ?? 'missing'}, warehouseId=${id(inventory?.warehouseId) || 'missing'}, token=${inventory?.snapshotToken ? 'present' : 'missing'}, available=${inventoryInbound?.availablePackageCount ?? 'missing'}`, OPTIONS)
2026-08-11 18:43:50 +08:00
if (!inventory?.snapshotToken) return
2026-08-11 20:10:24 +08:00
const repeatedInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const repeatedInventory = context.apiSuccess(MODULE, '未发生库存变化时重复读取实时库存快照', repeatedInventoryResponse, '', OPTIONS)
const inventorySnapshotVerified = inventoryIdentityVerified
&& repeatedInventory?.snapshotToken === inventory.snapshotToken
&& Number(repeatedInventory?.taskVersion) === Number(inventory.taskVersion)
&& inventorySignature(repeatedInventory) === inventorySignature(inventory)
context.assert(MODULE, '未变化库存返回稳定 token、版本和数量快照', repeatedInventoryResponse,
inventorySnapshotVerified,
`sameToken=${repeatedInventory?.snapshotToken === inventory.snapshotToken}, taskVersion=${repeatedInventory?.taskVersion ?? 'missing'}`, OPTIONS)
2026-08-11 11:41:41 +08:00
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
const batchPayload = {
2026-08-13 20:00:30 +08:00
warehouseId, loadingPlanId, containerBookingId,
2026-08-11 11:41:41 +08:00
expectedVersion: Number(claimed?.version),
2026-08-11 18:43:50 +08:00
inventorySnapshotToken: inventory.snapshotToken,
2026-08-11 11:41:41 +08:00
items: [{
itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1,
releaseMode: 'MANUAL', locationIds: [], version: Number(canonicalItem.version),
}],
}
2026-08-11 11:41:41 +08:00
const staleBatch = await request(context, 'POST', batchPath,
{ ...batchPayload, expectedVersion: Number(batchPayload.expectedVersion) + 1 },
{ 'Idempotency-Key': `${context.marker}-bridge-batch-stale`.slice(0, 128) })
context.addStep(MODULE, 'PDA 原子批次旧任务版本返回冲突', staleBatch,
staleBatch.http === 200 && codeOf(staleBatch) === 'A0409',
`HTTP=${staleBatch.http}, code=${codeOf(staleBatch)}`, OPTIONS)
2026-08-12 12:35:29 +08:00
const beforeInvalidTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
2026-08-12 12:35:29 +08:00
const beforeInvalidTask = context.apiSuccess(MODULE, '非法混合批次前读取 PDA 全量任务基线',
beforeInvalidTaskResponse, '', OPTIONS)
const beforeInvalidOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const beforeInvalidOperations = context.apiSuccess(MODULE, '非法混合批次前读取 PDA 操作历史基线',
beforeInvalidOperationsResponse, '', OPTIONS)
2026-08-11 11:41:41 +08:00
const invalidBatch = await request(context, 'POST', batchPath, {
...batchPayload,
items: [...batchPayload.items, {
itemId: '999999999999999999', action: 'SKIP', version: 0, reason: 'forged-item-must-rollback',
}],
}, { 'Idempotency-Key': `${context.marker}-bridge-batch-rollback`.slice(0, 128) })
context.addStep(MODULE, 'PDA 原子批次任一非法明细时整批拒绝', invalidBatch,
invalidBatch.http === 200 && codeOf(invalidBatch) === 'A0409',
`HTTP=${invalidBatch.http}, code=${codeOf(invalidBatch)}`, OPTIONS)
const unchangedAfterInvalidResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
2026-08-11 11:41:41 +08:00
const unchangedAfterInvalid = context.apiSuccess(MODULE, '回查失败批次后的 PDA 任务快照', unchangedAfterInvalidResponse, '', OPTIONS)
2026-08-12 12:35:29 +08:00
const afterInvalidOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const afterInvalidOperations = context.apiSuccess(MODULE, '非法混合批次后回查 PDA 操作历史',
afterInvalidOperationsResponse, '', OPTIONS)
context.assert(MODULE, '合法明细加非法明细失败后任务、全部明细和操作历史零写入', unchangedAfterInvalidResponse,
unchangedAfterInvalid?.taskStatus === beforeInvalidTask?.taskStatus
&& Number(unchangedAfterInvalid?.version) === Number(beforeInvalidTask?.version)
2026-08-12 13:24:17 +08:00
&& taskItemsSignature(unchangedAfterInvalid) === taskItemsSignature(beforeInvalidTask)
2026-08-12 12:35:29 +08:00
&& operationsSignature(afterInvalidOperations) === operationsSignature(beforeInvalidOperations),
2026-08-12 13:24:17 +08:00
`taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS)
2026-08-11 11:41:41 +08:00
const merchantLogin = await context.request('POST', '/api/auth/login', {
username: MERCHANT_USERNAME, password: MERCHANT_PASSWORD,
}, '')
const merchantToken = merchantLogin.http === 200 && codeOf(merchantLogin) === '200'
? merchantLogin.body?.data?.accessToken : null
context.addStep(MODULE, '取得客户侧隔离令牌用于 PDA 批次越权验证', merchantLogin,
Boolean(merchantToken), `username=${MERCHANT_USERNAME}`, OPTIONS)
const customerIsolatedBatch = merchantToken
? await request(context, 'POST', batchPath, batchPayload, {
Authorization: `Bearer ${merchantToken}`,
'Idempotency-Key': `${context.marker}-bridge-batch-customer`.slice(0, 128),
})
2026-08-11 11:41:41 +08:00
: merchantLogin
context.addStep(MODULE, '客户身份无权提交平台 PDA 原子批次', customerIsolatedBatch,
customerIsolatedBatch.http === 403 && codeOf(customerIsolatedBatch) === 'A0403'
&& Boolean(customerIsolatedBatch.requestId) && Boolean(customerIsolatedBatch.traceId),
`HTTP=${customerIsolatedBatch.http}, code=${codeOf(customerIsolatedBatch)}, requestId=${customerIsolatedBatch.requestId || 'missing'}, traceId=${customerIsolatedBatch.traceId || 'missing'}`, OPTIONS)
2026-08-11 20:10:24 +08:00
const customerIsolatedSnapshot = merchantToken
? await request(context, 'GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`,
undefined, { Authorization: `Bearer ${merchantToken}` })
: merchantLogin
context.addStep(MODULE, '客户身份无权读取平台 PDA 实时库存快照', customerIsolatedSnapshot,
customerIsolatedSnapshot.http === 403 && codeOf(customerIsolatedSnapshot) === 'A0403'
&& Boolean(customerIsolatedSnapshot.requestId) && Boolean(customerIsolatedSnapshot.traceId),
`HTTP=${customerIsolatedSnapshot.http}, code=${codeOf(customerIsolatedSnapshot)}, requestId=${customerIsolatedSnapshot.requestId || 'missing'}, traceId=${customerIsolatedSnapshot.traceId || 'missing'}`, OPTIONS)
2026-08-11 11:41:41 +08:00
const currentAssigneeId = Number(claimed?.assignment?.assigneeId)
const reassigned = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
assigneeId: currentAssigneeId + 1000000, assigneeName: `${context.marker}-other-picker`,
expectedAssignmentVersion: Number(claimed?.assignment?.version),
operationId: `${context.marker}-bridge-assign-other`.slice(0, 128),
})
const otherAssignment = context.apiSuccess(MODULE, '将 PDA 任务改派给另一作业员', reassigned, '', OPTIONS)
const operatorIsolatedBatch = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': `${context.marker}-bridge-batch-other-operator`.slice(0, 128) })
context.addStep(MODULE, '非任务作业员提交 PDA 原子批次返回冲突', operatorIsolatedBatch,
operatorIsolatedBatch.http === 200 && codeOf(operatorIsolatedBatch) === 'A0409',
`HTTP=${operatorIsolatedBatch.http}, code=${codeOf(operatorIsolatedBatch)}`, OPTIONS)
const restoredAssignmentResponse = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/assign`, {
assigneeId: currentAssigneeId, assigneeName: claimed?.assignment?.assigneeName || 'admin',
expectedAssignmentVersion: Number(otherAssignment?.assignment?.version),
operationId: `${context.marker}-bridge-assign-restore`.slice(0, 128),
})
const restoredAssignment = context.apiSuccess(MODULE, '恢复 PDA 任务到当前作业员', restoredAssignmentResponse, '', OPTIONS)
context.assert(MODULE, '作业员隔离验证后任务归属恢复', restoredAssignmentResponse,
id(restoredAssignment?.assignment?.assigneeId) === id(currentAssigneeId),
`assigneeId=${id(restoredAssignment?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
2026-08-11 20:10:24 +08:00
let inventoryMutationVerified = false
let inventoryMutationCleanup = null
let effectiveBatchPayload = batchPayload
const overflowResource = inventoryInbound?.resources?.find?.((resource) =>
resource?.sharedOverflow === true && id(resource?.occupancyId))
if (overflowResource) {
const mutationSuffix = context.marker.replace(/\D/g, '').slice(-10)
const targetLocationResponse = await context.request('POST', '/api/platform/v1/locations?tenantId=0', {
warehouseId, zoneCode: `SNAP-${mutationSuffix.slice(-6)}`, zoneName: '库存快照验收区',
locationCode: `SNAP-${mutationSuffix}`, locationName: '库存快照换绑库位',
locationType: 'PALLET', maxWeightKg: 1000, status: 'AVAILABLE',
remark: `${context.marker}-inventory-snapshot-mutation`,
})
const targetLocationId = id(context.apiSuccess(MODULE, '创建库存快照变化目标库位',
targetLocationResponse, '', OPTIONS))
if (targetLocationId) {
inventoryMutationCleanup = {
cleaned: false, targetLocationId,
reason: '目标库位在真实换绑后属于活动占用,公开删除接口会拒绝,不伪报已清理',
}
}
const beforeMutationTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
2026-08-11 20:10:24 +08:00
const beforeMutationTask = context.apiSuccess(MODULE, '库存变化前读取 PDA 任务基线', beforeMutationTaskResponse, '', OPTIONS)
const beforeMutationOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const beforeMutationOperations = context.apiSuccess(MODULE, '库存变化前读取 PDA 操作历史基线',
beforeMutationOperationsResponse, '', OPTIONS)
const rebindResponse = targetLocationId
? await request(context, 'POST',
`/api/platform/v1/locations/temporary-occupancies/${encodeURIComponent(id(overflowResource.occupancyId))}/rebind`, {
targetLocationId, idempotencyKey: `${context.marker}-snapshot-rebind`.slice(0, 128),
reason: `${context.marker}-inventory-changed`,
})
: targetLocationResponse
const rebound = context.apiSuccess(MODULE, '通过公开 A0 换绑接口制造真实库存变化', rebindResponse, '', OPTIONS)
context.assert(MODULE, '真实库存换绑保持同仓且位置发生变化', rebindResponse,
id(rebound?.occupancyId) === id(overflowResource.occupancyId)
&& id(rebound?.previousLocationId) === id(overflowResource.locationId)
&& id(rebound?.locationId) === targetLocationId,
`occupancyId=${id(rebound?.occupancyId) || 'missing'}, previousLocationId=${id(rebound?.previousLocationId) || 'missing'}, locationId=${id(rebound?.locationId) || 'missing'}`, OPTIONS)
const staleInventoryBatch = await request(context, 'POST', batchPath, batchPayload,
{ 'Idempotency-Key': `${context.marker}-bridge-batch-stale-inventory`.slice(0, 128) })
context.addStep(MODULE, '真实库存变化后旧 snapshot token 提交返回冲突', staleInventoryBatch,
staleInventoryBatch.http === 200 && codeOf(staleInventoryBatch) === 'A0409'
&& String(staleInventoryBatch.body?.message || '').includes('库存已变化'),
`HTTP=${staleInventoryBatch.http}, code=${codeOf(staleInventoryBatch)}, message=${staleInventoryBatch.body?.message || 'missing'}`, OPTIONS)
const afterStaleTaskResponse = await context.request('GET',
`${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)
2026-08-11 20:10:24 +08:00
const afterStaleTask = context.apiSuccess(MODULE, '旧库存快照冲突后回查 PDA 任务', afterStaleTaskResponse, '', OPTIONS)
const afterStaleOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const afterStaleOperations = context.apiSuccess(MODULE, '旧库存快照冲突后回查 PDA 操作历史',
afterStaleOperationsResponse, '', OPTIONS)
context.assert(MODULE, '旧 token 冲突对任务、明细、已拣数量和操作历史零写入', afterStaleTaskResponse,
Number(afterStaleTask?.version) === Number(beforeMutationTask?.version)
2026-08-12 13:24:17 +08:00
&& taskItemsSignature(afterStaleTask) === taskItemsSignature(beforeMutationTask)
2026-08-11 20:10:24 +08:00
&& operationsSignature(afterStaleOperations) === operationsSignature(beforeMutationOperations),
2026-08-12 13:24:17 +08:00
`taskVersion=${afterStaleTask?.version ?? 'missing'}, items=${taskItemsSignature(afterStaleTask)}, operations=${afterStaleOperations?.length ?? 'missing'}`, OPTIONS)
2026-08-11 20:10:24 +08:00
const refreshedInventoryResponse = await context.request('GET',
`${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`)
const refreshedInventory = context.apiSuccess(MODULE, '库存变化后刷新 PDA 实时库存快照',
refreshedInventoryResponse, '', OPTIONS)
const refreshedInbound = refreshedInventory?.inbounds?.find?.((item) => id(item.inboundId) === inboundId)
inventoryMutationVerified = refreshedInventory?.snapshotToken !== inventory.snapshotToken
&& Number(refreshedInventory?.taskVersion) === Number(inventory.taskVersion)
&& refreshedInbound?.resources?.some?.((resource) => id(resource.occupancyId) === id(overflowResource.occupancyId)
&& id(resource.locationId) === targetLocationId)
context.assert(MODULE, '刷新快照返回新 token、同任务版本和变更后库存', refreshedInventoryResponse,
inventoryMutationVerified,
`tokenChanged=${refreshedInventory?.snapshotToken !== inventory.snapshotToken}, taskVersion=${refreshedInventory?.taskVersion ?? 'missing'}, targetLocation=${targetLocationId || 'missing'}`, OPTIONS)
if (refreshedInventory?.snapshotToken) {
effectiveBatchPayload = { ...batchPayload, inventorySnapshotToken: refreshedInventory.snapshotToken }
}
} else {
context.addCoverageGap(MODULE, OPTIONS.scenario, 'PDA 真实库存变化后的旧快照冲突与刷新重试',
`inventory-snapshot 已真实返回 taskId=${canonicalTaskId}、warehouseId=${warehouseId} 并验证稳定 token/version=${inventorySnapshotVerified};本轮 ${inventoryInbound?.resources?.length ?? 0} 条占用均为普通库位,当前唯一平台代理库存变更入口只允许 A0 临时占用换绑,不能安全改动普通占用或调用内部 Warehouse 接口`,
'fixture')
}
const canonicalPick = await request(context, 'POST', batchPath, effectiveBatchPayload,
2026-08-11 11:41:41 +08:00
{ 'Idempotency-Key': batchKey })
const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS)
2026-08-13 20:00:30 +08:00
const awaitingReview = pickedCanonical?.taskStatus === 'LOADING'
&& pickedCanonical?.workflowStatus === 'AWAITING_REVIEW'
context.assert(MODULE, 'PDA 原子批次完成拣货并等待人工确认', canonicalPick,
awaitingReview && pickedCanonical?.items?.every?.((item) => item.itemStatus !== 'PENDING'),
`taskStatus=${pickedCanonical?.taskStatus || 'missing'}, workflow=${pickedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
2026-08-11 20:10:24 +08:00
const canonicalBatchReplay = await request(context, 'POST', batchPath, effectiveBatchPayload,
2026-08-11 11:41:41 +08:00
{ 'Idempotency-Key': batchKey })
const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200'
2026-08-11 11:41:41 +08:00
&& id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId
2026-08-13 20:00:30 +08:00
&& canonicalBatchReplay.body?.data?.taskStatus === 'LOADING'
&& canonicalBatchReplay.body?.data?.workflowStatus === 'AWAITING_REVIEW'
&& Number(canonicalBatchReplay.body?.data?.version) === Number(pickedCanonical?.version)
2026-08-13 20:00:30 +08:00
context.assert(MODULE, 'PDA 原子批次同参同键重放保持待人工确认', canonicalBatchReplay,
atomicBatchReplayVerified,
2026-08-11 11:41:41 +08:00
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
2026-08-13 20:00:30 +08:00
const completeResponse = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/complete`, {
expectedTaskVersion: Number(pickedCanonical?.version),
operationId: `${context.marker}-bridge-complete`.slice(0, 128), remark: context.marker,
})
const completedCanonical = context.apiSuccess(MODULE, '人工确认柜次 PDA 装柜完成', completeResponse, '', OPTIONS)
const directPdaCompletion = completedCanonical?.taskStatus === 'COMPLETED'
&& completedCanonical?.workflowStatus === 'COMPLETED'
context.assert(MODULE, '人工确认后 PDA 任务进入 COMPLETED', completeResponse,
directPdaCompletion && completedCanonical?.items?.every?.((item) => item.itemStatus === 'PICKED'),
`taskStatus=${completedCanonical?.taskStatus || 'missing'}, workflow=${completedCanonical?.workflowStatus || 'missing'}`, OPTIONS)
2026-08-12 13:24:17 +08:00
const completionOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`)
const completionOperations = context.apiSuccess(MODULE, '回查原子 Batch 完成审计', completionOperationsResponse, '', OPTIONS)
2026-08-13 20:00:30 +08:00
context.assert(MODULE, '人工完成后保留 CLAIM/BATCH/COMPLETE_LOADING 审计', completionOperationsResponse,
2026-08-12 13:24:17 +08:00
Array.isArray(completionOperations)
&& completionOperations.some((item) => item.actionType === 'CLAIM'
&& id(item.operatorId) && Boolean(item.createdAt))
&& completionOperations.some((item) => item.actionType === 'BATCH'
&& id(item.operatorId) && Boolean(item.createdAt))
2026-08-13 20:00:30 +08:00
&& completionOperations.some((item) => item.actionType === 'COMPLETE_LOADING'
&& id(item.operatorId) && Boolean(item.createdAt))
2026-08-12 13:24:17 +08:00
&& !completionOperations.some((item) => item.actionType === 'REVIEW'),
`actions=${Array.isArray(completionOperations) ? completionOperations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS)
const pickedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
const pickedPlan = context.apiSuccess(MODULE, '首笔真实拣货后回查柜次计划', pickedPlanResponse, '', OPTIONS)
2026-08-13 20:00:30 +08:00
context.assert(MODULE, '人工确认后柜次装柜计划完成', pickedPlanResponse,
directPdaCompletion && pickedPlan?.status === 'COMPLETED',
`status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS)
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2,
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) })
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
context.assert(MODULE, directPdaCompletion
? '装柜已开始时预装确认保持 LOADING 和 CONFIRMING'
: '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
directPdaCompletion
? confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMING'
&& Number(confirmed?.preloadPlan?.version) === 2
: confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
&& Number(confirmed?.preloadPlan?.version) === 3,
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
if (confirmed?.mainStatus !== 'LOADING') return
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS)
context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload,
id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId
&& preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker,
`planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS)
const loadingResponse = await context.request('GET', detailPath)
const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS)
const loadingVersion = Number(loading?.summary?.version)
context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse,
loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion),
`status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS)
if (!Number.isInteger(loadingVersion)) return
2026-08-13 20:00:30 +08:00
// COMPLETE_LOADING is the only positive completion action; REVIEW is a
// negative compatibility assertion after the task is already complete.
const canonicalReview = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/review`, {
2026-08-13 20:00:30 +08:00
expectedTaskVersion: Number(completedCanonical?.version),
operationId: `${context.marker}-bridge-review`.slice(0, 128), remark: context.marker,
})
context.addStep(MODULE, '柜次 PDA 已完成后拒绝重复复核', canonicalReview,
directPdaCompletion
? canonicalReview.http === 200 && codeOf(canonicalReview) === 'A0409'
: canonicalReview.http === 200 && codeOf(canonicalReview) === '200',
`HTTP=${canonicalReview.http}, code=${codeOf(canonicalReview)}`, OPTIONS)
2026-08-13 20:00:30 +08:00
context.addStep(MODULE, '柜次 PDA 作为现场作业唯一事实源', null, true,
'本场景不调用旧入库级 picking/start 写接口,避免客户端汇总数量覆盖柜次任务事实', OPTIONS)
const evidenceTypes = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL']
const evidenceItems = []
for (const evidenceType of evidenceTypes) {
const upload = await uploadEvidence(context, tenantId, inboundId, evidenceType)
const uploaded = context.apiSuccess(MODULE, `上传本轮 ${evidenceType} 真实证据文件`, upload, '', OPTIONS)
const fileId = id(uploaded?.fileId)
context.assert(MODULE, `${evidenceType} 文件归属类型和媒体元数据正确`, upload,
Boolean(fileId) && uploaded?.businessType === `LOADING_${evidenceType}`
&& uploaded?.contentType === 'image/png' && Number(uploaded?.sizeBytes) > 0,
`fileId=${fileId || 'missing'}, businessType=${uploaded?.businessType || 'missing'}`, OPTIONS)
if (!fileId) return
const evidence = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
const evidenceData = context.apiSuccess(MODULE, `绑定本轮 ${evidenceType} 装柜证据`, evidence, '', OPTIONS)
const evidenceItem = evidenceData?.evidences?.evidences?.find?.((item) => item.evidenceType === evidenceType)
context.assert(MODULE, `${evidenceType} 证据归属、marker 和版本正确`, evidence,
evidenceData?.mainStatus === 'LOADING' && id(evidenceItem?.fileId) === fileId
&& evidenceItem?.fileName === `${context.marker}-${evidenceType.toLowerCase()}.png`
&& evidenceItem?.remark === context.marker && Number(evidenceItem?.version) === 0
&& (evidenceType !== 'SEAL' || evidenceItem?.sealNo === `SEAL-${suffix}`),
`mainStatus=${evidenceData?.mainStatus || 'missing'}, evidenceId=${id(evidenceItem?.id) || 'missing'}`, OPTIONS)
if (!evidenceItem?.id) return
evidenceItems.push(evidenceItem)
2026-08-12 12:35:29 +08:00
if (evidenceType === 'PHOTO_STEP_1') {
const replay = await bindEvidence(context, base, tenantId, evidenceType, fileId, suffix)
const replayData = context.apiSuccess(MODULE, '装柜证据绑定同键同参幂等重放', replay, '', OPTIONS)
const replayItems = replayData?.evidences?.evidences?.filter?.((item) => item.evidenceType === evidenceType) || []
context.assert(MODULE, '证据绑定重放返回同一记录且不重复追加', replay,
replayItems.length === 1 && id(replayItems[0]?.id) === id(evidenceItem.id),
`evidenceId=${id(replayItems[0]?.id) || 'missing'}, count=${replayItems.length}`, OPTIONS)
const conflict = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: 0, evidenceType, fileId,
fileName: 'client-must-not-override.png', sealNo: null, remark: `${context.marker}-different`,
}, { 'Idempotency-Key': `${context.marker}-p3-evidence-${evidenceType}`.slice(0, 128) })
context.addStep(MODULE, '装柜证据绑定同键异参返回冲突', conflict,
conflict.http === 200 && codeOf(conflict) === 'A0409'
&& Boolean(conflict.requestId) && Boolean(conflict.traceId),
`HTTP=${conflict.http}, code=${codeOf(conflict)}, requestId=${conflict.requestId || 'missing'}, traceId=${conflict.traceId || 'missing'}`, OPTIONS)
}
const download = await context.requestBinary('GET', `/api/platform/v1/files/loading-evidence/${tenantId}/${inboundId}/${fileId}`)
context.assert(MODULE, `下载本轮 ${evidenceType} 证据文件`, download,
download.http === 200 && download.contentType.toLowerCase().includes('image/png') && download.bytes.length > 0,
`bytes=${download.bytes.length}, contentType=${download.contentType || 'missing'}`, OPTIONS)
if (evidenceType === 'PHOTO_STEP_4') {
const missingSeal = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: loadingVersion, remark: 'must require seal' },
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-seal`.slice(0, 128) })
context.addStep(MODULE, '缺铅封照片和锁码时拒绝完成装柜', missingSeal, failure(missingSeal),
`HTTP=${missingSeal.http}, code=${codeOf(missingSeal)}`, OPTIONS)
}
}
const firstPhoto = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_1')
const sealEvidence = evidenceItems.find((item) => item.evidenceType === 'SEAL')
2026-08-10 11:28:57 +08:00
const appendedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_1')
const appendedFile = context.apiSuccess(MODULE, '追加同类型第二张装柜证据文件', appendedUpload, '', OPTIONS)
const appendedFileId = id(appendedFile?.fileId)
const appended = appendedFileId
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_1', appendedFileId, suffix, 'PHOTO_STEP_1_APPEND')
: appendedUpload
const appendedData = context.apiSuccess(MODULE, '追加同类型第二张装柜证据', appended, '', OPTIONS)
const appendedItems = appendedData?.evidences?.evidences?.filter?.((item) => item.evidenceType === 'PHOTO_STEP_1') || []
context.assert(MODULE, '同类型装柜证据支持多图追加且不替换历史', appended,
Boolean(appendedFileId) && appendedItems.length >= 2
&& appendedItems.some((item) => id(item.fileId) === id(firstPhoto?.fileId))
&& appendedItems.some((item) => id(item.fileId) === appendedFileId),
`photoStep1Count=${appendedItems.length}, original=${id(firstPhoto?.fileId) || 'missing'}, appended=${appendedFileId || 'missing'}`, OPTIONS)
const wrongType = await bindEvidence(context, base, tenantId, 'PHOTO_STEP_2', id(firstPhoto?.fileId), suffix, 'wrong-type')
context.addStep(MODULE, '不同证据类型的真实 fileId 不可交叉绑定', wrongType, failure(wrongType),
`HTTP=${wrongType.http}, code=${codeOf(wrongType)}`, OPTIONS)
2026-08-08 21:14:39 +08:00
const forged = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: '999999999999999999',
2026-08-08 21:14:39 +08:00
fileName: `${context.marker}-forged.png`, sealNo: `FORGED-${suffix}`, remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-forged-evidence`.slice(0, 128) })
context.addStep(MODULE, '伪造装柜证据 fileId 归属校验拒绝', forged, failure(forged),
`HTTP=${forged.http}, code=${codeOf(forged)}`, OPTIONS)
2026-08-10 11:28:57 +08:00
const missingSealText = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, {
expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: id(sealEvidence?.fileId),
fileName: `${context.marker}-missing-seal-text.png`, sealNo: ' ', remark: context.marker,
}, { 'Idempotency-Key': `${context.marker}-p3-missing-seal-text`.slice(0, 128) })
context.addStep(MODULE, '带锁码照片缺少锁码文本时拒绝绑定', missingSealText, failure(missingSealText),
`HTTP=${missingSealText.http}, code=${codeOf(missingSealText)}`, OPTIONS)
const voidTarget = appendedItems.find((item) => id(item.fileId) === appendedFileId)
const voidResponse = voidTarget
? await request(context, 'POST', `${base}/loading/evidence/${encodeURIComponent(id(voidTarget.id))}/void?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: Number(voidTarget.version), reason: `${context.marker}-wrong-photo` },
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void`.slice(0, 128) })
: appended
const voidData = context.apiSuccess(MODULE, '作废错误装柜证据并保留审计', voidResponse, '', OPTIONS)
context.assert(MODULE, '错图作废后原记录保留且不再计入有效证据', voidResponse,
Boolean(voidTarget) && voidData?.evidences?.evidences?.some?.((item) => id(item.id) === id(voidTarget.id)
&& (item.status === 'VOID' || item.valid === false) && item.voidReason === `${context.marker}-wrong-photo`),
`evidenceId=${id(voidTarget?.id) || 'missing'}, status=${voidTarget?.status || 'missing'}`, OPTIONS)
2026-08-12 12:35:29 +08:00
const missingTypeTarget = evidenceItems.find((item) => item.evidenceType === 'PHOTO_STEP_4')
const missingTypePath = `${base}/loading/evidence/${encodeURIComponent(id(missingTypeTarget?.id))}/void?tenantId=${encodeURIComponent(tenantId)}`
const staleMissingTypeVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath,
{ expectedVersion: Number(missingTypeTarget.version) + 1, reason: `${context.marker}-stale-version` },
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-stale`.slice(0, 128) })
: voidResponse
context.addStep(MODULE, '装柜证据作废旧版本返回冲突', staleMissingTypeVoid,
Boolean(missingTypeTarget) && staleMissingTypeVoid.http === 200 && codeOf(staleMissingTypeVoid) === 'A0409',
`HTTP=${staleMissingTypeVoid.http}, code=${codeOf(staleMissingTypeVoid)}`, OPTIONS)
const missingTypeVoidKey = `${context.marker}-p3-evidence-void-missing-type`.slice(0, 128)
const missingTypeVoidBody = {
expectedVersion: Number(missingTypeTarget?.version), reason: `${context.marker}-temporary-missing-type`,
}
const missingTypeVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': missingTypeVoidKey })
: voidResponse
const missingTypeVoidData = context.apiSuccess(MODULE, '临时作废唯一 PHOTO_STEP_4 证据', missingTypeVoid, '', OPTIONS)
context.assert(MODULE, '临时作废后 PHOTO_STEP_4 不再计入有效证据', missingTypeVoid,
Boolean(missingTypeTarget) && missingTypeVoidData?.evidences?.evidences?.some?.((item) =>
id(item.id) === id(missingTypeTarget.id) && item.status === 'VOID'),
`evidenceId=${id(missingTypeTarget?.id) || 'missing'}`, OPTIONS)
const missingTypeVoidReplay = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': missingTypeVoidKey })
: voidResponse
context.assert(MODULE, '证据作废同键同参重放保持同一 VOID 结果', missingTypeVoidReplay,
missingTypeVoidReplay.http === 200 && codeOf(missingTypeVoidReplay) === '200'
&& missingTypeVoidReplay.body?.data?.evidences?.evidences?.some?.((item) =>
id(item.id) === id(missingTypeTarget?.id) && item.status === 'VOID'),
`HTTP=${missingTypeVoidReplay.http}, code=${codeOf(missingTypeVoidReplay)}`, OPTIONS)
const missingTypeVoidConflict = missingTypeTarget
? await request(context, 'POST', missingTypePath,
{ ...missingTypeVoidBody, reason: `${context.marker}-different-void-reason` },
{ 'Idempotency-Key': missingTypeVoidKey })
: voidResponse
context.addStep(MODULE, '证据作废同键异参返回冲突', missingTypeVoidConflict,
missingTypeVoidConflict.http === 200 && codeOf(missingTypeVoidConflict) === 'A0409',
`HTTP=${missingTypeVoidConflict.http}, code=${codeOf(missingTypeVoidConflict)}`, OPTIONS)
const duplicateVoid = missingTypeTarget
? await request(context, 'POST', missingTypePath, missingTypeVoidBody,
{ 'Idempotency-Key': `${context.marker}-p3-evidence-void-duplicate`.slice(0, 128) })
: voidResponse
context.addStep(MODULE, '已作废证据使用新请求重复作废返回冲突', duplicateVoid,
duplicateVoid.http === 200 && codeOf(duplicateVoid) === 'A0409',
`HTTP=${duplicateVoid.http}, code=${codeOf(duplicateVoid)}`, OPTIONS)
const missingTypeComplete = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
{ expectedVersion: loadingVersion, remark: `${context.marker}-missing-photo-step-4` },
{ 'Idempotency-Key': `${context.marker}-p3-complete-missing-photo-step-4`.slice(0, 128) })
context.addStep(MODULE, '作废唯一证据类型后 LOADED 门禁拒绝', missingTypeComplete,
missingTypeComplete.http === 200 && codeOf(missingTypeComplete) === 'A0409',
`HTTP=${missingTypeComplete.http}, code=${codeOf(missingTypeComplete)}`, OPTIONS)
const replacementUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_4')
const replacementFile = context.apiSuccess(MODULE, '补传缺失 PHOTO_STEP_4 真实文件', replacementUpload, '', OPTIONS)
const replacementFileId = id(replacementFile?.fileId)
const replacementBind = replacementFileId
? await bindEvidence(context, base, tenantId, 'PHOTO_STEP_4', replacementFileId, suffix, 'PHOTO_STEP_4_RESTORE')
: replacementUpload
const replacementData = context.apiSuccess(MODULE, '补传恢复 PHOTO_STEP_4 有效证据', replacementBind, '', OPTIONS)
context.assert(MODULE, '补传后五类有效证据恢复且旧 VOID 记录保留', replacementBind,
Boolean(replacementFileId)
&& replacementData?.evidences?.evidences?.some?.((item) => item.evidenceType === 'PHOTO_STEP_4'
&& id(item.fileId) === replacementFileId && item.status !== 'VOID')
&& replacementData?.evidences?.evidences?.some?.((item) => id(item.id) === id(missingTypeTarget?.id)
&& item.status === 'VOID'),
`replacementFileId=${replacementFileId || 'missing'}`, OPTIONS)
const customerEvidence = merchantToken
? await request(context, 'GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`,
undefined, { Authorization: `Bearer ${merchantToken}` })
: merchantLogin
context.addStep(MODULE, '客户身份无权访问平台装柜证据', customerEvidence,
customerEvidence.http === 403 && codeOf(customerEvidence) === 'A0403'
&& Boolean(customerEvidence.requestId) && Boolean(customerEvidence.traceId),
`HTTP=${customerEvidence.http}, code=${codeOf(customerEvidence)}, requestId=${customerEvidence.requestId || 'missing'}, traceId=${customerEvidence.traceId || 'missing'}`, OPTIONS)
const wrongTenantEvidence = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(Number(tenantId) + 1)}`)
context.addStep(MODULE, '错误客户作用域不可读取本轮装柜证据', wrongTenantEvidence,
wrongTenantEvidence.http === 200 && codeOf(wrongTenantEvidence) === 'A0404',
`HTTP=${wrongTenantEvidence.http}, code=${codeOf(wrongTenantEvidence)}`, OPTIONS)
const wrongInboundEvidence = await context.request('GET',
`/api/platform/v1/inbounds/999999999999999999/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
context.addStep(MODULE, '错误入库单不可读取本轮装柜证据', wrongInboundEvidence,
wrongInboundEvidence.http === 200 && codeOf(wrongInboundEvidence) === 'A0404',
`HTTP=${wrongInboundEvidence.http}, code=${codeOf(wrongInboundEvidence)}`, OPTIONS)
const postLoadedUpload = await uploadEvidence(context, tenantId, inboundId, 'PHOTO_STEP_2')
const postLoadedFile = context.apiSuccess(MODULE, '预置 LOADED 后新增证据边界文件', postLoadedUpload, '', OPTIONS)
const postLoadedFileId = id(postLoadedFile?.fileId)
2026-08-08 21:14:39 +08:00
const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`)
const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS)
context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList,
2026-08-10 11:28:57 +08:00
evidenceListData?.evidences?.length >= evidenceTypes.length
&& evidenceTypes.every((type) => evidenceListData.evidences.some((item) => item.evidenceType === type))
&& evidenceListData.evidences.some((item) => item.evidenceType === 'SEAL' && item.sealNo === `SEAL-${suffix}`),
2026-08-08 21:14:39 +08:00
`evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS)
2026-08-13 20:00:30 +08:00
const beforeHardGateResponse = await context.request('GET', detailPath)
const beforeHardGate = context.apiSuccess(MODULE, '读取 LOADED 硬门禁调用前即时版本', beforeHardGateResponse, '', OPTIONS)
const beforeHardGateVersion = Number(beforeHardGate?.summary?.version)
context.assert(MODULE, 'LOADED 硬门禁即时基线状态和版本有效', beforeHardGateResponse,
beforeHardGate?.summary?.processStatus === 'LOADING' && Number.isInteger(beforeHardGateVersion),
`status=${beforeHardGate?.summary?.processStatus || 'missing'}, version=${beforeHardGate?.summary?.version ?? 'missing'}`, OPTIONS)
if (!Number.isInteger(beforeHardGateVersion)) return
const blockedBeforeDownstream = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
2026-08-13 20:00:30 +08:00
{ expectedVersion: beforeHardGateVersion, remark: `${context.marker}-before-downstream` },
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-downstream`.slice(0, 128) })
context.addStep(MODULE, '订舱拖车报关未齐备时 LOADED 硬门禁拒绝', blockedBeforeDownstream,
failure(blockedBeforeDownstream),
`HTTP=${blockedBeforeDownstream.http}, code=${codeOf(blockedBeforeDownstream)}`, OPTIONS)
2026-08-12 10:59:39 +08:00
const blockedDetailResponse = await context.request('GET', detailPath)
const blockedDetail = context.apiSuccess(MODULE, '回查 LOADED 硬门禁拒绝后的入库状态', blockedDetailResponse, '', OPTIONS)
context.assert(MODULE, 'LOADED 硬门禁拒绝后状态和版本零写入', blockedDetailResponse,
blockedDetail?.summary?.processStatus === 'LOADING'
2026-08-13 20:00:30 +08:00
&& Number(blockedDetail?.summary?.version) === beforeHardGateVersion,
2026-08-12 10:59:39 +08:00
`status=${blockedDetail?.summary?.processStatus || 'missing'}, version=${blockedDetail?.summary?.version ?? 'missing'}`, OPTIONS)
2026-08-08 21:14:39 +08:00
context.createdResources.phase3Loading = {
tenantId, inboundId, preloadPlanId: planId, evidenceIds: evidenceItems.map((item) => id(item.id)),
2026-08-12 10:59:39 +08:00
voidedEvidenceId: id(voidTarget?.id), evidenceTypes, sealNo: `SEAL-${suffix}`,
2026-08-12 12:35:29 +08:00
missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId,
immutableEvidenceId: id(firstPhoto?.id), immutableEvidenceVersion: Number(firstPhoto?.version),
postLoadedFileId,
2026-08-14 10:09:46 +08:00
oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId,
canonicalPdaTaskId: canonicalTaskId,
pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion,
2026-08-13 20:00:30 +08:00
atomicBatchVerified: awaitingReview && directPdaCompletion && atomicBatchReplayVerified,
2026-08-11 20:10:24 +08:00
inventorySnapshotVerified: inventorySnapshotVerified && directPdaCompletion && atomicBatchReplayVerified,
inventoryMutationVerified, inventoryMutationCleanup,
marker: context.marker, inboundStatus: 'LOADING', loadingPlanStatus: 'COMPLETED', finalStatus: 'COMPLETED',
}
2026-08-08 21:14:39 +08:00
}