const MODULE = 'inbound-measurement' const SCENARIO = 'mojia-carton-measurement' const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } const REUSABLE_PALLET_CODES = Object.freeze(['TP-001', 'YW-TP-002']) const reusablePalletsByWarehouse = new Map() const WEIGHT_IMAGE = 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]) function asId(value) { return value == null ? '' : String(value) } function asNumber(value) { return Number(value) } function closeTo(actual, expected, tolerance = 0.000001) { return Number.isFinite(asNumber(actual)) && Math.abs(asNumber(actual) - expected) <= tolerance } function ceiling(value, scale) { const factor = 10 ** scale const floatingPointNoise = Number.EPSILON * Math.max(1, Math.abs(value)) * 4 return Math.ceil((value - floatingPointNoise) * factor) / factor } function halfUp(value, scale) { const factor = 10 ** scale return Math.round((value + Number.EPSILON) * factor) / factor } function query(tenantId) { return `tenantId=${encodeURIComponent(tenantId)}` } function validPdf(response) { return response.http === 200 && response.contentType.toLowerCase().includes('pdf') && response.bytes?.length > 4 && new TextDecoder().decode(response.bytes.slice(0, 4)) === '%PDF' } async function reusablePallets(context, warehouseId) { const cacheKey = asId(warehouseId) const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}` if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey) const pallets = [] if (allowAnyExistingPallet) { const response = await context.request('GET', `/api/platform/v1/pallets?page=1&size=100&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`) const page = context.apiSuccess(MODULE, '查询当前仓库已有可复用托盘', response, `warehouseId=${warehouseId}`, OPTIONS) const candidates = (page?.records || []).filter((item) => asId(item?.warehouseId) === cacheKey && asId(item?.id) && String(item?.palletCode || '').trim()) context.assert(MODULE, '当前仓库至少存在一个可复用托盘', response, candidates.length > 0, `warehouseId=${warehouseId}, matched=${candidates.map((item) => item.palletCode).join('|') || 'missing'}`, OPTIONS) if (candidates.length === 0) return null const selected = candidates.slice(0, 2).map((item) => ({ id: asId(item.id), code: String(item.palletCode) })) reusablePalletsByWarehouse.set(scopedCacheKey, selected) return selected } for (const palletCode of REUSABLE_PALLET_CODES) { const response = await context.request('GET', `/api/platform/v1/pallets?page=1&size=20&tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}&palletCode=${encodeURIComponent(palletCode)}`) const page = context.apiSuccess(MODULE, `查询原有可复用托盘 ${palletCode}`, response, `warehouseId=${warehouseId}`, OPTIONS) const exact = (page?.records || []).find((item) => asId(item?.warehouseId) === cacheKey && String(item?.palletCode || '') === palletCode && asId(item?.id)) context.assert(MODULE, `查询原有称重托盘 ${palletCode}`, response, true, `warehouseId=${warehouseId}, matched=${exact?.palletCode || 'missing'}, fallback=disabled`, OPTIONS) if (!exact) { context.assert(MODULE, '固定称重托盘主数据完整', response, false, `warehouseId=${warehouseId}, missing=${palletCode}`, OPTIONS) return null } pallets.push({ id: asId(exact.id), code: palletCode }) } reusablePalletsByWarehouse.set(scopedCacheKey, pallets) return pallets } function randomlyReusePalletIds(pallets, palletCount, randomValue) { const seed = Number.isInteger(Number(randomValue)) ? Number(randomValue) : Date.now() return Array.from({ length: palletCount }, (_, index) => pallets[(seed >>> (index % 31)) % pallets.length].id) } async function waitForCompletedMeasurement(context, measurementPath, expectedStatuses = ['WAIT_SPLIT'], attempts = 30) { let response = null let measurement = null for (let attempt = 1; attempt <= attempts; attempt += 1) { response = await context.request('GET', measurementPath) measurement = response?.http === 200 && String(response?.body?.code) === '200' ? response.body.data : null if (expectedStatuses.includes(measurement?.processStatus)) return { response, measurement, attempts: attempt } if (attempt < attempts) await new Promise((resolve) => setTimeout(resolve, 1_000)) } return { response, measurement, attempts } } export async function completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId, measurement, targetPalletNo = null, deferSeal = false, mergeToFirstPallet = false }) { const pendingResponse = await context.request('GET', `/api/platform/v1/first-leg/pda-dropship/tasks/pending?warehouseId=${warehouseId}&keyword=${encodeURIComponent(inboundCode)}`) const pending = context.apiSuccess(MODULE, '查询一件代发待合托任务', pendingResponse, `inboundId=${inboundId}`, OPTIONS) const task = (pending || []).find((item) => asId(item?.inboundId) === inboundId && asId(item?.warehouseId) === warehouseId) context.assert(MODULE, '测量审核后生成当前入仓单唯一合托任务', pendingResponse, Boolean(asId(task?.taskId)) && Number(task?.totalCartons) === Number(measurement?.actualPackageCount), `taskId=${asId(task?.taskId) || 'missing'}, totalCartons=${task?.totalCartons ?? 'missing'}`, OPTIONS) if (!task?.taskId) return null const taskId = asId(task.taskId) const claimResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/claim`, { warehouseId }) const claimed = context.apiSuccess(MODULE, '领取一件代发合托任务', claimResponse, `taskId=${taskId}`, OPTIONS) context.assert(MODULE, '合托任务绑定当前操作员且进入CLAIMED', claimResponse, claimed?.taskStatus === 'CLAIMED' && asId(claimed?.inboundId) === inboundId, `taskStatus=${claimed?.taskStatus ?? 'missing'}`, OPTIONS) const totalCartons = Number(measurement?.actualPackageCount) const expectedPalletCount = Number(measurement?.expectedPalletCount) const taskPath = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}?warehouseId=${warehouseId}` if (!claimed?.tenantId || claimed.taskStatus !== 'CLAIMED') return null // Task.pallets contains pallets with actual bound cartons, not unused pre-generated pallets. const palletResponse = await context.request('GET', `/api/platform/v1/first-leg/pda-dropship/inbound-orders/${inboundId}/pallets?accountId=${asId(claimed.tenantId)}`) const palletData = context.apiSuccess(MODULE, '读取本轮入仓单预生成托盘', palletResponse, '', OPTIONS) const generatedPallets = Array.isArray(palletData) ? palletData : [] const palletSetupValid = Number(claimed.generatedPalletCount) === expectedPalletCount && claimed.palletSetupReady === true && generatedPallets.length === expectedPalletCount && generatedPallets.every((item) => item?.status === 'OPEN' && Number(item?.cartonCount) === 0 && /^DP-\d{6}-\d{4}$/.test(item?.palletNo || '')) context.assert(MODULE, '审核通过时已按预计托数预生成全部DP托盘码', palletResponse, palletSetupValid, `expected=${expectedPalletCount}, generated=${generatedPallets.length}`, OPTIONS) if (!palletSetupValid) return null let countedCartons = 0 const sealedCartonMarks = [] for (let index = 0; index < totalCartons; index += 1) { const boxCode = `${inboundCode}#${totalCartons}-${index + 1}` const resolveResponse = await context.request('GET', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/cartons/resolve?warehouseId=${warehouseId}&boxCode=${encodeURIComponent(boxCode)}`) const resolved = context.apiSuccess(MODULE, `解析第${index + 1}箱的冻结箱唛`, resolveResponse, `boxCode=${boxCode}`, OPTIONS) const resolvedSkuLines = Array.isArray(resolved?.skuLines) ? resolved.skuLines : [] context.assert(MODULE, `第${index + 1}箱允许清点且每箱SKU明细完整`, resolveResponse, resolved?.canCount === true && resolvedSkuLines.length > 0 && resolvedSkuLines.every((line) => asId(line?.inboundDetailId) && line?.skuCode && Number(line?.expectedQuantity) > 0), `canCount=${resolved?.canCount ?? 'missing'}, skuLines=${resolvedSkuLines.length}`, OPTIONS) if (resolved?.canCount !== true || resolvedSkuLines.length < 1) continue const countPayload = { warehouseId, boxCode, skuLines: resolvedSkuLines.map((line) => ({ inboundDetailId: asId(line.inboundDetailId), skuCode: line.skuCode, actualQuantity: Number(line.expectedQuantity), discrepancyAction: 'MATCH', discrepancyRemark: null, })), palletNo: index === 0 ? null : (targetPalletNo || generatedPallets[Math.min(index, generatedPallets.length - 1)].palletNo), operationId: `${context.marker}-dropship-count-${index + 1}`.slice(0, 128), } const countResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/cartons/count`, countPayload) const counted = context.apiSuccess(MODULE, `清点第${index + 1}箱并冻结实收SKU数`, countResponse, `boxCode=${boxCode}`, OPTIONS) context.assert(MODULE, `第${index + 1}箱清点结果与冻结箱唛一致`, countResponse, counted?.carton?.boxCode === resolved?.boxCode && Array.isArray(counted?.carton?.skuLines) && counted.carton.skuLines.length === resolvedSkuLines.length && counted.carton.skuLines.every((line) => Number(line?.actualQuantity) === Number(line?.expectedQuantity) && line?.discrepancyStatus === 'MATCH'), `cartonId=${asId(counted?.carton?.cartonId) || 'missing'}, skuLines=${counted?.carton?.skuLines?.length ?? 'missing'}`, OPTIONS) if (counted?.carton?.boxCode === resolved?.boxCode) { countedCartons += 1 sealedCartonMarks.push(resolved.boxCode) } if (index === 0) { context.assert(MODULE, '先扫箱模式将已清点箱暂存为待组托', countResponse, Number(counted?.unboundCartons) === 1 && counted?.pallet == null, `unboundCartons=${counted?.unboundCartons ?? 'missing'}`, OPTIONS) const replayResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/cartons/count`, countPayload) const replayed = context.apiSuccess(MODULE, '重复清点操作按operationId幂等返回', replayResponse, `boxCode=${boxCode}`, OPTIONS) context.assert(MODULE, '重复清点没有重复累计箱数', replayResponse, Number(replayed?.countedCartons) === Number(counted?.countedCartons) && Number(replayed?.unboundCartons) === Number(counted?.unboundCartons), `counted=${replayed?.countedCartons ?? 'missing'}, unbound=${replayed?.unboundCartons ?? 'missing'}`, OPTIONS) const firstPallet = targetPalletNo ? { palletNo: targetPalletNo } : generatedPallets[0] const bindPayload = { warehouseId, palletNo: firstPallet.palletNo, operationId: `${context.marker}-dropship-bind-first`.slice(0, 128) } const bindResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/bind-pending`, bindPayload) const bound = context.apiSuccess(MODULE, '后扫预生成托盘码并绑定全部待组托箱', bindResponse, `taskId=${taskId}`, OPTIONS) const pallet = bound?.pallets?.find((item) => item.palletNo === firstPallet.palletNo) || null context.assert(MODULE, '待组托箱绑定到OPEN托盘且不再游离', bindResponse, Number(bound?.unboundCartons) === 0 && (targetPalletNo ? pallet?.cartons?.filter((item) => asId(item.inboundId) === inboundId).length === 1 : Number(pallet?.cartonCount) === 1) && pallet?.status === 'OPEN' && pallet?.palletNo === firstPallet.palletNo && bound?.pallets?.length === 1, `unbound=${bound?.unboundCartons ?? 'missing'}, palletStatus=${pallet?.status ?? 'missing'}`, OPTIONS) const replayBindResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/bind-pending`, bindPayload) const replayBind = context.apiSuccess(MODULE, '预生成托盘绑定同键重放', replayBindResponse, '', OPTIONS) context.assert(MODULE, '预生成托盘绑定重放不新增托盘且计数稳定', replayBindResponse, replayBind?.pallets?.length === bound?.pallets?.length && Number(replayBind?.boundCartons) === Number(bound?.boundCartons) && Number(replayBind?.unboundCartons) === 0, '', OPTIONS) } else { const expectedPallet = targetPalletNo ? { palletNo: targetPalletNo } : generatedPallets[Math.min(index, generatedPallets.length - 1)] context.assert(MODULE, `第${index + 1}箱直接进入当前OPEN托盘`, countResponse, counted?.pallet?.palletNo === expectedPallet.palletNo && counted?.pallet?.status === 'OPEN', `palletNo=${counted?.pallet?.palletNo ?? 'missing'}, status=${counted?.pallet?.status ?? 'missing'}`, OPTIONS) } } const countedTaskResponse = await context.request('GET', taskPath) let sealed = context.apiSuccess(MODULE, '清点完成后回查预生成托盘分配', countedTaskResponse, '', OPTIONS) context.assert(MODULE, '预生成托盘覆盖测量冻结的全部箱数', countedTaskResponse, totalCartons > 0 && countedCartons === totalCartons && Number(sealed?.boundCartons) === totalCartons && sealed?.pallets?.length === (targetPalletNo ? 1 : expectedPalletCount) && sealed?.pallets?.every((item) => Number(item?.cartonCount) > 0), `counted=${countedCartons}, bound=${sealed?.boundCartons ?? 'missing'}, expected=${totalCartons || 'missing'}`, OPTIONS) if (mergeToFirstPallet) { const target = sealed?.pallets?.[0] const sources = (sealed?.pallets || []).slice(1) context.assert(MODULE, '单票合托前确有多个实物托盘', countedTaskResponse, Boolean(target) && sources.length > 0, '', OPTIONS) if (!target || sources.length === 0) return null for (const source of sources) { for (const carton of source.cartons || []) { const payload = { warehouseId, targetPalletNo: target.palletNo, operationId: `${context.marker}-merge-${carton.cartonId}`.slice(0, 128) } const path = `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/cartons/${carton.cartonId}/move` const movedResponse = await context.request('POST', path, payload) sealed = context.apiSuccess(MODULE, '通过正式移箱接口合并单票托盘', movedResponse, '', OPTIONS) const replayResponse = await context.request('POST', path, payload) const replay = context.apiSuccess(MODULE, '单票合托移箱同键重放', replayResponse, '', OPTIONS) context.assert(MODULE, '移箱重放保持总箱数守恒', replayResponse, Number(replay?.boundCartons) === totalCartons && Number(replay?.unboundCartons) === 0, '', OPTIONS) } } context.assert(MODULE, '单票合托后仅一个实物托盘且箱唛不丢失', null, sealed?.pallets?.length === 1 && sealed.pallets[0].palletNo === target.palletNo && sealed.pallets[0].cartons?.length === totalCartons && new Set(sealed.pallets[0].cartons.map((item) => item.boxCode)).size === totalCartons, '', OPTIONS) } if (deferSeal) { context.assert(MODULE, '多票合托首票归托后保持开放等待下一票', null, sealed?.taskStatus !== 'COMPLETED' && sealed?.pallets?.every((item) => item.status === 'OPEN'), '', OPTIONS) return { ...sealed, sealedCartonMarks, sealedSourcePalletNos: [] } } let lastSealResponse = countedTaskResponse for (let index = 0; index < (sealed?.pallets || []).length; index += 1) { const pallet = sealed.pallets.find((item) => item.status === 'OPEN') if (!pallet) break const sealResponse = await context.request('POST', `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/${asId(pallet.palletId)}/seal`, { warehouseId, expectedVersion: Number(pallet.version), operationId: `${context.marker}-dropship-seal-${index + 1}`.slice(0, 128), }) lastSealResponse = sealResponse sealed = context.apiSuccess(MODULE, `封托第${index + 1}个预生成托盘`, sealResponse, `taskId=${taskId}, palletNo=${pallet.palletNo}`, OPTIONS) } context.assert(MODULE, '全部箱数封托后入仓单允许进入WAIT_SPLIT', lastSealResponse, sealed?.taskStatus === 'COMPLETED' && Number(sealed?.countedCartons) === Number(sealed?.totalCartons) && Number(sealed?.boundCartons) === Number(sealed?.totalCartons) && Number(sealed?.unboundCartons) === 0 && sealed?.pallets?.every((item) => item?.status === 'SEALED'), `taskStatus=${sealed?.taskStatus ?? 'missing'}, counted=${sealed?.countedCartons ?? 'missing'}, bound=${sealed?.boundCartons ?? 'missing'}`, OPTIONS) return { ...sealed, sealedCartonMarks, sealedSourcePalletNos: (sealed?.pallets || []).map((item) => item.palletNo).filter(Boolean) } } async function provisionWarehouseResources(context, { inboundId, tenantId, warehouseId, palletCount }) { if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null const resources = [] const candidates = await reusablePallets(context, warehouseId) if (!candidates) return null const palletIds = randomlyReusePalletIds(candidates, palletCount, context.context?.waitSplitVariant?.randomValue) const candidateKeyword = candidates.find((item) => item.id === palletIds[0])?.code || '' const candidateResponse = await context.request('GET', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-pallet-options?${query(tenantId)}&page=1&size=20&keyword=${encodeURIComponent(candidateKeyword)}`) const candidatePage = context.apiSuccess(MODULE, '按关键词分页查询当前入库仓称重托盘候选', candidateResponse, `warehouseId=${warehouseId}, keyword=${candidateKeyword}`, OPTIONS) const pageControlled = Array.isArray(candidatePage?.records) && candidatePage.records.length <= 20 && Number(candidatePage?.page) === 1 && Number(candidatePage?.size) === 20 && Number(candidatePage?.total) >= candidatePage.records.length && candidatePage.records.some((item) => asId(item?.palletId) === palletIds[0]) context.assert(MODULE, '托盘候选保持服务端关键词分页且不返回无界数组', candidateResponse, pageControlled, `records=${candidatePage?.records?.length ?? 'missing'}, page=${candidatePage?.page ?? 'missing'}, size=${candidatePage?.size ?? 'missing'}, total=${candidatePage?.total ?? 'missing'}`, OPTIONS) if (!pageControlled) return null for (let index = 1; index <= palletCount; index += 1) { const palletId = palletIds[index - 1] context.assert(MODULE, `随机复用原有称重托盘 ${index}/${palletCount}`, null, Boolean(candidates.find((item) => item.id === palletId)?.code), `warehouseId=${warehouseId}, palletId=${palletId}`, OPTIONS) // 库位由 Warehouse 在测量审核时按正式规则分配;普通位不足会复用唯一的 A0 共享溢出位。 // 验收脚本不得为了制造容量而新增普通库位。 resources.push({ palletId, locationId: null }) } context.createdResources.inboundWarehouseResources ??= {} context.createdResources.inboundWarehouseResources[inboundId] = { marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount, reusablePalletCodes: candidates.map((item) => item.code), resources, } return { palletId: palletIds[0], palletIds, palletCount, resources } } async function createConfirmedWeightEvidence(context, { inboundId, tenantId, palletIds, grossWeightPerPalletKg, }) { if (!Array.isArray(palletIds) || palletIds.length < 1) return null const confirmedResults = [] const guideEvolution = [] for (let index = 0; index < palletIds.length; index += 1) { const palletId = palletIds[index] const uploadResponse = await context.requestMultipart('POST', `/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, { content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}-${index + 1}.png`, }) const upload = context.apiSuccess(MODULE, `上传称重图片并执行 OCR ${index + 1}/${palletIds.length}`, uploadResponse, `inboundId=${inboundId}`, OPTIONS) const fileId = asId(upload?.fileId) if (!fileId) return null const createResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, { fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null, ocrWeightValue: null, ocrWeightUnit: null, confidence: null, }) const created = context.apiSuccess(MODULE, `登记称重图片与托盘快照 ${index + 1}/${palletIds.length}`, createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS) if (!created?.id) return null context.assert(MODULE, `OCR无有效重量时转人工确认 ${index + 1}/${palletIds.length}`, createResponse, created.recognitionStatus === 'NEEDS_MANUAL' && created.ocrWeightValue == null, `recognitionStatus=${created.recognitionStatus || 'missing'}, ocrWeightValue=${created.ocrWeightValue ?? 'null'}`, OPTIONS) const grossWeight = Number(grossWeightPerPalletKg) + index * 5 const confirmResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, { version: Number(created.version), adjustedWeightValue: grossWeight, adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 自动化人工确认称重`, }) const confirmed = context.apiSuccess(MODULE, `人工确认称重图片净重 ${index + 1}/${palletIds.length}`, confirmResponse, `resultId=${created.id}, palletId=${palletId}`, OPTIONS) const netWeightKg = Number(confirmed?.netWeightKg) const grossWeightKg = Number(confirmed?.grossWeightKg) if (confirmed?.recognitionStatus !== 'CONFIRMED' || !Number.isFinite(netWeightKg) || netWeightKg <= 0) { return null } const staleConfirmResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, { version: Number(created.version), adjustedWeightValue: grossWeight + 1, adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 旧版本冲突验证`, }) context.assert(MODULE, `称重旧版本确认被拒绝且不覆盖已确认结果 ${index + 1}/${palletIds.length}`, staleConfirmResponse, staleConfirmResponse.http === 200 && String(staleConfirmResponse.body?.code) === 'A0409', `HTTP=${staleConfirmResponse.http}, code=${staleConfirmResponse.body?.code ?? 'missing'}, requestId=${staleConfirmResponse.requestId || 'missing'}`, OPTIONS) confirmedResults.push({ id: asId(created.id), palletId, netWeightKg, grossWeightKg, version: Number(confirmed?.version), }) if (context.options?.verifyPalletLocationGuide) { const guideResponse = await context.request('GET', `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`) const guideWorkspace = context.apiSuccess(MODULE, `逐张确认后回查整票库位块 ${index + 1}/${palletIds.length}`, guideResponse, `resultId=${asId(created.id)}, confirmedCount=${confirmedResults.length}`, OPTIONS) const verified = verifyPalletLocationGuide(context, guideResponse, guideWorkspace, confirmedResults.map((result) => result.id), `第${index + 1}张确认后`) const previous = guideEvolution.at(-1) if (previous) { const priorSlotsStable = previous.slots.every((prior) => { const current = verified.slots.find((slot) => Number(slot?.slotNo) === Number(prior?.slotNo)) return current && asId(current.reservationId) === asId(prior.reservationId) && asId(current.locationId) === asId(prior.locationId) && String(current.locationCode || '') === String(prior.locationCode || '') && Boolean(current.sharedOverflow) === Boolean(prior.sharedOverflow) }) context.assert(MODULE, `第${index + 1}张确认后扩容只追加且既有槽身份不漂移`, guideResponse, asId(verified.guide?.blockId) === previous.blockId && verified.slots.length >= previous.slots.length && priorSlotsStable, `beforeBlock=${previous.blockId || 'missing'}, afterBlock=${asId(verified.guide?.blockId) || 'missing'}, beforeSlots=${previous.slots.length}, afterSlots=${verified.slots.length}`, OPTIONS) } guideEvolution.push({ blockId: asId(verified.guide?.blockId), slots: verified.slots.map((slot) => ({ slotNo: Number(slot?.slotNo), reservationId: asId(slot?.reservationId), locationId: asId(slot?.locationId), locationCode: String(slot?.locationCode || ''), sharedOverflow: Boolean(slot?.sharedOverflow), })), }) } } const workspaceResponse = await context.request('GET', `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}`) const workspace = context.apiSuccess(MODULE, '回查已确认称重图片和货物总重量', workspaceResponse, `resultIds=${confirmedResults.map((result) => result.id).join(',')}`, OPTIONS) const confirmedRows = (workspace?.weightResults || []) .filter((result) => result?.recognitionStatus === 'CONFIRMED') const grossWeightKg = confirmedResults.reduce((sum, result) => sum + result.grossWeightKg, 0) const netWeightKg = confirmedResults.reduce((sum, result) => sum + result.netWeightKg, 0) const persisted = confirmedRows.length === palletIds.length && confirmedResults.every((expected) => confirmedRows.some((row) => asId(row?.id) === expected.id && asId(row?.palletId) === expected.palletId && String(row?.palletCode || '').trim() && String(row?.palletType || '').trim() && Number.isFinite(Number(row?.palletTareWeightKg)))) && closeTo(workspace?.actualGrossWeightKg, grossWeightKg, 0.001) && closeTo(workspace?.actualNetWeightKg, netWeightKg, 0.001) context.assert(MODULE, '图片张数决定托数且净重汇总已落地', workspaceResponse, persisted, `confirmedImages=${confirmedRows.length}, expectedPallets=${palletIds.length}, grossWeight=${grossWeightKg}, netWeight=${netWeightKg}, actualGross=${workspace?.actualGrossWeightKg ?? 'missing'}, actualNet=${workspace?.actualNetWeightKg ?? 'missing'}`, OPTIONS) return persisted ? { count: confirmedRows.length, grossWeightKg, netWeightKg, confirmedResults, guideEvolution } : null } function verifyPalletLocationGuide(context, response, workspace, expectedWeightResultIds, name) { const guide = workspace?.palletLocationGuide const slots = Array.isArray(guide?.slots) ? guide.slots : [] const weights = Array.isArray(workspace?.weightResults) ? workspace.weightResults : [] const boundSlots = slots.filter((slot) => asId(slot?.boundWeightResultId)) const emptySlots = slots.filter((slot) => slot?.slotStatus === 'RESERVED_EMPTY') const identityComplete = asId(guide?.blockId) && slots.length === Number(guide?.reservedSlotCount) && Number(guide?.actualPalletCount) === boundSlots.length && Number(guide?.reservedEmptySlotCount) === emptySlots.length && slots.every((slot) => Number.isInteger(Number(slot?.slotNo)) && Number(slot.slotNo) > 0 && asId(slot?.reservationId) && asId(slot?.locationId) && String(slot?.locationCode || '').trim() && typeof slot?.sharedOverflow === 'boolean' && ['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'].includes(String(slot?.slotStatus || ''))) context.assert(MODULE, `${name}整票库位块计数与槽身份完整`, response, Boolean(identityComplete), `blockId=${asId(guide?.blockId) || 'missing'}, reserved=${guide?.reservedSlotCount ?? 'missing'}, actual=${guide?.actualPalletCount ?? 'missing'}, empty=${guide?.reservedEmptySlotCount ?? 'missing'}, slots=${slots.length}`, OPTIONS) const bindingsStable = expectedWeightResultIds.every((resultId) => { const row = weights.find((item) => asId(item?.id) === resultId) const slot = slots.find((item) => asId(item?.boundWeightResultId) === resultId) return row && slot && asId(row.blockId) === asId(guide?.blockId) && Number(row.slotNo) === Number(slot.slotNo) && asId(row.reservationId) === asId(slot.reservationId) && asId(row.locationId) === asId(slot.locationId) && String(row.locationCode || '') === String(slot.locationCode || '') && Boolean(row.sharedOverflow) === Boolean(slot.sharedOverflow) }) context.assert(MODULE, `${name}称重图片按 resultId 显式绑定槽位`, response, bindingsStable, `expectedResultIds=${expectedWeightResultIds.join('|') || 'none'}, boundResultIds=${boundSlots.map((slot) => asId(slot.boundWeightResultId)).join('|') || 'none'}`, OPTIONS) return { guide, slots, weights, identityComplete: Boolean(identityComplete), bindingsStable } } async function recreateConfirmedWeightEvidence(context, { inboundId, tenantId, palletId, grossWeightKg, }) { const uploadResponse = await context.requestMultipart('POST', `/api/platform/v1/files/measurements/${tenantId}/${inboundId}`, { content: WEIGHT_IMAGE, type: 'image/png', name: `weight-${inboundId}-replacement.png`, }) const upload = context.apiSuccess(MODULE, '重新上传替代称重图片', uploadResponse, `inboundId=${inboundId}`, OPTIONS) const fileId = asId(upload?.fileId) if (!fileId) return null const createResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results?${query(tenantId)}`, { fileId, palletId, ocrProvider: 'ADMIN_ACCEPTANCE', ocrRawText: null, ocrWeightValue: null, ocrWeightUnit: null, confidence: null, }) const created = context.apiSuccess(MODULE, '登记替代称重图片与托盘快照', createResponse, `fileId=${fileId}, palletId=${palletId}`, OPTIONS) if (!created?.id) return null const confirmResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${created.id}/confirm?${query(tenantId)}`, { version: Number(created.version), adjustedWeightValue: Number(grossWeightKg), adjustedWeightUnit: 'KG', adjustmentReason: `${context.marker} 删除后替代称重确认`, }) const confirmed = context.apiSuccess(MODULE, '确认替代称重图片并复用空槽', confirmResponse, `resultId=${asId(created.id)}, palletId=${palletId}`, OPTIONS) const valid = confirmed?.recognitionStatus === 'CONFIRMED' && Number.isFinite(Number(confirmed?.grossWeightKg)) && Number.isFinite(Number(confirmed?.netWeightKg)) context.assert(MODULE, '替代图片形成新的已确认称重结果', confirmResponse, valid, `resultId=${asId(confirmed?.id) || 'missing'}, status=${confirmed?.recognitionStatus || 'missing'}`, OPTIONS) return valid ? { id: asId(confirmed.id), palletId: asId(confirmed.palletId), grossWeightKg: Number(confirmed.grossWeightKg), netWeightKg: Number(confirmed.netWeightKg), version: Number(confirmed.version), } : null } function findIsolatedInbound(context) { const roots = [context.createdResources, context.context] const visited = new Set() const candidates = [] function visit(value, inheritedMarker = '') { if (!value || typeof value !== 'object' || visited.has(value)) return visited.add(value) const marker = String(value.marker || value.testMarker || inheritedMarker || '') const inboundId = asId(value.inboundId || value.inbound?.id || value.inbound?.inboundId) const tenantId = asId(value.tenantId || value.customerId || value.inbound?.tenantId) if (marker === context.marker && inboundId && tenantId) { candidates.push({ inboundId, tenantId, marker, source: value }) } for (const [key, child] of Object.entries(value)) { if (key === 'temporaryPassword' || key === 'token') continue if (child && typeof child === 'object') visit(child, marker) } } roots.forEach((root) => visit(root)) return candidates[0] || null } async function multipartRequest(context, path, data) { const form = new FormData() form.append('data', new Blob([JSON.stringify(data)], { type: 'application/json' })) form.append('image', new Blob([WEIGHT_IMAGE], { type: 'image/png' }), 'carton-evidence.png') 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 content = await response.text() let body = null try { body = content ? JSON.parse(content) : null } catch { /* Preserve only a bounded error string. */ } return { method: 'POST', path, http: response.status, body, raw: body === null ? content.slice(0, 500) : null, requestId: body?.requestId || response.headers.get('x-request-id'), traceId: body?.traceId || response.headers.get('x-trace-id'), } } catch (error) { return { method: 'POST', path, http: 0, body: null, raw: error instanceof Error ? error.message : String(error), requestId: null, traceId: null } } } async function requestWithHeaders(context, method, path, body, extraHeaders) { try { const response = await fetch(`${context.options.gateway}${path}`, { method, headers: { Accept: 'application/json', Authorization: `Bearer ${context.token}`, 'Content-Type': 'application/json; charset=utf-8', ...extraHeaders, }, body: JSON.stringify(body), signal: AbortSignal.timeout(30_000), }) const content = await response.text() let payload = null try { payload = content ? JSON.parse(content) : null } catch { /* Preserve only a bounded error string. */ } return { method, path, http: response.status, body: payload, raw: payload === null ? content.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: error instanceof Error ? error.message : String(error), requestId: null, traceId: null } } } async function updateConfirmedPackageCount(context, measurementPath, itemId, body) { const updatePath = measurementPath.replace(/\?.*$/, '') + `/items/${encodeURIComponent(itemId)}/confirmed-package-count?${measurementPath.split('?')[1] || ''}` let lastResponse = null for (let attempt = 1; attempt <= 2; attempt += 1) { lastResponse = await context.request('PUT', updatePath, body) if (lastResponse?.http !== 0) return lastResponse const recovered = await context.request('GET', measurementPath) const detail = recovered?.http === 200 && String(recovered?.body?.code) === '200' ? recovered.body?.data : null const item = detail?.items?.find((candidate) => asId(candidate?.id) === asId(itemId)) if (Number(item?.confirmedPackageCount) === Number(body.confirmedPackageCount) && Number(detail?.version) > Number(body.measurementVersion)) { return { ...recovered, method: 'GET', path: measurementPath, recoveredFromTransportFailure: true, body: { ...recovered.body, data: { ...item, measurementVersion: Number(detail.version) } }, } } if (attempt < 2) await new Promise((resolve) => setTimeout(resolve, 500)) } return lastResponse } function finishScenario(context, start) { const steps = context.steps.slice(start) context.scenarios.push({ name: SCENARIO, module: MODULE, success: steps.every((step) => step.passed), deploymentBlocked: steps.some((step) => step.deploymentBlocker), passed: steps.filter((step) => step.passed).length, failed: steps.filter((step) => !step.passed).length, }) } export async function run(context) { const start = context.steps.length const isolated = findIsolatedInbound(context) if (!isolated) { context.addStep(MODULE, '取得隔离的待测量入库单', null, false, `orders 模块未提供 marker=${context.marker} 的 inboundId/tenantId;为避免修改随机历史订单,本模块停止写入`, OPTIONS) finishScenario(context, start) return } const { inboundId, tenantId } = isolated const detailPath = `/api/platform/inbounds/${inboundId}?${query(tenantId)}` const inboundResponse = await context.request('GET', detailPath) const inboundDetail = context.apiSuccess(MODULE, '查询隔离入库单详情', inboundResponse, `inboundId=${inboundId}, tenantId=${tenantId}`, OPTIONS) const summary = inboundDetail?.summary const provenanceOk = asId(summary?.id) === inboundId && asId(summary?.tenantId) === tenantId && ['WAIT_MEASURE', 'MEASURING'].includes(summary?.processStatus) context.assert(MODULE, '隔离入库单身份与可测量状态正确', inboundResponse, provenanceOk, `identity=${asId(summary?.id) === inboundId && asId(summary?.tenantId) === tenantId}, status=${summary?.processStatus ?? 'missing'}`, OPTIONS) if (!provenanceOk) { finishScenario(context, start) return } const inboundCode = String(summary.inboundCode || '') if (!inboundCode) { context.assert(MODULE, '箱码生成前置数据完整', inboundResponse, false, 'inboundCode=missing', OPTIONS) finishScenario(context, start) return } const labelPath = `/api/platform/inbounds/${inboundId}/carton-label-pdf?${query(tenantId)}` const labelResponse = await context.requestBinary('POST', labelPath) const pdfOk = validPdf(labelResponse) context.assert(MODULE, '生成箱唛 PDF 并冻结稳定箱码', labelResponse, pdfOk, `contentType=${labelResponse.contentType || 'missing'}, bytes=${labelResponse.bytes?.length ?? 0}`, OPTIONS) if (!pdfOk) { finishScenario(context, start) return } const frozenInboundResponse = await context.request('GET', detailPath) const frozenInbound = context.apiSuccess(MODULE, '首次打印后回查冻结箱数', frozenInboundResponse, '', OPTIONS) const frozenSummary = frozenInbound?.summary const originalPackageCount = Number(frozenSummary?.totalPackageCount) context.assert(MODULE, '首次打印按订单货物冻结原始总箱数', frozenInboundResponse, frozenSummary?.inboundCode === inboundCode && Number.isInteger(originalPackageCount) && originalPackageCount >= 1, `inboundCodeStable=${frozenSummary?.inboundCode === inboundCode}, totalPackageCount=${frozenSummary?.totalPackageCount ?? 'missing'}`, OPTIONS) if (!Number.isInteger(originalPackageCount) || originalPackageCount < 1) { finishScenario(context, start) return } const boxCode = `${inboundCode}#${originalPackageCount}-1` const reprintResponse = await context.requestBinary('POST', labelPath) context.assert(MODULE, '重复整单打印复用已冻结箱码映射', reprintResponse, validPdf(reprintResponse), `boxCode=${boxCode}, contentType=${reprintResponse.contentType || 'missing'}, bytes=${reprintResponse.bytes?.length ?? 0}`, OPTIONS) const partialPrintPath = `${labelPath}&cartonIndex=1` const partialPrintResponse = await context.requestBinary('POST', partialPrintPath) context.assert(MODULE, '指定第1箱补打复用已冻结箱码映射', partialPrintResponse, validPdf(partialPrintResponse), `boxCode=${boxCode}, cartonIndex=1, contentType=${partialPrintResponse.contentType || 'missing'}, bytes=${partialPrintResponse.bytes?.length ?? 0}`, OPTIONS) if (!validPdf(reprintResponse) || !validPdf(partialPrintResponse)) { finishScenario(context, start) return } const scanPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/carton-scans?${query(tenantId)}` const measurementProfile = context.context?.waitSplitVariant?.measurementProfile const firstDimensions = measurementProfile?.first || { length: 59.1, width: 40.2, height: 30.3 } const firstShipmentId = `FBA-${context.marker.slice(-12)}-01` const firstScanResponse = await multipartRequest(context, scanPath, { boxCode, ...firstDimensions, scannedShipmentId: firstShipmentId, machineNo: 'MJ-ADMIN-E2E-01', measuredAt: new Date().toISOString().slice(0, 19), }) const firstScan = context.apiSuccess(MODULE, '模拟墨家首次扫描单箱', firstScanResponse, `boxCode=${boxCode}, machineNo=MJ-ADMIN-E2E-01`, OPTIONS) context.assert(MODULE, '首次扫描新增一条且默认代表1箱', firstScanResponse, firstScan?.created === true && asId(firstScan?.boxCode) === boxCode && Number(firstScan?.confirmedPackageCount) === 1 && asId(firstScan?.itemId), `created=${firstScan?.created}, confirmedPackageCount=${firstScan?.confirmedPackageCount ?? 'missing'}`, OPTIONS) context.assert(MODULE, '重复打印及指定箱补打后原箱码仍可解析', firstScanResponse, firstScan?.created === true && firstScan?.boxCode === boxCode, `boxCodeStable=${firstScan?.boxCode === boxCode}, resolvedInboundId=${inboundId}`, OPTIONS) if (!firstScan?.itemId || !firstScan?.measurementId) { finishScenario(context, start) return } const measurementPath = `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement?${query(tenantId)}` const firstQueryResponse = await context.request('GET', measurementPath) const firstQuery = context.apiSuccess(MODULE, '查询箱级测量详情', firstQueryResponse, `measurementId=${asId(firstScan.measurementId)}`, OPTIONS) const firstItem = firstQuery?.items?.find((item) => asId(item.id) === asId(firstScan.itemId)) context.assert(MODULE, '箱级查询回显箱码、尺寸、图片与设备信息', firstQueryResponse, firstItem?.boxCode === boxCode && asId(firstItem?.intelligentImageFileId) && firstItem?.machineNo === 'MJ-ADMIN-E2E-01' && firstItem?.scannedShipmentId === firstShipmentId && closeTo(firstItem?.lengthCm, firstDimensions.length) && closeTo(firstItem?.widthCm, firstDimensions.width) && closeTo(firstItem?.heightCm, firstDimensions.height), `itemFound=${Boolean(firstItem)}, imageFileId=${asId(firstItem?.intelligentImageFileId) || 'missing'}`, OPTIONS) const countResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId, { measurementVersion: Number(firstScan.measurementVersion), confirmedPackageCount: originalPackageCount }) const countResult = context.apiSuccess(MODULE, '人工确认当前测量代表箱数', countResponse, `confirmedPackageCount=${originalPackageCount}, transportRecovered=${countResponse?.recoveredFromTransportFailure === true}`, OPTIONS) context.assert(MODULE, '人工箱数更新且测量版本递增', countResponse, Number(countResult?.confirmedPackageCount) === originalPackageCount && Number(countResult?.measurementVersion) === Number(firstScan.measurementVersion) + 1, `version=${countResult?.measurementVersion ?? 'missing'}`, OPTIONS) const replacement = measurementProfile?.replacement || { length: 59.5, width: 41.2, height: 31.3 } const replacementShipmentId = `FBA-${context.marker.slice(-12)}-02` const rescanResponse = await multipartRequest(context, scanPath, { boxCode, ...replacement, scannedShipmentId: replacementShipmentId, machineNo: 'MJ-ADMIN-E2E-02', measuredAt: new Date().toISOString().slice(0, 19), }) const rescan = context.apiSuccess(MODULE, '同箱号重新扫描覆盖当前记录', rescanResponse, `boxCode=${boxCode}, machineNo=MJ-ADMIN-E2E-02`, OPTIONS) context.assert(MODULE, '覆盖保留同一条记录和人工箱数', rescanResponse, rescan?.created === false && asId(rescan?.itemId) === asId(firstScan.itemId) && Number(rescan?.confirmedPackageCount) === originalPackageCount && closeTo(rescan?.lengthCm, replacement.length), `sameItem=${asId(rescan?.itemId) === asId(firstScan.itemId)}, confirmedPackageCount=${rescan?.confirmedPackageCount ?? 'missing'}`, OPTIONS) let measuredResponse = await context.request('GET', measurementPath) let measured = context.apiSuccess(MODULE, '回查覆盖后的尺寸与体积结果', measuredResponse, '', OPTIONS) let measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId)) const weightedOuter = { length: Number(measuredItem?.calculatedOuterLengthCm ?? measuredItem?.outerLengthCm), width: Number(measuredItem?.calculatedOuterWidthCm ?? measuredItem?.outerWidthCm), height: Number(measuredItem?.calculatedOuterHeightCm ?? measuredItem?.outerHeightCm), } const calculatedVolume = weightedOuter.length * weightedOuter.width * weightedOuter.height / 1_000_000 const usesMatchedAddRule = String(measuredItem?.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_') const unitVolume = usesMatchedAddRule ? halfUp(calculatedVolume, 3) : ceiling(calculatedVolume, 4) const totalVolume = unitVolume * originalPackageCount const volumeWeight = ceiling(totalVolume * 167, 3) // Device scans retain the measured girth while the weighted dimensions are // used for volume. The subsequent manual draft explicitly keeps ADD so the // same customer dimension rule remains frozen for review. const girth = ceiling(replacement.length + 2 * (replacement.width + replacement.height), 3) context.assert(MODULE, '覆盖尺寸、箱数和服务端体积公式正确', measuredResponse, measuredItem && closeTo(measuredItem.lengthCm, replacement.length) && closeTo(measuredItem.widthCm, replacement.width) && closeTo(measuredItem.heightCm, replacement.height) && measuredItem.scannedShipmentId === replacementShipmentId && Number(measuredItem.confirmedPackageCount) === originalPackageCount && closeTo(measuredItem.calculatedOuterLengthCm, weightedOuter.length) && closeTo(measuredItem.calculatedOuterWidthCm, weightedOuter.width) && closeTo(measuredItem.calculatedOuterHeightCm, weightedOuter.height) && (measuredItem.coefficientMode == null || ['ACTUAL', 'ADD', 'MULTIPLY'].includes(measuredItem.coefficientMode)) && Boolean(measuredItem.calculationRuleVersion) && closeTo(measuredItem.unitVolumeCbm, unitVolume) && closeTo(measuredItem.totalVolumeCbm, totalVolume) && closeTo(measuredItem.volumeWeightKg, volumeWeight) && closeTo(measuredItem.girthCm, girth) && closeTo(measured.actualVolumeCbm, totalVolume) && closeTo(measured.volumeWeightKg, volumeWeight), `weightedOuter=${weightedOuter.length}x${weightedOuter.width}x${weightedOuter.height}, unitVolume=${unitVolume}, totalVolume=${totalVolume}, volumeWeight=${volumeWeight}, girth=${girth}`, OPTIONS) const incrementalDetailCount = Number(context.context?.phase3LoadingIncrementalDetailCount || 1) if (incrementalDetailCount > 1) { const packagesPerDetail = Number(context.context?.phase3LoadingIncrementalPackageCount || 1) const configuredPackageCounts = context.context?.waitSplitVariant?.packageCounts const detailPackageCounts = Array.isArray(configuredPackageCounts) && configuredPackageCounts.length === incrementalDetailCount ? configuredPackageCounts.map(Number) : Array.from({ length: incrementalDetailCount }, () => packagesPerDetail) const expectedPackages = detailPackageCounts.reduce((sum, count) => sum + count, 0) context.assert(MODULE, '动态增量多明细箱数前置与冻结总数一致', measuredResponse, expectedPackages === originalPackageCount, `detailCount=${incrementalDetailCount}, packageCounts=${detailPackageCounts.join('+')}, frozen=${originalPackageCount}`, OPTIONS) if (expectedPackages === originalPackageCount) { const firstDetailPackageCount = detailPackageCounts[0] const resetFirstCountResponse = await updateConfirmedPackageCount(context, measurementPath, firstScan.itemId, { measurementVersion: Number(measured?.version), confirmedPackageCount: firstDetailPackageCount }) const resetFirstCount = context.apiSuccess(MODULE, '增量 fixture 将首条货物确认箱数恢复为自身箱数', resetFirstCountResponse, `itemId=${asId(firstScan.itemId)}, confirmedPackageCount=${firstDetailPackageCount}`, OPTIONS) context.assert(MODULE, '增量首条货物箱数和版本更新正确', resetFirstCountResponse, Number(resetFirstCount?.confirmedPackageCount) === firstDetailPackageCount && Number(resetFirstCount?.measurementVersion) === Number(measured?.version) + 1, `version=${resetFirstCount?.measurementVersion ?? 'missing'}`, OPTIONS) for (let detailIndex = 1; detailIndex < incrementalDetailCount; detailIndex += 1) { const detailPackageCount = detailPackageCounts[detailIndex] const cartonIndex = detailPackageCounts.slice(0, detailIndex).reduce((sum, count) => sum + count, 0) + 1 const detailBoxCode = `${inboundCode}#${originalPackageCount}-${cartonIndex}` const scanResponse = await multipartRequest(context, scanPath, { boxCode: detailBoxCode, ...replacement, scannedShipmentId: `FBA-${context.marker.slice(-12)}-${String(detailIndex + 2).padStart(2, '0')}`, machineNo: `MJ-ADMIN-E2E-INC-${detailIndex + 1}`, measuredAt: new Date().toISOString().slice(0, 19), }) const scanned = context.apiSuccess(MODULE, `增量 fixture 扫描第${detailIndex + 1}条独立货物箱码`, scanResponse, `boxCode=${detailBoxCode}`, OPTIONS) context.assert(MODULE, `增量第${detailIndex + 1}条扫描解析独立货物`, scanResponse, scanned?.created === true && asId(scanned?.itemId) && asId(scanned?.itemId) !== asId(firstScan.itemId), `itemId=${asId(scanned?.itemId) || 'missing'}, inboundDetailId=${asId(scanned?.inboundDetailId) || 'pending-query'}`, OPTIONS) if (!scanned?.itemId) continue const countDetailResponse = await updateConfirmedPackageCount(context, measurementPath, scanned.itemId, { measurementVersion: Number(scanned.measurementVersion), confirmedPackageCount: detailPackageCount }) const countDetail = context.apiSuccess(MODULE, `增量第${detailIndex + 1}条货物确认自身箱数`, countDetailResponse, `confirmedPackageCount=${detailPackageCount}`, OPTIONS) context.assert(MODULE, `增量第${detailIndex + 1}条货物箱数更新正确`, countDetailResponse, Number(countDetail?.confirmedPackageCount) === detailPackageCount, `measurementVersion=${countDetail?.measurementVersion ?? 'missing'}`, OPTIONS) } measuredResponse = await context.request('GET', measurementPath) measured = context.apiSuccess(MODULE, '回查增量 fixture 动态测量明细', measuredResponse, '', OPTIONS) measuredItem = measured?.items?.find((item) => asId(item.id) === asId(firstScan.itemId)) const detailIds = new Set((measured?.items || []).map((item) => asId(item?.inboundDetailId)).filter(Boolean)) const mixedDropship = context.createdResources?.orders?.fulfillmentType === 'ONE_CLICK_DROPSHIP' context.assert(MODULE, mixedDropship ? '混箱一件代发复用物理箱测量明细' : '增量 fixture 形成动态真实独立货物测量明细', measuredResponse, mixedDropship ? detailIds.size === 1 : detailIds.size >= incrementalDetailCount && measured.items.filter((item) => detailIds.has(asId(item?.inboundDetailId))).length >= incrementalDetailCount, `measurementItems=${measured?.items?.length ?? 'missing'}, distinctInboundDetails=${detailIds.size}`, OPTIONS) } } let inboundDetailId = measuredItem?.inboundDetailId if (!inboundDetailId) { const orderId = asId(frozenSummary?.orderId || context.createdResources?.orders?.orderId) const orderDetailResponse = await context.request('GET', `/api/platform/v1/orders/first-leg/${encodeURIComponent(orderId)}?${query(tenantId)}`) const orderDetail = context.apiSuccess(MODULE, '测量详情缺少货物ID时回查隔离订单', orderDetailResponse, `orderId=${orderId}`, OPTIONS) const cargoes = Array.isArray(orderDetail?.cargoes) ? [...orderDetail.cargoes] : [] cargoes.sort((left, right) => Number(left?.itemNo ?? Number.MAX_SAFE_INTEGER) - Number(right?.itemNo ?? Number.MAX_SAFE_INTEGER) || asId(left?.id).localeCompare(asId(right?.id))) const firstCargo = cargoes[0] const cargoBelongsToRun = asId(orderDetail?.order?.id) === orderId && String(firstCargo?.cargoRemark || '').includes(context.marker) inboundDetailId = cargoBelongsToRun ? firstCargo?.id : null context.assert(MODULE, '草稿绑定本轮箱码对应的隔离货物', orderDetailResponse, cargoBelongsToRun && asId(inboundDetailId), `cargoCount=${cargoes.length}, markerMatched=${cargoBelongsToRun}, inboundDetailId=${asId(inboundDetailId) || 'missing'}`, OPTIONS) } if (!inboundDetailId || !measuredItem?.intelligentImageFileId) { context.assert(MODULE, '草稿保存前置字段完整', measuredResponse, false, `inboundDetailId=${asId(inboundDetailId) || 'missing'}, imageFileId=${asId(measuredItem?.intelligentImageFileId) || 'missing'}`, OPTIONS) finishScenario(context, start) return } const draftItems = incrementalDetailCount > 1 ? (measured?.items || []).filter((item) => asId(item?.inboundDetailId) && asId(item?.intelligentImageFileId)) .slice(0, incrementalDetailCount).map((item) => ({ inboundDetailId: item.inboundDetailId, itemNo: item.itemNo, boxSpecification: item.boxSpecification, packageCount: Number(item.confirmedPackageCount), lengthCm: Number(item.lengthCm), widthCm: Number(item.widthCm), heightCm: Number(item.heightCm), intelligentImageFileId: item.intelligentImageFileId, overLimitWarning: item.overLimitWarning ?? null, outerLengthCm: item.outerLengthCm ?? null, outerWidthCm: item.outerWidthCm ?? null, outerHeightCm: item.outerHeightCm ?? null, coefficientMode: String(item.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_') ? 'ADD' : item.coefficientMode ?? null, remark: context.marker, })) : [{ inboundDetailId, itemNo: measuredItem.itemNo, boxSpecification: measuredItem.boxSpecification, packageCount: originalPackageCount, lengthCm: replacement.length, widthCm: replacement.width, heightCm: replacement.height, intelligentImageFileId: measuredItem.intelligentImageFileId, overLimitWarning: measuredItem.overLimitWarning ?? null, outerLengthCm: measuredItem.outerLengthCm ?? null, outerWidthCm: measuredItem.outerWidthCm ?? null, outerHeightCm: measuredItem.outerHeightCm ?? null, coefficientMode: String(measuredItem.calculationRuleVersion || '').startsWith('CUSTOMER_DIMENSION_') ? 'ADD' : measuredItem.coefficientMode ?? null, remark: context.marker, }] const palletCount = Math.max(1, Math.min(originalPackageCount, Number(context.context?.waitSplitVariant?.palletCount || 1))) const draftPayload = { measurementId: measured.measurementId, version: Number(measured.version), actualGoodsName: `墨家测量验收-${context.marker.slice(-12)}`, expectedPalletCount: context.createdResources?.orders?.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? palletCount : null, items: draftItems, } const draftResponse = await context.request('PUT', `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/draft?${query(tenantId)}`, draftPayload) const draft = context.apiSuccess(MODULE, '保存同一测量批次草稿', draftResponse, `measurementId=${asId(measured.measurementId)}, items=${draftItems.length}`, OPTIONS) context.assert(MODULE, '草稿沿用批次并递增版本', draftResponse, asId(draft?.measurementId) === asId(measured.measurementId) && Number(draft?.version) === Number(measured.version) + 1 && draft?.status === 'DRAFT', `status=${draft?.status ?? 'missing'}, version=${draft?.version ?? 'missing'}`, OPTIONS) if (!draft?.measurementId) { finishScenario(context, start) return } const warehouseResourcesReady = await provisionWarehouseResources(context, { inboundId, tenantId, warehouseId: asId(summary?.warehouseId), palletCount, }) context.assert(MODULE, '测量提交前按称重图片数准备真实库位', draftResponse, Boolean(warehouseResourcesReady), `warehouseId=${asId(summary?.warehouseId) || 'missing'}, palletCount=${palletCount}`, OPTIONS) if (!warehouseResourcesReady) { finishScenario(context, start) return } const weightEvidence = await createConfirmedWeightEvidence(context, { inboundId, tenantId, palletIds: warehouseResourcesReady.palletIds, grossWeightPerPalletKg: Number(context.context?.waitSplitVariant?.grossWeightPerPalletKg || 100), }) context.assert(MODULE, '测量提交前已通过正式接口确认称重图片', draftResponse, Boolean(weightEvidence), `confirmedImages=${weightEvidence?.count ?? 0}, expected=${palletCount}`, OPTIONS) if (!weightEvidence) { finishScenario(context, start) return } if (context.options?.verifyPalletLocationGuide) { const guideResponse = await context.request('GET', measurementPath) const guideWorkspace = context.apiSuccess(MODULE, '提交前读取整票库位块与称重图片槽绑定', guideResponse, `inboundId=${inboundId}, processStatus=${guideResponse.body?.data?.processStatus ?? 'missing'}`, OPTIONS) const beforeDelete = verifyPalletLocationGuide(context, guideResponse, guideWorkspace, weightEvidence.confirmedResults.map((item) => item.id), '删除前') if (!beforeDelete.identityComplete || !beforeDelete.bindingsStable) { context.createdResources.inboundMeasurementLocationGuide = { marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId), preSubmitGuideAvailable: false, processStatus: guideWorkspace?.processStatus || null, } finishScenario(context, start) return } const overflowSlots = beforeDelete.slots.filter((slot) => slot?.sharedOverflow === true) context.addStep(MODULE, '正式数据 A0 共享溢出观测', guideResponse, true, overflowSlots.length > 0 ? `sharedOverflow=true, slots=${overflowSlots.map((slot) => `${slot.slotNo}:${slot.reservationId}:${slot.locationCode}`).join('|')}` : '本票未触发 A0;为保护共享测试仓,不批量停用普通库位制造容量不足', { scenario: SCENARIO }) const deletedWeight = weightEvidence.confirmedResults[0] const previousSlot = beforeDelete.slots.find((slot) => asId(slot?.boundWeightResultId) === deletedWeight.id) const deleteResponse = await context.request('DELETE', `/api/platform/v1/first-leg/inbounds/${inboundId}/weight-results/${deletedWeight.id}?${query(tenantId)}&version=${deletedWeight.version}`) const deleteSucceeded = deleteResponse.http === 200 && String(deleteResponse.body?.code) === '200' context.assert(MODULE, '可编辑状态删除一张已确认称重图片', deleteResponse, deleteSucceeded, `HTTP=${deleteResponse.http}, code=${deleteResponse.body?.code ?? 'missing'}, message=${deleteResponse.body?.message ?? 'missing'}, requestId=${deleteResponse.requestId || 'missing'}, traceId=${deleteResponse.traceId || 'missing'}`, OPTIONS) if (!deleteSucceeded) { context.createdResources.inboundMeasurementLocationGuide = { marker: context.marker, inboundId, tenantId, blockId: asId(beforeDelete.guide?.blockId), deletedWeightResultId: deletedWeight.id, deleteSucceeded: false, deleteCode: deleteResponse.body?.code || null, deleteMessage: deleteResponse.body?.message || null, deleteRequestId: deleteResponse.requestId || null, deleteTraceId: deleteResponse.traceId || null, sharedOverflowObserved: overflowSlots.length > 0, } finishScenario(context, start) return } const afterDeleteResponse = await context.request('GET', measurementPath) const afterDeleteWorkspace = context.apiSuccess(MODULE, '删除后回查整票库位块', afterDeleteResponse, `deletedWeightResultId=${deletedWeight.id}`, OPTIONS) const afterDelete = verifyPalletLocationGuide(context, afterDeleteResponse, afterDeleteWorkspace, weightEvidence.confirmedResults.slice(1).map((item) => item.id), '删除后') const retainedSlot = afterDelete.slots.find((slot) => asId(slot?.reservationId) === asId(previousSlot?.reservationId) && asId(slot?.locationId) === asId(previousSlot?.locationId)) const retainedEmpty = Boolean(previousSlot) && retainedSlot?.slotStatus === 'RESERVED_EMPTY' && !asId(retainedSlot?.boundWeightResultId) && Number(afterDelete.guide?.reservedSlotCount) === Number(beforeDelete.guide?.reservedSlotCount) context.assert(MODULE, '删除只解绑图片且原 reservation/location 变为 RESERVED_EMPTY', afterDeleteResponse, retainedEmpty, `reservationId=${asId(retainedSlot?.reservationId) || 'missing'}, locationId=${asId(retainedSlot?.locationId) || 'missing'}, slotStatus=${retainedSlot?.slotStatus || 'missing'}`, OPTIONS) if (!retainedEmpty) { finishScenario(context, start) return } const replacementWeight = await recreateConfirmedWeightEvidence(context, { inboundId, tenantId, palletId: deletedWeight.palletId, grossWeightKg: deletedWeight.grossWeightKg, }) if (!replacementWeight) { finishScenario(context, start) return } const reboundResponse = await context.request('GET', measurementPath) const reboundWorkspace = context.apiSuccess(MODULE, '替代图片确认后回查空槽复用', reboundResponse, `replacementWeightResultId=${replacementWeight.id}`, OPTIONS) const activeResults = [...weightEvidence.confirmedResults.slice(1), replacementWeight] const rebound = verifyPalletLocationGuide(context, reboundResponse, reboundWorkspace, activeResults.map((item) => item.id), '重绑后') const reboundSlot = rebound.slots.find((slot) => asId(slot?.boundWeightResultId) === replacementWeight.id) const reusedSlot = reboundSlot?.slotStatus === 'BOUND' && asId(reboundSlot?.reservationId) === asId(previousSlot?.reservationId) && asId(reboundSlot?.locationId) === asId(previousSlot?.locationId) && Number(rebound.guide?.reservedEmptySlotCount) === Number(beforeDelete.guide?.reservedEmptySlotCount) context.assert(MODULE, '替代图片按新 resultId 复用原空槽', reboundResponse, reusedSlot, `oldResultId=${deletedWeight.id}, newResultId=${replacementWeight.id}, reservationId=${asId(reboundSlot?.reservationId) || 'missing'}, locationId=${asId(reboundSlot?.locationId) || 'missing'}, slotStatus=${reboundSlot?.slotStatus || 'missing'}`, OPTIONS) if (!reusedSlot) { finishScenario(context, start) return } weightEvidence.confirmedResults = activeResults weightEvidence.grossWeightKg = activeResults.reduce((sum, item) => sum + item.grossWeightKg, 0) weightEvidence.netWeightKg = activeResults.reduce((sum, item) => sum + item.netWeightKg, 0) context.createdResources.inboundMeasurementLocationGuide = { marker: context.marker, inboundId, tenantId, blockId: asId(rebound.guide?.blockId), deletedWeightResultId: deletedWeight.id, replacementWeightResultId: replacementWeight.id, deleteSucceeded: true, slotReused: true, reservationId: asId(reboundSlot?.reservationId), locationId: asId(reboundSlot?.locationId), sharedOverflowObserved: overflowSlots.length > 0, } } const submitResponse = await context.request('POST', `/api/platform/v1/first-leg/inbounds/${inboundId}/measurement/submit?${query(tenantId)}`, { measurementId: draft.measurementId, version: Number(draft.version) }) const submitted = context.apiSuccess(MODULE, '提交测量审核', submitResponse, `measurementId=${asId(draft.measurementId)}`, OPTIONS) context.assert(MODULE, '提交后进入待测量审核状态', submitResponse, submitted?.processStatus === 'MEASURE_PENDING' && submitted?.measureReviewStatus === 'PENDING', `processStatus=${submitted?.processStatus ?? 'missing'}, reviewStatus=${submitted?.measureReviewStatus ?? 'missing'}`, OPTIONS) const rejectedScanResponse = await multipartRequest(context, scanPath, { boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-03', measuredAt: new Date().toISOString().slice(0, 19), }) const rejected = rejectedScanResponse.http === 200 && String(rejectedScanResponse.body?.code) === 'A0409' && String(rejectedScanResponse.body?.message || '').includes('测量已提交') context.assert(MODULE, '测量提交后拒绝设备覆盖并返回中文提示', rejectedScanResponse, rejected, `HTTP=${rejectedScanResponse.http}, code=${rejectedScanResponse.body?.code ?? 'missing'}, chineseMessage=${String(rejectedScanResponse.body?.message || '').includes('测量已提交')}`, OPTIONS) const pendingInboundResponse = await context.request('GET', detailPath) const pendingInbound = context.apiSuccess(MODULE, '审核前读取最新入库单版本', pendingInboundResponse, '', OPTIONS) const pendingSummary = pendingInbound?.summary context.assert(MODULE, '审核前状态和版本有效', pendingInboundResponse, pendingSummary?.processStatus === 'MEASURE_PENDING' && pendingSummary?.measureReviewStatus === 'PENDING' && Number.isInteger(Number(pendingSummary?.version)), `status=${pendingSummary?.processStatus ?? 'missing'}, version=${pendingSummary?.version ?? 'missing'}`, OPTIONS) const approvalPath = `/api/platform/v1/inbounds/${inboundId}/measurements/approve?${query(tenantId)}` const approvalResponse = await requestWithHeaders(context, 'POST', approvalPath, { measurementId: draft.measurementId, expectedVersion: Number(pendingSummary?.version), reason: null, }, { 'Idempotency-Key': `${context.marker}-mojia-approve`.slice(0, 128) }) context.apiSuccess(MODULE, 'Admin 审核通过墨家测量', approvalResponse, `measurementId=${asId(draft.measurementId)}`, OPTIONS) // Measurement approval may enqueue pricing/inbound completion. Poll the // public projection instead of treating the transient WAIT_PRICING state as // a smart-loading candidate or manufacturing fixture state in the script. const dropship = context.createdResources?.orders?.fulfillmentType === 'ONE_CLICK_DROPSHIP' let completed = await waitForCompletedMeasurement(context, measurementPath, dropship ? ['WAIT_PALLETIZE'] : ['WAIT_SPLIT']) let finalResponse = completed.response let finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, `pollAttempts=${completed.attempts}`, OPTIONS) context.assert(MODULE, '审核后入库与测量状态闭环且结果未变化', finalResponse, finalMeasurement?.processStatus === (dropship ? 'WAIT_PALLETIZE' : 'WAIT_SPLIT') && finalMeasurement?.measureReviewStatus === 'APPROVED' && finalMeasurement?.measurementStatus === 'APPROVED' && Number(finalMeasurement?.actualPackageCount) === originalPackageCount && closeTo(finalMeasurement?.actualGrossWeightKg, weightEvidence.grossWeightKg, 0.001) && closeTo(finalMeasurement?.actualNetWeightKg, weightEvidence.netWeightKg, 0.001) && closeTo(finalMeasurement?.actualVolumeCbm, totalVolume) && closeTo(finalMeasurement?.volumeWeightKg, volumeWeight), `processStatus=${finalMeasurement?.processStatus ?? 'missing'}, measurementStatus=${finalMeasurement?.measurementStatus ?? 'missing'}, packages=${finalMeasurement?.actualPackageCount ?? 'missing'}, pallets=${weightEvidence.count}, grossWeight=${finalMeasurement?.actualGrossWeightKg ?? 'missing'}, netWeight=${finalMeasurement?.actualNetWeightKg ?? 'missing'}`, OPTIONS) let sealedCartonMarks = [] let sealedSourcePalletNos = [] if (dropship && finalMeasurement?.processStatus === 'WAIT_PALLETIZE') { if (context.options?.prepareDropshipManualCount) { const warehouseId = asId(summary?.warehouseId) const pendingResponse = await context.request('GET', `/api/platform/v1/first-leg/pda-dropship/tasks/pending?warehouseId=${warehouseId}&keyword=${encodeURIComponent(inboundCode)}`) const pending = context.apiSuccess(MODULE, '准备PDA人工清点任务', pendingResponse, `inboundId=${inboundId}`, OPTIONS) const task = (pending || []).find((item) => asId(item?.inboundId) === inboundId) context.assert(MODULE, '任务停在未领取、未清点、未归托状态', pendingResponse, Boolean(asId(task?.taskId)) && task?.taskStatus === 'PENDING' && Number(task?.countedCartons) === 0 && Number(task?.boundCartons) === 0 && Number(task?.totalCartons) === Number(finalMeasurement?.actualPackageCount), `taskId=${asId(task?.taskId) || 'missing'}, status=${task?.taskStatus ?? 'missing'}, counted=${task?.countedCartons ?? 'missing'}`, OPTIONS) context.createdResources.dropshipManualCount = { taskId: asId(task?.taskId), inboundId, inboundCode, warehouseId, warehouseName: task?.warehouseName || null, totalCartons: Number(task?.totalCartons || 0), taskStatus: task?.taskStatus || null, } } else { const sealed = await completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId: asId(summary?.warehouseId), measurement: finalMeasurement, }) sealedCartonMarks = sealed?.taskStatus === 'COMPLETED' ? sealed.sealedCartonMarks : [] sealedSourcePalletNos = sealed?.taskStatus === 'COMPLETED' ? (sealed.sealedSourcePalletNos || []).filter(Boolean) : [] completed = await waitForCompletedMeasurement(context, measurementPath, ['WAIT_SPLIT'], 10) finalResponse = completed.response finalMeasurement = context.apiSuccess(MODULE, '合托后回查一件代发入仓单', finalResponse, `pollAttempts=${completed.attempts}`, OPTIONS) context.assert(MODULE, '一件代发合托完成后进入智能分柜门禁', finalResponse, finalMeasurement?.processStatus === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED' && finalMeasurement?.measurementStatus === 'APPROVED', `processStatus=${finalMeasurement?.processStatus ?? 'missing'}`, OPTIONS) } } const approvedScanResponse = await multipartRequest(context, scanPath, { boxCode, ...replacement, machineNo: 'MJ-ADMIN-E2E-AFTER-APPROVAL', measuredAt: new Date().toISOString().slice(0, 19), }) context.assert(MODULE, '审核及合托完成后箱码尺寸与图片快照不可再覆盖', approvedScanResponse, approvedScanResponse.http === 200 && String(approvedScanResponse.body?.code) === 'A0409', `HTTP=${approvedScanResponse.http}, code=${approvedScanResponse.body?.code ?? 'missing'}, requestId=${approvedScanResponse.requestId || 'missing'}`, OPTIONS) context.createdResources.inboundMeasurement = { sealedCartonMarks, sealedSourcePalletNos, marker: context.marker, tenantId, inboundId, orderId: asId(summary?.orderId || context.createdResources?.orders?.orderId), measurementId: asId(draft.measurementId), boxCode, finalStatus: finalMeasurement?.measurementStatus || null, processStatus: finalMeasurement?.processStatus || null, palletCount: weightEvidence.count, actualGrossWeightKg: weightEvidence.grossWeightKg, actualPackageCount: Number(finalMeasurement?.actualPackageCount || 0), destinationType: context.createdResources?.orders?.destinationType || null, destinationWarehouseId: context.createdResources?.orders?.destinationWarehouseId || null, destinationWarehouseCode: context.createdResources?.orders?.destinationWarehouseCode || null, } finishScenario(context, start) }