diff --git a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs index d89ddff9c..bf84af1f1 100644 --- a/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement-box-code-contract.test.mjs @@ -61,7 +61,7 @@ test('demo package keeps pallet pages bounded and historical snapshots read-only }) test('approval freezes measurement and requires quantity weight and volume conservation', () => { - assert.match(source, /审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖/) + assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/) assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/) assert.match(reviewTests, /shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch/) assert.match(reviewTests, /shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve/) @@ -71,3 +71,13 @@ test('approval freezes measurement and requires quantity weight and volume conse assert.match(source, /actualVolumeCbm[^\n]+totalVolume/) assert.match(source, /volumeWeightKg[^\n]+volumeWeight/) }) + +test('dropship measurement completes the formal palletization workflow before smart loading', () => { + assert.match(source, /pda-dropship\/tasks\/pending/) + assert.match(source, /pda-dropship\/tasks\/\$\{taskId\}\/claim/) + assert.match(source, /pallets\/open/) + assert.match(source, /pallets\/\$\{palletId\}\/scan/) + assert.match(source, /pallets\/\$\{palletId\}\/seal/) + assert.match(source, /dropship \? \['WAIT_PALLETIZE'\] : \['WAIT_SPLIT'\]/) + assert.match(source, /一件代发合托完成后进入智能分柜门禁/) +}) diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index 856c8594e..b7e2caed7 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -71,19 +71,85 @@ function randomlyReusePalletIds(pallets, palletCount, randomValue) { pallets[(seed >>> (index % 31)) % pallets.length].id) } -async function waitForCompletedMeasurement(context, measurementPath, attempts = 30) { +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 (measurement?.processStatus === 'WAIT_SPLIT') return { response, measurement, attempts: attempt } + 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 } } +async function completeDropshipPalletization(context, { inboundId, inboundCode, warehouseId, measurement }) { + const pendingResponse = await context.request('GET', + `/api/platform/v1/first-leg/pda-dropship/tasks/pending?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 openResponse = await context.request('POST', + `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/open`, { + warehouseId, operationId: `${context.marker}-dropship-open`.slice(0, 128), + }) + let pallet = context.apiSuccess(MODULE, '开启一件代发工作托盘', openResponse, + `taskId=${taskId}`, OPTIONS) + const palletId = asId(pallet?.palletId) + if (!palletId) return null + + const totalCartons = Number(measurement?.actualPackageCount) + let scannedCartons = 0 + for (let index = 0; index < totalCartons; index += 1) { + const boxCode = `${inboundCode}#${totalCartons}-${index + 1}` + const scanResponse = await context.request('POST', + `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/${palletId}/scan`, { + warehouseId, boxCode, cartonCount: 1, + operationId: `${context.marker}-dropship-scan-${index + 1}`.slice(0, 128), + }) + const scanned = context.apiSuccess(MODULE, `合托扫描第${index + 1}箱冻结箱唛`, scanResponse, + `boxCode=${boxCode}`, OPTIONS) + context.assert(MODULE, `第${index + 1}箱进入工作托盘`, scanResponse, + Number(scanned?.addedCartons) === 1 && asId(scanned?.pallet?.palletId) === palletId, + `added=${scanned?.addedCartons ?? 'missing'}, remaining=${scanned?.remainingCartons ?? 'missing'}`, OPTIONS) + pallet = scanned?.pallet || pallet + if (Number(scanned?.addedCartons) === 1) scannedCartons += 1 + } + context.assert(MODULE, '一件代发工作托盘覆盖测量冻结的全部箱数', null, + totalCartons > 0 && scannedCartons === totalCartons, + `scanned=${scannedCartons}, expected=${totalCartons || 'missing'}`, OPTIONS) + + const sealResponse = await context.request('POST', + `/api/platform/v1/first-leg/pda-dropship/tasks/${taskId}/pallets/${palletId}/seal`, { + warehouseId, expectedVersion: Number(pallet?.version), + operationId: `${context.marker}-dropship-seal`.slice(0, 128), + }) + const sealed = context.apiSuccess(MODULE, '封托并完成一件代发合托任务', sealResponse, + `taskId=${taskId}, palletId=${palletId}`, OPTIONS) + context.assert(MODULE, '全部箱数封托后入仓单允许进入WAIT_SPLIT', sealResponse, + sealed?.taskStatus === 'COMPLETED' + && Number(sealed?.allocatedCartons) === Number(sealed?.totalCartons) + && sealed?.pallets?.every((item) => item?.palletStatus === 'COMPLETED'), + `taskStatus=${sealed?.taskStatus ?? 'missing'}, allocated=${sealed?.allocatedCartons ?? 'missing'}`, OPTIONS) + return sealed +} + async function provisionWarehouseResources(context, { inboundId, tenantId, warehouseId, palletCount }) { if (!warehouseId || !Number.isInteger(palletCount) || palletCount < 1) return null @@ -622,12 +688,15 @@ export async function run(context) { // 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 completed = await waitForCompletedMeasurement(context, measurementPath) - const finalResponse = completed.response - const finalMeasurement = context.apiSuccess(MODULE, '查询审核后的最终测量状态', finalResponse, + 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 === 'WAIT_SPLIT' && finalMeasurement?.measureReviewStatus === 'APPROVED' + finalMeasurement?.processStatus === (dropship ? 'WAIT_PALLETIZE' : 'WAIT_SPLIT') + && finalMeasurement?.measureReviewStatus === 'APPROVED' && finalMeasurement?.measurementStatus === 'APPROVED' && Number(finalMeasurement?.actualPackageCount) === originalPackageCount && closeTo(finalMeasurement?.actualGrossWeightKg, weightEvidence.grossWeightKg, 0.001) @@ -636,11 +705,26 @@ export async function run(context) { && 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) + if (dropship && finalMeasurement?.processStatus === 'WAIT_PALLETIZE') { + await completeDropshipPalletization(context, { + inboundId, inboundCode, warehouseId: asId(summary?.warehouseId), measurement: finalMeasurement, + }) + 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, '审核进入WAIT_SPLIT后箱码尺寸与图片快照不可再覆盖', approvedScanResponse, + 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)