diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index a98160ebb..9efc81f42 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -172,7 +172,7 @@ export async function run(context) { `dropship-browser-fixture.mjs` 通过总入口 `--modules dropship-browser-fixture` 执行,自动依赖客户资质准备;复用矩阵生产者创建一票四箱两托,完成国内装柜、船节点和按柜生成海外入库计划,明确停在 `READY_FOR_BROWSER_RECEIVING`。只准备新测试数据,不调用海外 PDA 收货或出库、不创建海外操作员;后续由浏览器页面验收。报告仍保存于 `createdResources.dropshipPalletMatrix`,准备通过不代表全流程通过,不自动删除数据。 -`dropship-mixed-palletization.mjs` 通过总入口 `--modules dropship-mixed-palletization` 执行,自动依赖客户资质准备。经正式 HTTP 创建两客户三票一件代发(2/3/1 个物理箱,每箱两个 SKU),完成下单审核和测量,交替调用跨客户箱码预览、领取、清点、先箱后托批绑、移回/归回、六箱混托封托、解封和重新封托。核对客户/入仓单/SKU/数量守恒、只读识别无写入、同键重放、未知码/跨仓拒绝及逐票待分柜;结果保存在 `createdResources.dropshipMixedPalletization`。使用显式目标环境文件,开发机获授权时 Gateway 必须显式指向本机。会创建并保留客户、订单、测量与托盘数据,不自动清理;失败必须检查报告,重跑创建新批次。终点为 `MIXED_PALLET_SEALED_VERIFIED`,不代表装柜、海外履约或 Android 真机验收通过。 +`dropship-mixed-palletization.mjs` 通过总入口 `--modules dropship-mixed-palletization` 执行,自动依赖客户资质准备。经正式 HTTP 创建两客户三票一件代发(2/3/1 个物理箱,每箱两个 SKU),完成下单审核和测量,交替调用跨客户箱码预览、领取、清点、先箱后托批绑、移回/归回、六箱混托封托、解封和重新封托。核对客户/入仓单/SKU/数量守恒、只读识别无写入、同键重放、未知码/跨仓拒绝及逐票待分柜;结果保存在 `createdResources.dropshipMixedPalletization`。另外保留四票已测量、未领取未组托样本:同客户两票(2/1 箱)和跨客户两票(2/3 箱),逐票核对 Admin `WAIT_PALLETIZE` 列表可见,保存 `manualRecords` 和 `manualStage=WAIT_PALLETIZE_VISIBLE`。显式启用 `orders` 的 `orderFixtureOnly` 来源生产范围,保留审核收敛、重放与受理启动,默认订单全量验收仍覆盖申报修改;称重复用当前仓已有托盘,不要求固定编号。使用显式目标环境文件,开发机获授权时 Gateway 必须显式指向本机。会创建并保留客户、订单、测量与托盘数据,不自动清理;失败必须检查报告,重跑创建新批次。终点为 `MIXED_PALLET_SEALED_VERIFIED`,不代表装柜、海外履约或 Android 真机验收通过。离线合同为 `dropship-mixed-palletization-contract.test.mjs`。 `dropship-pallet-matrix.mjs` 验证下述四场景整托手工加入、整托移除、智能推荐、独立订舱与文件放舱、真实柜号关联、预装确认、PDA 整托拣货和五类证据上传;异步装柜完成必须等到任务 `COMPLETED` 并逐票回查 `LOADED`。智能装柜的托盘汇总与装柜详情逐票组成分别核对,不能把共托汇总行的首票编号当作全托归属。随后经正式报关、国内离仓及人工船节点接口推进至 `PICKUP_READY`,再按柜生成逐票海外入库计划,检查来源托盘完整绑定、共享托盘身份一致和同键重放。海外收货使用本轮创建并绑定接柜仓的 PDA 操作员,上传三类照片、签收、逐实物托盘收货、逐票箱数核平和完成入库,同键重放及同托新键扫码均不得重复入账。随后从本轮逐票库存候选创建一件代发专用出库,携带原始箱唛由 PDA 拣货、完成,核对终态、幂等和可用库存扣减;finally 停用本轮操作员。出库重放按业务身份和数据库毫秒时间精度核对。离线合同为 `dropship-pallet-matrix-contract.test.mjs`;脚本覆盖不等于真实 HTTP 已通过,须检查当轮报告和库存审计。 diff --git a/scripts/admin-test-env-modules/dropship-mixed-palletization-contract.test.mjs b/scripts/admin-test-env-modules/dropship-mixed-palletization-contract.test.mjs new file mode 100644 index 000000000..22b568ccc --- /dev/null +++ b/scripts/admin-test-env-modules/dropship-mixed-palletization-contract.test.mjs @@ -0,0 +1,33 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' + +const source = readFileSync(new URL('./dropship-mixed-palletization.mjs', import.meta.url), 'utf8') +const orders = readFileSync(new URL('./orders.mjs', import.meta.url), 'utf8') + +test('mixed pallet acceptance uses formal producers and fails on any failed preparation assertion', () => { + assert.match(source, /await createOrder\(context\)/) + assert.match(source, /await measure\(context\)/) + assert.match(source, /!context.steps.slice\(start\).some\(\(step\) => !step.passed\)/) + assert.match(source, /if \(!passed\) throw new Error/) + assert.match(source, /context.context.allowAnyExistingPallet = true/) +}) + +test('retained manual fixtures cover same and different customers and remain visible and unclaimed', () => { + assert.match(source, /\[2, 1, 2, 3\].entries\(\)/) + assert.match(source, /index < 3 \? savedContext.merchantQualificationFixture : secondCustomer/) + assert.match(source, /index < 2 \? 'SAME_CUSTOMER' : 'CROSS_CUSTOMER'/) + assert.match(source, /mainStatus=WAIT_PALLETIZE&inboundCode=/) + assert.match(source, /task.taskStatus === 'PENDING' && task.countedCartons === 0 && task.boundCartons === 0/) + assert.match(source, /result.manualStage = 'WAIT_PALLETIZE_VISIBLE'/) +}) + +test('explicit order producer scope preserves approval convergence and default declaration acceptance', () => { + const branch = orders.indexOf('if (context.context.orderFixtureOnly === true)') + assert.ok(branch > orders.indexOf('const inboundResult = await waitForInbound')) + assert.ok(branch > orders.indexOf('await verifyOrderApprovalReplay(context')) + assert.ok(branch < orders.indexOf('const editableDetailResponse')) + assert.match(orders.slice(branch, orders.indexOf('const editableDetailResponse')), /await finishOrderLifecycle\(context/) + assert.match(orders, /申报修改保持订单号入仓码和渠道且版本递增/) + assert.match(orders, /申报修改异步同步到Firstleg入库事实/) +}) diff --git a/scripts/admin-test-env-modules/dropship-mixed-palletization.mjs b/scripts/admin-test-env-modules/dropship-mixed-palletization.mjs index db9f86a2e..a732f79de 100644 --- a/scripts/admin-test-env-modules/dropship-mixed-palletization.mjs +++ b/scripts/admin-test-env-modules/dropship-mixed-palletization.mjs @@ -12,6 +12,7 @@ export async function run(context) { const savedContext = { ...context.context } const originalManualCount = context.options.prepareDropshipManualCount const records = [] + let secondCustomer = null const result = { marker: originalMarker, stage: 'PREPARING', records, fullFlowVerified: false } context.createdResources.dropshipMixedPalletization = result function check(name, response, passed, details = '') { @@ -32,11 +33,13 @@ export async function run(context) { return command(`${record.label}回查任务`, 'GET', `${BASE}/tasks/${record.source.taskId}?warehouseId=${record.source.warehouseId}`) } try { + context.context.orderFixtureOnly = true + context.context.allowAnyExistingPallet = true check('存在本轮已审核客户', null, Boolean(context.context.merchantQualificationFixture?.tenantId)) for (const [index, count] of [2, 3, 1].entries()) { const label = ['A-2BOX', 'B-3BOX', 'B-1BOX'][index] context.marker = `${originalMarker}-${label}` - if (index === 1) await createCustomer(context, { publishEnvironment: false }) + if (index === 1) { await createCustomer(context, { publishEnvironment: false }); secondCustomer = context.context.merchantQualificationFixture } for (const key of ['orders', 'inboundMeasurement', 'dropshipManualCount']) delete context.createdResources[key] for (const key of ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']) delete context.context[key] context.context.orderDestinationType = 'OVERSEAS' @@ -127,6 +130,33 @@ export async function run(context) { result.pallet = sealed.pallet; result.members = sealed.members; result.counted = counted result.stage = 'MIXED_PALLET_SEALED_VERIFIED' for (const record of records) record.task = await readTask(record) + // 保留同客户两票和跨客户两票独立样本,均停在未领取、未组托状态。 + result.manualRecords = [] + for (const [index, count] of [2, 1, 2, 3].entries()) { + context.marker = `${originalMarker}-MANUAL-${index + 1}` + context.context.merchantQualificationFixture = index < 3 ? savedContext.merchantQualificationFixture : secondCustomer + for (const key of ['orders', 'inboundMeasurement', 'dropshipManualCount']) delete context.createdResources[key] + for (const key of ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']) delete context.context[key] + context.context.orderDestinationType = 'OVERSEAS' + context.context.orderFulfillmentType = 'ONE_CLICK_DROPSHIP' + context.context.waitSplitVariant = { skuCount: 2, packageCountPerSku: count, packageCounts: [count, count], + totalPackageCount: count, palletCount: 1, skuCodes: [`MANUAL-COMMON-${originalMarker}`.slice(-60), `MANUAL-SECOND-${originalMarker}`.slice(-60)] } + context.options.prepareDropshipManualCount = true + const start = context.steps.length + await createOrder(context) + await measure(context) + const source = context.createdResources.dropshipManualCount + check('人工打托样本正式下单测量通过', null, Boolean(source?.taskId) && !context.steps.slice(start).some((step) => !step.passed)) + const order = { ...context.createdResources.orders } + const list = await command('回查一件代发打托列表可见', 'GET', `/api/platform/v1/orders?mainStatus=WAIT_PALLETIZE&inboundCode=${encodeURIComponent(source.inboundCode)}&page=1&size=20`) + const row = list.records?.find((item) => String(item.orderId) === order.orderId) + check('人工样本出现在用户的一件代发打托页签', null, Boolean(row) && row.mainStatus === 'WAIT_PALLETIZE') + const task = await readTask({ label: `MANUAL-${index + 1}`, source }) + check('人工样本未领取未清点未归托', null, task.taskStatus === 'PENDING' && task.countedCartons === 0 && task.boundCartons === 0) + result.manualRecords.push({ label: `MANUAL-${index + 1}`, group: index < 2 ? 'SAME_CUSTOMER' : 'CROSS_CUSTOMER', + order, source: { ...source }, measurement: { ...context.createdResources.inboundMeasurement } }) + } + result.manualStage = 'WAIT_PALLETIZE_VISIBLE' } finally { context.marker = originalMarker; context.context = savedContext context.options.prepareDropshipManualCount = originalManualCount diff --git a/scripts/admin-test-env-modules/orders-contract.test.mjs b/scripts/admin-test-env-modules/orders-contract.test.mjs index 00f019527..597b210df 100644 --- a/scripts/admin-test-env-modules/orders-contract.test.mjs +++ b/scripts/admin-test-env-modules/orders-contract.test.mjs @@ -57,7 +57,7 @@ for (const [status, history] of [['PROCESSING', ['APPROVED', 'ACCEPTED', 'PROCES test('approved stop runs after full approval replay, inbound readiness and shared measurement handoff', () => { const source = readFileSync(new URL('./orders.mjs', import.meta.url), 'utf8') const run = source.slice(source.indexOf('export async function run(')) - const finish = run.indexOf('await finishOrderLifecycle(') + const finish = run.lastIndexOf('await finishOrderLifecycle(') for (const fragment of ['await verifyOrderApprovalReplay(', '隔离入库单 marker 来源与可测量状态正确', 'context.createdResources.orders.inboundId = inboundId', 'context.context.createdInbound =', '履约任务与本轮订单和客户一致']) { assert.ok(run.indexOf(fragment) >= 0 && run.indexOf(fragment) < finish, fragment) diff --git a/scripts/admin-test-env-modules/orders.mjs b/scripts/admin-test-env-modules/orders.mjs index a727e8dd2..06f97550a 100644 --- a/scripts/admin-test-env-modules/orders.mjs +++ b/scripts/admin-test-env-modules/orders.mjs @@ -1110,6 +1110,25 @@ export async function run(context) { await verifyOrderApprovalReplay(context, { tenantId, orderId, inboundId, inboundCode: id(inboundResult.detail?.summary?.inboundCode) }) + // 专项造单仅生产已审核的真实入库来源;默认全量验收仍继续验证申报修改。 + if (context.context.orderFixtureOnly === true) { + const inbound = inboundResult.detail + Object.assign(context.createdResources.orders, { + inboundId, inboundCode: inbound.summary.inboundCode, + customerRemark: inbound.publicRemark, + internalRemark: `${context.marker} Admin 订单链路验收`, + cargoRemark: payload.cargoes?.[0]?.cargoRemark || null, + processStatus: inbound.summary.processStatus, taskId: id(inboundResult.task.id), + fixtureOnly: true, + }) + const source = { tenantId, inboundId, marker: context.marker } + context.createdResources.ocrInbound = source + context.createdResources.testInbound = source + for (const key of ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound']) context.context[key] = source + await finishOrderLifecycle(context, { tenantId, orderId, firstLegDetailPath }) + return + } + const editableDetailResponse = await context.request('GET', firstLegDetailPath) const editableDetail = context.apiSuccess(MODULE, 'Admin 读取审核后客户申报资料', editableDetailResponse) const declarationDetail = editableDetail?.order?.order ? editableDetail.order : editableDetail