import { diffPushPreloadStability, projectPushPreloadStability } from './push-preload-stability.mjs' import { createHash } from 'node:crypto' const MODULE = 'phase3-loading' const OPTIONS = { scenario: 'phase3-split-preload-loading', deploymentBlocker: true } const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || 'merchant_admin' const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || 'ChangeMe123!' const MATSON_SUPPLIER_NAME = 'MATSON 开发联调船司' function id(value) { return value == null ? '' : String(value) } export function markerSuffix(marker) { const value = String(marker || '') const digits = value.replace(/\D/g, '').slice(-8).padStart(8, '0') const digest = createHash('sha256').update(value).digest('hex').slice(0, 8).toUpperCase() return `${digits}-${digest}` } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } function responseEvidence(response) { return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response)}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` } function failure(response) { return [400, 401, 403, 404, 409, 503].includes(response?.http) || (response?.http === 200 && codeOf(response) !== '200') } async function ensureMatsonSupplier(context, records) { const existing = records.find((value) => value?.id && value?.name === MATSON_SUPPLIER_NAME) if (existing?.status === 'ACTIVE') return existing if (existing?.id) { const activatedResponse = await context.request('PATCH', `/api/platform/v1/suppliers/${encodeURIComponent(id(existing.id))}/status?tenantId=0`, { status: 'ACTIVE' }) const activated = context.apiSuccess(MODULE, '启用稳定MATSON船司开发夹具', activatedResponse, `supplierId=${id(existing.id)}`, OPTIONS) return activated?.id ? activated : null } const createdResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', { name: MATSON_SUPPLIER_NAME, category: 'BOOKING', region: '开发环境', legalRepresentative: '开发联调', contactName: '开发联调', phone: '13800000000', email: 'matson-dev-fixture@example.com', address: '开发环境稳定船司夹具', businessLicenseNo: 'DEV-CARRIER-MATSON', licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: '三四期开发环境HTTP验收稳定船司夹具', }) const supplierId = id(context.apiSuccess(MODULE, '创建稳定MATSON船司开发夹具', createdResponse, `name=${MATSON_SUPPLIER_NAME}`, OPTIONS)) return supplierId ? { id: supplierId, name: MATSON_SUPPLIER_NAME, status: 'ACTIVE' } : null } 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)}` 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) 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) }) } function fixture(context) { return context.createdResources?.orchestrationFixtures?.phase3Loading || context.createdResources?.orchestrationFixtures?.inboundMeasurement || context.createdResources?.inboundMeasurement || context.createdResources?.orders } 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, }))) } 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), }))) } export async function run(context) { const source = fixture(context) const tenantId = id(source?.tenantId) const inboundId = id(source?.inboundId) if (!tenantId || !inboundId || source?.marker !== context.marker) { context.addStep(MODULE, '取得本轮 WAIT_SPLIT 隔离入库单', null, false, `未找到 orders/inbound-measurement 本轮隔离 fixture,未使用历史入库单;fixtureMarker=${source?.marker || 'missing'}, runMarker=${context.marker}`, 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, initialStatus === 'WAIT_SPLIT' && Number.isInteger(initialVersion), `status=${initialStatus || 'missing'}, version=${initial?.summary?.version ?? 'missing'}`, OPTIONS) if (initialStatus !== 'WAIT_SPLIT' || !Number.isInteger(initialVersion)) return const base = `/api/platform/v1/inbounds/${inboundId}` const suffix = markerSuffix(context.marker) const warehouseId = id(initial?.summary?.warehouseId) const containerNo = `P3B-${suffix}`.slice(0, 64) const smartLoadingBase = '/api/platform/v1/first-leg/smart-loading' const pdaBase = '/api/platform/v1/first-leg/pda-loading' const supplierResponse = await context.request('GET', '/api/platform/v1/suppliers?tenantId=0&category=BOOKING&page=1&size=200') const supplierPage = context.apiSuccess(MODULE, '读取订舱代理与船司供应商事实源', supplierResponse, '', OPTIONS) const supplierRecords = Array.isArray(supplierPage?.records) ? supplierPage.records : [] const supplier = supplierRecords.find((value) => value?.id && value?.name && value?.status === 'ACTIVE' && value.name !== MATSON_SUPPLIER_NAME) context.assert(MODULE, '存在可用于发布订舱的 ACTIVE 订舱代理', supplierResponse, Boolean(supplier?.id && supplier?.name), `supplierId=${id(supplier?.id) || 'missing'}`, OPTIONS) if (!supplier?.id) return const shippingCarrier = await ensureMatsonSupplier(context, supplierRecords) context.assert(MODULE, '订舱船司与订舱代理使用各自真实供应商事实', supplierResponse, Boolean(shippingCarrier?.id && shippingCarrier?.name === MATSON_SUPPLIER_NAME), `shippingCompanyId=${id(shippingCarrier?.id) || 'missing'}, bookingAgentId=${id(supplier.id)}`, OPTIONS) if (!shippingCarrier?.id) return context.createdResources.phase3CarrierSupplier = { supplierId: id(shippingCarrier.id), name: shippingCarrier.name, status: 'ACTIVE', stableDevFixture: true, } const bookingDraft = { customerId: tenantId, customerName: initial?.summary?.customerName, warehouseId, channelCode: initial?.summary?.channelCode, billOfLadingNo: `P3BL-${suffix}`.slice(0, 128), shippingCompanyId: id(shippingCarrier.id), shippingCompanyName: shippingCarrier.name, bookingAgentId: id(supplier.id), bookingAgentName: supplier.name, vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64), 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', 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: '40HC', containerKind: 'FCL', quantity: 1, capacityCbm: 76, maxWeightKg: 28000 }], } const simulationResponse = await request(context, 'POST', '/api/platform/v1/containers', { channelCode: initial?.summary?.channelCode, warehouseId, containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, }) 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 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 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) const assignedItem = assignedLoadingPlan?.items?.find?.((item) => id(item.inboundId) === inboundId) const orderRemarks = source || {} context.assert(MODULE, '装柜明细客户备注与最新内部备注来源精确且不冒用货物备注', assignmentResponse, assignedItem?.customerRemark === orderRemarks.customerRemark && assignedItem?.internalRemark === orderRemarks.internalRemark && assignedItem?.internalRemark !== orderRemarks.cargoRemark, `customerRemark=${assignedItem?.customerRemark || 'missing'}, internalRemark=${assignedItem?.internalRemark || 'missing'}, cargoRemark=${orderRemarks.cargoRemark || 'missing'}`, OPTIONS) if (!assignedItem?.id || !Number.isInteger(Number(assignedItem.version))) return const warehouseRemark = `${context.marker}-WAREHOUSE-REMARK` const warehouseRemarkPath = `${smartLoadingBase}/plans/${loadingPlanId}/items/${id(assignedItem.id)}/warehouse-remark` const warehouseRemarkBody = Object.freeze({ warehouseRemark, expectedPlanVersion: Number(assignedLoadingPlan.version), expectedItemVersion: Number(assignedItem.version), operationId: `${context.marker}-warehouse-remark`.slice(0, 128) }) const remarkResponse = await request(context, 'PATCH', warehouseRemarkPath, warehouseRemarkBody) const remarkedPlan = context.apiSuccess(MODULE, 'EDITING 装柜明细保存仓库备注', remarkResponse, '', OPTIONS) const remarkedItem = remarkedPlan?.items?.find?.((item) => id(item.id) === id(assignedItem.id)) context.assert(MODULE, '仓库备注保存后计划与明细版本递增且三备注并存', remarkResponse, remarkedItem?.warehouseRemark === warehouseRemark && Number(remarkedPlan?.version) > Number(assignedLoadingPlan.version) && Number(remarkedItem?.version) > Number(assignedItem.version) && remarkedItem?.customerRemark === orderRemarks.customerRemark && remarkedItem?.internalRemark === orderRemarks.internalRemark, `planVersion=${remarkedPlan?.version ?? 'missing'}, itemVersion=${remarkedItem?.version ?? 'missing'}`, OPTIONS) if (!remarkedItem?.id) return const remarkReplayResponse = await request(context, 'PATCH', warehouseRemarkPath, warehouseRemarkBody) const replayedRemarkPlan = context.apiSuccess(MODULE, '仓库备注相同 operationId 与原请求精确重放', remarkReplayResponse, '', OPTIONS) const replayedRemarkItem = replayedRemarkPlan?.items?.find?.((item) => id(item.id) === id(assignedItem.id)) context.assert(MODULE, '仓库备注精确重放不重复递增版本或历史事实', remarkReplayResponse, Number(replayedRemarkPlan?.version) === Number(remarkedPlan?.version) && Number(replayedRemarkItem?.version) === Number(remarkedItem?.version) && replayedRemarkItem?.warehouseRemark === warehouseRemark, `planVersion=${replayedRemarkPlan?.version ?? 'missing'}, itemVersion=${replayedRemarkItem?.version ?? 'missing'}`, OPTIONS) const staleRemarkResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${id(assignedItem.id)}/warehouse-remark`, { warehouseRemark: `${warehouseRemark}-STALE`, expectedPlanVersion: Number(assignedLoadingPlan.version), expectedItemVersion: Number(assignedItem.version), operationId: `${context.marker}-warehouse-remark-stale`.slice(0, 128), }) context.addStep(MODULE, '仓库备注旧版本 A0409 且零写入', staleRemarkResponse, staleRemarkResponse.http === 200 && codeOf(staleRemarkResponse) === 'A0409', `HTTP=${staleRemarkResponse.http}, code=${codeOf(staleRemarkResponse)}`, OPTIONS) const afterStaleRemarkResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const afterStaleRemarkPlan = context.apiSuccess(MODULE, '仓库备注旧版本冲突后回查计划', afterStaleRemarkResponse, '', OPTIONS) const afterStaleRemarkItem = afterStaleRemarkPlan?.items?.find?.((item) => id(item.id) === id(remarkedItem.id)) context.assert(MODULE, '仓库备注旧版本冲突后版本与备注事实零漂移', afterStaleRemarkResponse, Number(afterStaleRemarkPlan?.version) === Number(remarkedPlan?.version) && Number(afterStaleRemarkItem?.version) === Number(remarkedItem?.version) && afterStaleRemarkItem?.warehouseRemark === warehouseRemark, `planVersion=${afterStaleRemarkPlan?.version ?? 'missing'}, itemVersion=${afterStaleRemarkItem?.version ?? 'missing'}`, OPTIONS) const clearRemarkResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${id(remarkedItem.id)}/warehouse-remark`, { warehouseRemark: ' ', expectedPlanVersion: Number(remarkedPlan.version), expectedItemVersion: Number(remarkedItem.version), operationId: `${context.marker}-warehouse-remark-clear`.slice(0, 128), }) const clearedPlan = context.apiSuccess(MODULE, 'EDITING 装柜明细空白仓库备注归一化为 null', clearRemarkResponse, '', OPTIONS) const clearedItem = clearedPlan?.items?.find?.((item) => id(item.id) === id(remarkedItem.id)) context.assert(MODULE, '清空仓库备注不影响客户与内部备注来源', clearRemarkResponse, clearedItem?.warehouseRemark == null && clearedItem?.customerRemark === orderRemarks.customerRemark && clearedItem?.internalRemark === orderRemarks.internalRemark, `warehouseRemark=${clearedItem?.warehouseRemark ?? 'null'}`, OPTIONS) if (!clearedItem?.id) return const restoreRemarkResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${id(clearedItem.id)}/warehouse-remark`, { warehouseRemark, expectedPlanVersion: Number(clearedPlan.version), expectedItemVersion: Number(clearedItem.version), operationId: `${context.marker}-warehouse-remark-restore`.slice(0, 128), }) const persistedRemarkPlan = context.apiSuccess(MODULE, '恢复真实仓库备注供装柜完成事件下发', restoreRemarkResponse, '', OPTIONS) if (!Number.isInteger(Number(persistedRemarkPlan?.version))) return const pendingPreloadResponse = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`) const pendingPreload = context.apiSuccess(MODULE, '智能排货自动建立 PENDING 预装聚合', pendingPreloadResponse, '', OPTIONS) const planId = id(pendingPreload?.id) context.assert(MODULE, '推送前入库单保持 WAIT_SPLIT 且预装聚合为 PENDING', pendingPreloadResponse, Boolean(planId) && pendingPreload?.status === 'PENDING' && Number.isInteger(Number(pendingPreload?.version)), `preloadPlanId=${planId || 'missing'}, status=${pendingPreload?.status || 'missing'}`, OPTIONS) if (!planId) return const pushRequest = Object.freeze({ path: `${smartLoadingBase}/plans/${loadingPlanId}/push-preload`, body: Object.freeze({ expectedVersion: Number(persistedRemarkPlan.version) }), headers: Object.freeze({ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) }), }) const pushResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers) const pushedLoadingPlan = context.apiSuccess(MODULE, '无真实柜号推送预装成功并冻结柜次明细', pushResponse, '', OPTIONS) context.assert(MODULE, '无号推送保持 EDITING、身份稳定且版本递增', pushResponse, id(pushedLoadingPlan?.id) === loadingPlanId && pushedLoadingPlan?.status === 'EDITING' && pushedLoadingPlan?.containerNo == null && Number(pushedLoadingPlan?.version) > Number(assignedLoadingPlan.version), `status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS) if (pushedLoadingPlan?.status !== 'EDITING') return const pushedDetailResponse = await context.request('GET', `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`) const pushedDetail = context.apiSuccess(MODULE, '无号推送后预装清单正式可见', pushedDetailResponse, '', OPTIONS) const pushedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`) const pushedLinks = context.apiSuccess(MODULE, '无号推送自动建立 bridge 关联', pushedLinksResponse, '', OPTIONS) const link = Array.isArray(pushedLinks) ? pushedLinks.find((item) => id(item.inboundId) === inboundId) : null context.assert(MODULE, 'push 自动建立唯一预装关联且分配身份稳定', pushedLinksResponse, Boolean(link?.id) && id(link?.loadingPlanId) === loadingPlanId && id(link?.preloadPlanId) === planId && Number(link?.allocatedPackageCount) === 1, `linkId=${id(link?.id) || 'missing'}, preloadPlanId=${id(link?.preloadPlanId) || 'missing'}`, OPTIONS) if (!link?.id) return const postPushPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const postPushPlan = context.apiSuccess(MODULE, '首次 push 后读取事务完成态柜次计划', postPushPlanResponse, '', OPTIONS) context.assert(MODULE, '事务完成态计划版本与 bridge 一致且保持 EDITING', postPushPlanResponse, postPushPlan?.status === 'EDITING' && Number(postPushPlan?.version) === Number(link?.loadingPlanVersion) && Number(postPushPlan?.version) > Number(assignedLoadingPlan.version), `pushResponseVersion=${pushedLoadingPlan?.version ?? 'missing'}, persistedVersion=${postPushPlan?.version ?? 'missing'}, bridgeVersion=${link?.loadingPlanVersion ?? 'missing'}`, OPTIONS) if (postPushPlan?.status !== 'EDITING' || !Number.isInteger(Number(postPushPlan?.version))) return const missingContainerTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const missingContainerTaskAbsent = missingContainerTaskResponse.http === 200 && codeOf(missingContainerTaskResponse) === 'A0404' context.addStep(MODULE, '无号 push 成功后 PDA 查询明确 A0404', missingContainerTaskResponse, missingContainerTaskAbsent, `HTTP=${missingContainerTaskResponse.http}, code=${codeOf(missingContainerTaskResponse)}, requestId=${missingContainerTaskResponse.requestId || 'missing'}, traceId=${missingContainerTaskResponse.traceId || 'missing'}`, OPTIONS) if (!missingContainerTaskAbsent) return const replayBaseline = projectPushPreloadStability({ plan: postPushPlan, detail: pushedDetail, links: pushedLinks, task: { http: missingContainerTaskResponse.http, code: codeOf(missingContainerTaskResponse) }, }) const pushReplayResponse = await request(context, 'POST', pushRequest.path, pushRequest.body, pushRequest.headers) context.apiSuccess(MODULE, 'push exact replay 返回首次结果', pushReplayResponse, '', OPTIONS) const replayPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const replayPlan = context.apiSuccess(MODULE, '重放后回查柜次计划', replayPlanResponse, '', OPTIONS) const replayDetailResponse = await context.request('GET', `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`) const replayDetail = context.apiSuccess(MODULE, '重放后回查预装历史', replayDetailResponse, '', OPTIONS) const replayLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`) const replayLinks = context.apiSuccess(MODULE, '重放后回查 bridge', replayLinksResponse, '', OPTIONS) const replayTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const replaySnapshot = projectPushPreloadStability({ plan: replayPlan, detail: replayDetail, links: replayLinks, task: { http: replayTaskResponse.http, code: codeOf(replayTaskResponse) }, }) const replayDiff = diffPushPreloadStability(replayBaseline, replaySnapshot) context.assert(MODULE, 'push exact replay 对计划版本历史关联和任务零漂移', pushReplayResponse, replayDiff.length === 0 && codeOf(replayTaskResponse) === 'A0404', `changed=${replayDiff.join('|') || 'none'}; replayTask=${responseEvidence(replayTaskResponse)}`, OPTIONS) const sameKeyDifferentBody = await request(context, 'POST', pushRequest.path, { expectedVersion: Number(postPushPlan.version) }, pushRequest.headers) context.addStep(MODULE, 'push 同 key 异参返回冲突', sameKeyDifferentBody, failure(sameKeyDifferentBody), `HTTP=${sameKeyDifferentBody.http}, code=${codeOf(sameKeyDifferentBody)}`, OPTIONS) const newKeyDuplicate = await request(context, 'POST', pushRequest.path, pushRequest.body, { 'Idempotency-Key': `${context.marker}-bridge-push-duplicate`.slice(0, 128) }) context.addStep(MODULE, '已推送计划使用新 key 重复 push 返回冲突', newKeyDuplicate, failure(newKeyDuplicate), `HTTP=${newKeyDuplicate.http}, code=${codeOf(newKeyDuplicate)}`, OPTIONS) const rejectedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const rejectedPlan = context.apiSuccess(MODULE, '非法 push 后回查柜次计划', rejectedPlanResponse, '', OPTIONS) const rejectedDetailResponse = await context.request('GET', `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`) const rejectedDetail = context.apiSuccess(MODULE, '非法 push 后回查预装历史', rejectedDetailResponse, '', OPTIONS) const rejectedLinksResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}/preloads`) const rejectedLinks = context.apiSuccess(MODULE, '非法 push 后回查 bridge', rejectedLinksResponse, '', OPTIONS) const rejectedTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const rejectedSnapshot = projectPushPreloadStability({ plan: rejectedPlan, detail: rejectedDetail, links: rejectedLinks, task: { http: rejectedTaskResponse.http, code: codeOf(rejectedTaskResponse) }, }) const rejectedDiff = diffPushPreloadStability(replayBaseline, rejectedSnapshot) context.assert(MODULE, '两类非法 push 后计划版本历史关联和任务仍零漂移', rejectedTaskResponse, failure(sameKeyDifferentBody) && failure(newKeyDuplicate) && rejectedDiff.length === 0 && codeOf(rejectedTaskResponse) === 'A0404', `changed=${rejectedDiff.join('|') || 'none'}; sameKey=${responseEvidence(sameKeyDifferentBody)}; newKey=${responseEvidence(newKeyDuplicate)}; task=${responseEvidence(rejectedTaskResponse)}`, OPTIONS) if (rejectedDiff.length !== 0 || codeOf(rejectedTaskResponse) !== 'A0404') return const unlinkedTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) context.addStep(MODULE, '无号且未关联订舱仍无 PDA 任务', unlinkedTaskResponse, unlinkedTaskResponse.http === 200 && codeOf(unlinkedTaskResponse) === 'A0404', `HTTP=${unlinkedTaskResponse.http}, code=${codeOf(unlinkedTaskResponse)}`, OPTIONS) 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 bookingDraftResponse = context.apiSuccess(MODULE, '创建三期 DRAFT 订舱(不生成真实柜)', bookingCreateResponse, '', OPTIONS) const bookingId = id(bookingDraftResponse?.oceanBooking?.id) const draftVersion = Number(bookingDraftResponse?.oceanBooking?.version) const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { expectedVersion: draftVersion, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) }) const bookingConfirmed = context.apiSuccess(MODULE, '三期订舱 RELEASED 冻结运输快照', releaseResponse, '', OPTIONS) const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'] const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, bookingConfirmed?.oceanBooking?.[field]])) const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '') const frozenTransportMatches = frozenTransportValid && transportFields.every((field) => String(frozenTransport[field]) === String(bookingDraft[field])) const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HC') context.assert(MODULE, 'RELEASED 订舱回显完整冻结运输快照且无真实柜号', releaseResponse, Boolean(bookingId) && bookingConfirmed?.oceanBooking?.status === 'RELEASED' && frozenTransportMatches && Boolean(requirement?.id) && (!bookingConfirmed?.containers || bookingConfirmed.containers.length === 0), `bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS) if (!bookingId || !requirement?.id || !frozenTransportMatches) return const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}`) const latestContainer = context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS) const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, { oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo, expectedVersion: Number(latestContainer?.version), }) const linked = context.apiSuccess(MODULE, '模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号', linkResponse, '', OPTIONS) context.assert(MODULE, '订舱关联保持柜次计划与真实柜号身份', linkResponse, id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo && linked?.containerNo === containerNo && id(linked?.oceanBookingId) === bookingId, `containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS) if (id(linked?.id) !== containerBookingId) return context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份和运输快照', releaseResponse, bookingConfirmed?.oceanBooking?.status === 'RELEASED' && id(linked?.oceanBookingId) === bookingId && linked?.containerNo === containerNo, `status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS) const unconfirmedTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) context.addStep(MODULE, '订舱 RELEASED 但预装未 CONFIRMED 仍无 PDA 任务', unconfirmedTaskResponse, unconfirmedTaskResponse.http === 200 && codeOf(unconfirmedTaskResponse) === 'A0404', `HTTP=${unconfirmedTaskResponse.http}, code=${codeOf(unconfirmedTaskResponse)}`, OPTIONS) const confirmableResponse = await context.request('GET', `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}?warehouseId=${encodeURIComponent(warehouseId)}`) const confirmable = context.apiSuccess(MODULE, '计划级确认前读取即时版本与就绪门禁', confirmableResponse, '', OPTIONS) const confirmableVersion = Number(confirmable?.summary?.version) context.assert(MODULE, '真实柜合并放舱后整份计划允许确认且 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) return const confirmPath = `/api/platform/v1/first-leg/loading-plans/${loadingPlanId}/confirm` const confirmBody = Object.freeze({ expectedVersion: confirmableVersion }) const confirmHeaders = Object.freeze({ 'Idempotency-Key': `${context.marker}-p3-plan-confirm`.slice(0, 128) }) const confirm = await request(context, 'POST', confirmPath, confirmBody, confirmHeaders) const confirmed = context.apiSuccess(MODULE, '计划级原子确认整份预装并触发唯一 PDA 任务', confirm, '', OPTIONS) const confirmedTaskId = id(confirmed?.readiness?.pdaTaskId) context.assert(MODULE, '计划确认返回 CONFIRMED、readyForPda 与唯一 taskId', confirm, id(confirmed?.planId) === loadingPlanId && id(confirmed?.containerBookingId) === containerBookingId && 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') return const confirmReplay = await request(context, 'POST', confirmPath, confirmBody, confirmHeaders) context.assert(MODULE, '计划级确认同键同参精确重放同一 taskId 与版本', confirmReplay, confirmReplay.http === 200 && codeOf(confirmReplay) === '200' && id(confirmReplay.body?.data?.readiness?.pdaTaskId) === confirmedTaskId && Number(confirmReplay.body?.data?.version) === Number(confirmed.version), `HTTP=${confirmReplay.http}, code=${codeOf(confirmReplay)}, requestId=${confirmReplay.requestId || 'missing'}, traceId=${confirmReplay.traceId || 'missing'}`, OPTIONS) const confirmSameKeyDifferentBody = await request(context, 'POST', confirmPath, { expectedVersion: Number(confirmed.version) }, confirmHeaders) context.addStep(MODULE, '计划级确认同 key 异参返回冲突', confirmSameKeyDifferentBody, confirmSameKeyDifferentBody.http === 200 && codeOf(confirmSameKeyDifferentBody) === 'A0409', `HTTP=${confirmSameKeyDifferentBody.http}, code=${codeOf(confirmSameKeyDifferentBody)}, requestId=${confirmSameKeyDifferentBody.requestId || 'missing'}, traceId=${confirmSameKeyDifferentBody.traceId || 'missing'}`, OPTIONS) const confirmNewKeyDuplicate = await request(context, 'POST', confirmPath, confirmBody, { 'Idempotency-Key': `${context.marker}-p3-plan-confirm-duplicate`.slice(0, 128) }) context.addStep(MODULE, '已确认计划使用新 key 重复确认返回冲突', confirmNewKeyDuplicate, confirmNewKeyDuplicate.http === 200 && codeOf(confirmNewKeyDuplicate) === 'A0409', `HTTP=${confirmNewKeyDuplicate.http}, code=${codeOf(confirmNewKeyDuplicate)}, requestId=${confirmNewKeyDuplicate.requestId || 'missing'}, traceId=${confirmNewKeyDuplicate.traceId || 'missing'}`, OPTIONS) const confirmedInboundResponse = await context.request('GET', detailPath) const confirmedInbound = context.apiSuccess(MODULE, '计划确认后 claim 前回查入库状态', confirmedInboundResponse, '', OPTIONS) const confirmedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const confirmedPlan = context.apiSuccess(MODULE, '计划确认后 claim 前回查柜次计划', confirmedPlanResponse, '', OPTIONS) context.assert(MODULE, '计划确认后 task 待领取且 claim 前仍 WAIT_SPLIT/EDITING', confirmedPlanResponse, confirmedInbound?.summary?.processStatus === 'WAIT_SPLIT' && confirmedPlan?.status === 'EDITING' && Number(confirmedPlan?.version) === Number(confirmed.version), `inbound=${confirmedInbound?.summary?.processStatus || 'missing'}, plan=${confirmedPlan?.status || 'missing'}`, OPTIONS) if (confirmedInbound?.summary?.processStatus !== 'WAIT_SPLIT' || confirmedPlan?.status !== 'EDITING') return const canonicalTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const canonicalTask = context.apiSuccess(MODULE, '读取柜次级 PDA 唯一事实任务', canonicalTaskResponse, '', OPTIONS) const canonicalTaskId = id(canonicalTask?.id) const taskDetailPath = `${pdaBase}/tasks/${canonicalTaskId}?warehouseId=${encodeURIComponent(warehouseId)}` const canonicalItem = canonicalTask?.items?.find?.((item) => id(item.inboundId) === inboundId) context.assert(MODULE, '柜次 PDA 任务与桥接入库明细一致', canonicalTaskResponse, Boolean(canonicalTaskId) && canonicalTaskId === confirmedTaskId && id(canonicalTask?.loadingPlanId) === loadingPlanId && id(canonicalTask?.containerBookingId) === containerBookingId && canonicalTask?.workflowStatus === 'UNASSIGNED' && Boolean(canonicalItem?.id), `taskId=${canonicalTaskId || 'missing'}, itemId=${id(canonicalItem?.id) || 'missing'}`, OPTIONS) if (!canonicalTaskId || !canonicalItem?.id) return const beforeClaimPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const beforeClaimPlan = context.apiSuccess(MODULE, '领取前读取柜次计划即时版本', beforeClaimPlanResponse, '', OPTIONS) const beforeClaimPlanVersion = Number(beforeClaimPlan?.version) if (!Number.isInteger(beforeClaimPlanVersion)) 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) > beforeClaimPlanVersion, `status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS) const inventoryResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`) const inventory = context.apiSuccess(MODULE, '读取 PDA 提交前实时库存快照', inventoryResponse, '', OPTIONS) const inventoryInbound = inventory?.inbounds?.find?.((item) => id(item.inboundId) === inboundId) const inventoryIdentityVerified = id(inventory?.taskId) === canonicalTaskId && id(inventory?.loadingPlanId) === loadingPlanId && id(inventory?.containerBookingId) === containerBookingId && Number(inventory?.taskVersion) === Number(claimed?.version) && id(inventory?.cabinetNo) && inventory?.currentContainerNo === containerNo && id(inventory?.warehouseId) === warehouseId && 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) if (!inventory?.snapshotToken) return 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) const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch` const batchKey = `${context.marker}-bridge-batch`.slice(0, 128) const batchPayload = { warehouseId, loadingPlanId, containerBookingId, expectedVersion: Number(claimed?.version), inventorySnapshotToken: inventory.snapshotToken, items: [{ itemId: id(canonicalItem.id), action: 'PICK', pickedPackageCount: 1, releaseMode: 'MANUAL', locationIds: [], version: Number(canonicalItem.version), }], } 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) const beforeInvalidTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) 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) 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)}`) const unchangedAfterInvalid = context.apiSuccess(MODULE, '回查失败批次后的 PDA 任务快照', unchangedAfterInvalidResponse, '', OPTIONS) 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) && taskItemsSignature(unchangedAfterInvalid) === taskItemsSignature(beforeInvalidTask) && operationsSignature(afterInvalidOperations) === operationsSignature(beforeInvalidOperations), `taskVersion=${unchangedAfterInvalid?.version ?? 'missing'}, items=${taskItemsSignature(unchangedAfterInvalid)}, operations=${afterInvalidOperations?.length ?? 'missing'}`, OPTIONS) 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), }) : 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) 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) 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) 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 = markerSuffix(context.marker) 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) { context.addStep(MODULE, '库存变化fixture创建失败后停止旧snapshot提交', targetLocationResponse, false, responseEvidence(targetLocationResponse), OPTIONS) return } if (targetLocationId) { inventoryMutationCleanup = { cleaned: false, targetLocationId, reason: '目标库位在真实换绑后属于活动占用,公开删除接口会拒绝,不伪报已清理', } } const beforeMutationTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) 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) const reboundValid = id(rebound?.occupancyId) === id(overflowResource.occupancyId) && id(rebound?.previousLocationId) === id(overflowResource.locationId) && id(rebound?.locationId) === targetLocationId context.assert(MODULE, '真实库存换绑保持同仓且位置发生变化', rebindResponse, reboundValid, `occupancyId=${id(rebound?.occupancyId) || 'missing'}, previousLocationId=${id(rebound?.previousLocationId) || 'missing'}, locationId=${id(rebound?.locationId) || 'missing'}`, OPTIONS) if (!reboundValid) { context.addStep(MODULE, '库存换绑未成功后停止旧snapshot提交', rebindResponse, false, responseEvidence(rebindResponse), OPTIONS) return } 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)}`) 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) && taskItemsSignature(afterStaleTask) === taskItemsSignature(beforeMutationTask) && operationsSignature(afterStaleOperations) === operationsSignature(beforeMutationOperations), `taskVersion=${afterStaleTask?.version ?? 'missing'}, items=${taskItemsSignature(afterStaleTask)}, operations=${afterStaleOperations?.length ?? 'missing'}`, OPTIONS) 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.addStep(MODULE, '自动制造库存变化并验证旧 snapshot token A0409', null, false, `本轮 ${inventoryInbound?.resources?.length ?? 0} 条占用没有可安全换绑的 A0 临时占用;不得以 coverageGap 作为通过`, OPTIONS) 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, { 'Idempotency-Key': batchKey }) const pickedCanonical = context.apiSuccess(MODULE, '原子提交柜次 PDA 整批拣货', canonicalPick, '', OPTIONS) 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) const canonicalBatchReplay = await request(context, 'POST', batchPath, effectiveBatchPayload, { 'Idempotency-Key': batchKey }) const atomicBatchReplayVerified = canonicalBatchReplay.http === 200 && codeOf(canonicalBatchReplay) === '200' && id(canonicalBatchReplay.body?.data?.id) === canonicalTaskId && canonicalBatchReplay.body?.data?.taskStatus === 'LOADING' && canonicalBatchReplay.body?.data?.workflowStatus === 'AWAITING_REVIEW' && Number(canonicalBatchReplay.body?.data?.version) === Number(pickedCanonical?.version) context.assert(MODULE, 'PDA 原子批次同参同键重放保持待人工确认', canonicalBatchReplay, atomicBatchReplayVerified, `HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS) const preFinalOperationsResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/operations`) const preFinalOperations = context.apiSuccess(MODULE, '回查下游门禁前原子 Batch 审计', preFinalOperationsResponse, '', OPTIONS) context.assert(MODULE, '最终确认前仅保留 CLAIM/BATCH 且无 COMPLETE_LOADING', preFinalOperationsResponse, Array.isArray(preFinalOperations) && preFinalOperations.some((item) => item.actionType === 'CLAIM' && id(item.operatorId) && Boolean(item.createdAt)) && preFinalOperations.some((item) => item.actionType === 'BATCH' && id(item.operatorId) && Boolean(item.createdAt)) && !preFinalOperations.some((item) => item.actionType === 'COMPLETE_LOADING') && !preFinalOperations.some((item) => item.actionType === 'REVIEW'), `actions=${Array.isArray(preFinalOperations) ? preFinalOperations.map((item) => item.actionType).join(',') : 'missing'}`, OPTIONS) const pickedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const pickedPlan = context.apiSuccess(MODULE, '首笔真实拣货后回查柜次计划', pickedPlanResponse, '', OPTIONS) context.assert(MODULE, '下游门禁前柜次计划保持 LOADING 且等待最终完成', pickedPlanResponse, pickedPlan?.status === 'LOADING' && pickedPlan?.finalCompletionStatus === 'IN_PROGRESS' && id(pickedPlan?.pdaTaskId) === canonicalTaskId && pickedPlan?.pdaTaskStatus === 'LOADING', `status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS) const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`) const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS) context.assert(MODULE, 'PDA 最终完成前预装清单仍为 CONFIRMED 且归属稳定', preload, id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId && preloadData?.status === 'CONFIRMED', `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 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) 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', `${pdaBase}/tasks/${canonicalTaskId}/complete`, { expectedTaskVersion: Number(pickedCanonical?.version), operationId: `${context.marker}-complete-before-seal`.slice(0, 128), 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') 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) const forged = await request(context, 'POST', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`, { expectedVersion: Number(sealEvidence?.version), evidenceType: 'SEAL', fileId: '999999999999999999', 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) 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) 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', `${pdaBase}/tasks/${canonicalTaskId}/complete`, { expectedTaskVersion: Number(pickedCanonical?.version), operationId: `${context.marker}-complete-missing-photo-step-4`.slice(0, 128), 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) const evidenceList = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`) const evidenceListData = context.apiSuccess(MODULE, '回查五类装柜证据列表', evidenceList, '', OPTIONS) context.assert(MODULE, '五类装柜证据完整且铅封号持久化', evidenceList, 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}`), `evidences=${evidenceListData?.evidences?.length ?? 'n/a'}`, OPTIONS) const incrementalFixture = context.createdResources?.orchestrationFixtures?.phase3LoadingIncremental const incrementalInboundId = id(incrementalFixture?.inboundId) let dynamicIncrementVerified = false if (!incrementalInboundId || incrementalFixture?.marker !== context.marker || incrementalInboundId === inboundId) { context.addStep(MODULE, '取得本轮独立动态增量入库单', null, false, `inboundId=${incrementalInboundId || 'missing'}, primaryInboundId=${inboundId}, marker=${incrementalFixture?.marker || 'missing'}`, OPTIONS) } else { const dynamicCandidatesResponse = await context.request('GET', `${smartLoadingBase}/candidates?bookingId=${encodeURIComponent(containerBookingId)}&warehouseId=${encodeURIComponent(warehouseId)}`) const dynamicCandidates = context.apiSuccess(MODULE, '查询同柜动态增量候选', dynamicCandidatesResponse, '', OPTIONS) const dynamicCandidate = Array.isArray(dynamicCandidates) ? dynamicCandidates.find((item) => id(item?.inboundId) === incrementalInboundId && Number(item?.availablePackageCount) > 0) : null context.assert(MODULE, '动态增量候选保持客户、仓库和渠道隔离', dynamicCandidatesResponse, Boolean(dynamicCandidate?.inboundDetailId) && id(dynamicCandidate?.accountId) === tenantId && id(dynamicCandidate?.warehouseId) === warehouseId && dynamicCandidate?.channelCode === initial?.summary?.channelCode, `inboundId=${incrementalInboundId}, detailId=${id(dynamicCandidate?.inboundDetailId) || 'missing'}, available=${dynamicCandidate?.availablePackageCount ?? 'missing'}`, OPTIONS) if (dynamicCandidate?.inboundDetailId) { const planBeforeIncrementResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const planBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前计划', planBeforeIncrementResponse, '', OPTIONS) const taskBeforeIncrementResponse = await context.request('GET', taskDetailPath) const taskBeforeIncrement = context.apiSuccess(MODULE, '读取 LOADING 动态增量前任务', taskBeforeIncrementResponse, '', OPTIONS) const evidenceBeforeIncrement = JSON.stringify(evidenceListData?.evidences || []) const incrementResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, { inboundId: incrementalInboundId, inboundDetailId: id(dynamicCandidate.inboundDetailId), packageCount: 1, expectedVersion: Number(planBeforeIncrement?.version), operationId: `${context.marker}-dynamic-assign`.slice(0, 128), }) const incrementedPlan = context.apiSuccess(MODULE, 'LOADING 阶段向同一 PDA task 增加入库单', incrementResponse, '', OPTIONS) const incrementalPlanItem = incrementedPlan?.items?.find?.((item) => id(item?.inboundId) === incrementalInboundId) const preservedRemarkItem = incrementedPlan?.items?.find?.((item) => id(item?.id) === id(assignedItem.id)) const incrementedTaskResponse = await context.request('GET', taskDetailPath) const incrementedTask = context.apiSuccess(MODULE, '增量后回查稳定 PDA 四元身份', incrementedTaskResponse, '', OPTIONS) const incrementalTaskItem = incrementedTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(incrementalPlanItem?.id)) const evidenceAfterIncrementResponse = await context.request('GET', `${base}/loading/evidence?tenantId=${encodeURIComponent(tenantId)}`) const evidenceAfterIncrement = context.apiSuccess(MODULE, '增量后回查既有五类证据', evidenceAfterIncrementResponse, '', OPTIONS) context.assert(MODULE, '增量保持稳定四元身份、LOADING 不回退并撤回最终完成门禁', incrementResponse, id(incrementedPlan?.id) === loadingPlanId && incrementedPlan?.status === 'LOADING' && incrementedPlan?.finalCompletionStatus === 'IN_PROGRESS' && id(incrementedPlan?.pdaTaskId) === canonicalTaskId && incrementedPlan?.pdaTaskStatus === 'LOADING' && Number(incrementedPlan?.version) > Number(planBeforeIncrement?.version) && Number(incrementedPlan?.pdaTaskVersion) > Number(taskBeforeIncrement?.version) && Number(incrementalPlanItem?.version) === 0 && Number(incrementalPlanItem?.pickedPackageCount) === 0 && Number(incrementalPlanItem?.minPackageCount) === 1 && incrementalPlanItem?.warehouseRemark == null && preservedRemarkItem?.warehouseRemark === warehouseRemark && incrementalPlanItem?.canAdjust === true && incrementalPlanItem?.canRemove === true && id(incrementedTask?.loadingPlanId) === loadingPlanId && id(incrementedTask?.containerBookingId) === containerBookingId && incrementedTask?.workflowStatus === 'IN_PROGRESS' && JSON.stringify(evidenceAfterIncrement?.evidences || []) === evidenceBeforeIncrement, `planVersion=${incrementedPlan?.version ?? 'missing'}, taskVersion=${incrementedTask?.version ?? 'missing'}, workflow=${incrementedTask?.workflowStatus || 'missing'}`, OPTIONS) if (incrementalTaskItem?.id) { const staleIncrementBatch = await request(context, 'POST', batchPath, { warehouseId, loadingPlanId, containerBookingId, expectedVersion: Number(incrementedTask?.version), inventorySnapshotToken: effectiveBatchPayload.inventorySnapshotToken, items: [{ itemId: id(incrementalTaskItem.id), action: 'PICK', pickedPackageCount: 1, releaseMode: 'MANUAL', locationIds: [], version: Number(incrementalTaskItem.version) }], }, { 'Idempotency-Key': `${context.marker}-dynamic-stale-inventory`.slice(0, 128) }) context.addStep(MODULE, '增量后旧 inventory token 明确 A0409 并要求刷新', staleIncrementBatch, staleIncrementBatch.http === 200 && codeOf(staleIncrementBatch) === 'A0409' && String(staleIncrementBatch.body?.message || '').includes('库存已变化,请刷新后重试'), responseEvidence(staleIncrementBatch), OPTIONS) const taskAfterStaleResponse = await context.request('GET', taskDetailPath) const taskAfterStale = context.apiSuccess(MODULE, '旧 token 冲突后回查增量任务', taskAfterStaleResponse, '', OPTIONS) const planAfterStaleResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const planAfterStale = context.apiSuccess(MODULE, '旧 token 冲突后回查增量计划', planAfterStaleResponse, '', OPTIONS) context.assert(MODULE, '增量旧 token 冲突后 plan/task/items 零写入', taskAfterStaleResponse, Number(taskAfterStale?.version) === Number(incrementedTask?.version) && taskItemsSignature(taskAfterStale) === taskItemsSignature(incrementedTask) && Number(planAfterStale?.version) === Number(incrementedPlan?.version), `taskVersion=${taskAfterStale?.version ?? 'missing'}, planVersion=${planAfterStale?.version ?? 'missing'}`, OPTIONS) const removeResponse = await request(context, 'DELETE', `${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(incrementalPlanItem.id))}`, { expectedPlanVersion: Number(incrementedPlan.version), expectedItemVersion: Number(incrementalPlanItem.version), operationId: `${context.marker}-dynamic-remove-unpicked`.slice(0, 128), }) const removedPlan = context.apiSuccess(MODULE, '未拣动态增量明细允许删除', removeResponse, '', OPTIONS) context.assert(MODULE, '删除未拣项递增计划和任务版本且稳定身份不变', removeResponse, !removedPlan?.items?.some?.((item) => id(item?.id) === id(incrementalPlanItem.id)) && Number(removedPlan?.version) > Number(incrementedPlan?.version) && Number(removedPlan?.pdaTaskVersion) > Number(incrementedPlan?.pdaTaskVersion) && id(removedPlan?.pdaTaskId) === canonicalTaskId && removedPlan?.status === 'LOADING', `planVersion=${removedPlan?.version ?? 'missing'}, taskVersion=${removedPlan?.pdaTaskVersion ?? 'missing'}`, OPTIONS) const reassignResponse = await request(context, 'POST', `${smartLoadingBase}/plans/${loadingPlanId}/assignments`, { inboundId: incrementalInboundId, inboundDetailId: id(dynamicCandidate.inboundDetailId), packageCount: 1, expectedVersion: Number(removedPlan?.version), operationId: `${context.marker}-dynamic-reassign`.slice(0, 128), }) const reassignedPlan = context.apiSuccess(MODULE, '删除后重新加入同一动态入库单', reassignResponse, '', OPTIONS) let effectiveDynamicPlan = reassignedPlan let reassignedPlanItem = reassignedPlan?.items?.find?.((item) => id(item?.inboundId) === incrementalInboundId) context.assert(MODULE, '动态增量 fixture 至少保留两件可用数量供调整与最小已拣门禁', dynamicCandidatesResponse, Number(dynamicCandidate?.availablePackageCount) >= 2, `availablePackageCount=${dynamicCandidate?.availablePackageCount ?? 'missing'}`, OPTIONS) if (Number(dynamicCandidate?.availablePackageCount) < 2) return if (Number(dynamicCandidate?.availablePackageCount) >= 2 && reassignedPlanItem?.id) { const adjustResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(reassignedPlanItem.id))}`, { packageCount: 2, expectedPlanVersion: Number(reassignedPlan?.version), expectedItemVersion: Number(reassignedPlanItem.version), operationId: `${context.marker}-dynamic-adjust`.slice(0, 128), }) const adjustedPlan = context.apiSuccess(MODULE, '未拣动态增量明细允许调整数量', adjustResponse, '', OPTIONS) const adjustedItem = adjustedPlan?.items?.find?.((item) => id(item?.id) === id(reassignedPlanItem.id)) context.assert(MODULE, '调整数量递增 plan/task/item 版本并保持稳定任务', adjustResponse, Number(adjustedPlan?.version) > Number(reassignedPlan?.version) && Number(adjustedPlan?.pdaTaskVersion) > Number(reassignedPlan?.pdaTaskVersion) && Number(adjustedItem?.version) > Number(reassignedPlanItem?.version) && Number(adjustedItem?.packageCount) === 2 && adjustedItem?.warehouseRemark === reassignedPlanItem?.warehouseRemark && id(adjustedPlan?.pdaTaskId) === canonicalTaskId, `planVersion=${adjustedPlan?.version ?? 'missing'}, taskVersion=${adjustedPlan?.pdaTaskVersion ?? 'missing'}, itemVersion=${adjustedItem?.version ?? 'missing'}`, OPTIONS) effectiveDynamicPlan = adjustedPlan reassignedPlanItem = adjustedItem } const refreshedDynamicTaskResponse = await context.request('GET', taskDetailPath) const refreshedDynamicTask = context.apiSuccess(MODULE, '重新增量后刷新 PDA task detail', refreshedDynamicTaskResponse, '', OPTIONS) const refreshedDynamicTaskItem = refreshedDynamicTask?.items?.find?.((item) => id(item?.loadingPlanItemId) === id(reassignedPlanItem?.id)) const refreshedDynamicInventoryResponse = await context.request('GET', `${pdaBase}/tasks/${canonicalTaskId}/inventory-snapshot?warehouseId=${encodeURIComponent(warehouseId)}`) const refreshedDynamicInventory = context.apiSuccess(MODULE, '重新增量后刷新 inventory snapshot', refreshedDynamicInventoryResponse, '', OPTIONS) const dynamicBatchResponse = await request(context, 'POST', batchPath, { warehouseId, loadingPlanId, containerBookingId, expectedVersion: Number(refreshedDynamicTask?.version), inventorySnapshotToken: refreshedDynamicInventory?.snapshotToken, items: [{ itemId: id(refreshedDynamicTaskItem?.id), action: 'PICK', pickedPackageCount: Number(reassignedPlanItem?.packageCount), releaseMode: 'MANUAL', locationIds: [], version: Number(refreshedDynamicTaskItem?.version) }], }, { 'Idempotency-Key': `${context.marker}-dynamic-pick`.slice(0, 128) }) const dynamicPickedTask = context.apiSuccess(MODULE, '刷新后完成动态新增项拣货', dynamicBatchResponse, '', OPTIONS) context.assert(MODULE, '动态新增项拣完后同一任务重新进入 AWAITING_REVIEW', dynamicBatchResponse, id(dynamicPickedTask?.id) === canonicalTaskId && dynamicPickedTask?.workflowStatus === 'AWAITING_REVIEW' && dynamicPickedTask?.taskStatus === 'LOADING' && dynamicPickedTask?.items?.every?.((item) => item.itemStatus !== 'PENDING'), `taskId=${id(dynamicPickedTask?.id) || 'missing'}, workflow=${dynamicPickedTask?.workflowStatus || 'missing'}`, OPTIONS) const pickedDynamicPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const pickedDynamicPlan = context.apiSuccess(MODULE, '动态新增项拣完后回查 minPackageCount', pickedDynamicPlanResponse, '', OPTIONS) const pickedDynamicPlanItem = pickedDynamicPlan?.items?.find?.((item) => id(item?.id) === id(reassignedPlanItem?.id)) const beforePickedMutationTaskResponse = await context.request('GET', taskDetailPath) const beforePickedMutationTask = context.apiSuccess(MODULE, '已拣项负向前读取任务快照', beforePickedMutationTaskResponse, '', OPTIONS) const reducePickedResponse = await request(context, 'PATCH', `${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(pickedDynamicPlanItem?.id))}`, { packageCount: Math.max(1, Number(pickedDynamicPlanItem?.minPackageCount) - 1), expectedPlanVersion: Number(pickedDynamicPlan?.version), expectedItemVersion: Number(pickedDynamicPlanItem?.version), operationId: `${context.marker}-dynamic-reduce-picked`.slice(0, 128), }) context.addStep(MODULE, '已拣项减量低于 minPackageCount 返回 A0409', reducePickedResponse, reducePickedResponse.http === 200 && codeOf(reducePickedResponse) === 'A0409', responseEvidence(reducePickedResponse), OPTIONS) const deletePickedResponse = await request(context, 'DELETE', `${smartLoadingBase}/plans/${loadingPlanId}/items/${encodeURIComponent(id(pickedDynamicPlanItem?.id))}`, { expectedPlanVersion: Number(pickedDynamicPlan?.version), expectedItemVersion: Number(pickedDynamicPlanItem?.version), operationId: `${context.marker}-dynamic-delete-picked`.slice(0, 128), }) context.addStep(MODULE, '已拣项删除返回 A0409', deletePickedResponse, deletePickedResponse.http === 200 && codeOf(deletePickedResponse) === 'A0409', responseEvidence(deletePickedResponse), OPTIONS) const afterPickedMutationPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const afterPickedMutationPlan = context.apiSuccess(MODULE, '已拣项负向后回查计划', afterPickedMutationPlanResponse, '', OPTIONS) const afterPickedMutationTaskResponse = await context.request('GET', taskDetailPath) const afterPickedMutationTask = context.apiSuccess(MODULE, '已拣项负向后回查任务', afterPickedMutationTaskResponse, '', OPTIONS) context.assert(MODULE, '已拣项删除和减量失败后 plan/task/items 零写入', afterPickedMutationPlanResponse, Number(afterPickedMutationPlan?.version) === Number(pickedDynamicPlan?.version) && Number(afterPickedMutationTask?.version) === Number(beforePickedMutationTask?.version) && taskItemsSignature(afterPickedMutationTask) === taskItemsSignature(beforePickedMutationTask), `planVersion=${afterPickedMutationPlan?.version ?? 'missing'}, taskVersion=${afterPickedMutationTask?.version ?? 'missing'}`, OPTIONS) dynamicIncrementVerified = dynamicPickedTask?.workflowStatus === 'AWAITING_REVIEW' && Number(pickedDynamicPlanItem?.minPackageCount) === Number(reassignedPlanItem?.packageCount) && Number(effectiveDynamicPlan?.version) <= Number(pickedDynamicPlan?.version) } } } // 报关协同在本模块之后执行,最终 COMPLETE_LOADING 只由 phase3-loading-finalize 发起。 // 当前服务端明确不以拖车作为前置门禁,因此这里不得用一次预期失败的写请求维持 LOADING。 const stagedDetailResponse = await context.request('GET', detailPath) const stagedDetail = context.apiSuccess(MODULE, '下游协同前读取装柜中入库状态', stagedDetailResponse, '', OPTIONS) const stagedTaskResponse = await context.request('GET', taskDetailPath) const stagedTask = context.apiSuccess(MODULE, '下游协同前读取待最终完成 PDA 任务', stagedTaskResponse, '', OPTIONS) const stagedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`) const stagedPlan = context.apiSuccess(MODULE, '下游协同前读取装柜中计划', stagedPlanResponse, '', OPTIONS) context.assert(MODULE, '报关与最终完成前保持 LOADING 且不提前写 COMPLETE_LOADING', stagedDetailResponse, stagedDetail?.summary?.processStatus === 'LOADING' && stagedTask?.taskStatus === 'LOADING' && stagedTask?.workflowStatus === 'AWAITING_REVIEW' && stagedPlan?.status === 'LOADING', `inboundStatus=${stagedDetail?.summary?.processStatus || 'missing'}, taskStatus=${stagedTask?.taskStatus || 'missing'}, workflow=${stagedTask?.workflowStatus || 'missing'}, planStatus=${stagedPlan?.status || 'missing'}`, OPTIONS) context.createdResources.phase3Loading = { tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId, cartonMark: source?.boxCode || source?.cartonMark || null, cartonMarks: [...new Set((stagedTask?.items || []).flatMap((item) => item?.boxCodes || []) .map((value) => String(value || '').trim()).filter(Boolean))], fulfillmentType: source?.fulfillmentType || 'STANDARD', destinationType: initial?.summary?.destinationType || source?.destinationType || null, destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId), destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null, evidenceIds: evidenceItems.map((item) => id(item.id)), voidedEvidenceId: id(voidTarget?.id), evidenceTypes, sealNo: `SEAL-${suffix}`, missingTypeVoidedEvidenceId: id(missingTypeTarget?.id), replacementEvidenceFileId: replacementFileId, immutableEvidenceId: id(firstPhoto?.id), immutableEvidenceVersion: Number(firstPhoto?.version), postLoadedFileId, oceanBookingId: bookingId, loadingPlanId, containerBookingId, containerNo, warehouseId, canonicalPdaTaskId: canonicalTaskId, pickingTaskId: canonicalTaskId, inboundVersion: loadingVersion, atomicBatchVerified: awaitingReview && atomicBatchReplayVerified, inventorySnapshotVerified: inventorySnapshotVerified && atomicBatchReplayVerified, inventoryMutationVerified, inventoryMutationCleanup, dynamicIncrementVerified, incrementalInboundId, pdaTaskVersion: Number(stagedTask?.version), marker: context.marker, inboundStatus: 'LOADING', loadingPlanStatus: 'LOADING', finalStatus: 'AWAITING_REVIEW', } }