diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index 69439a022..89b7e826b 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -1243,7 +1243,7 @@ export class AcceptanceRun { const createdKeys = ['orders', 'ocrInbound', 'testInbound', 'inboundMeasurement'] const contextKeys = ['inboundWeightOcr', 'ocrInbound', 'testInbound', 'createdInbound', 'phase3LoadingRequireSharedOverflow', 'phase3LoadingIncrementalPackageCount', - 'phase3LoadingIncrementalDetailCount', 'orderProfile'] + 'phase3LoadingIncrementalDetailCount', 'orderProfile', 'measurementPalletOverride'] const createdSnapshot = new Map(createdKeys.map((key) => [key, { present: Object.hasOwn(this.createdResources, key), value: this.createdResources[key], }])) @@ -1268,8 +1268,11 @@ export class AcceptanceRun { const isolation = requireSharedOverflow ? await this.disableAvailableLocationsForA0(warehouseId, requiredPalletCount) : { forced: false, disabled: [] } - this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true + this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow try { + if (requireSharedOverflow) { + this.context.measurementPalletOverride = await this.createPhase3FreshMeasurementPallet(warehouseId) + } const measurement = await import(new URL('./admin-test-env-modules/inbound-measurement.mjs', import.meta.url)) await this.executeModule('inbound-measurement-phase3-loading-fixture', () => measurement.run(this)) } finally { @@ -1297,6 +1300,7 @@ export class AcceptanceRun { const incrementalMarker = `${originalMarker}-LOAD${suffix}-INC` this.marker = incrementalMarker this.context.phase3LoadingRequireSharedOverflow = false + delete this.context.measurementPalletOverride this.context.phase3LoadingIncrementalPackageCount = 2 this.context.phase3LoadingIncrementalDetailCount = 4 this.context.orderProfile = { @@ -1462,6 +1466,37 @@ export class AcceptanceRun { { scenario: 'phase3-phase4-producer-fixture', deploymentBlocker: true }) } + async createPhase3FreshMeasurementPallet(warehouseId) { + const module = 'phase3-loading-fixture' + const options = { scenario: 'phase3-loading-a0-fixture', deploymentBlocker: true } + const payload = { + warehouseId, palletType: 'STANDARD', capacityCbm: 1.5, weightKg: 25, + color: 'ORANGE', remark: `${this.marker}-inventory-snapshot-a0`, + } + const createResponse = await this.request('POST', '/api/platform/v1/pallets?tenantId=0', payload) + const palletId = String(this.apiSuccess(module, '创建本轮库存快照专用称重托盘', createResponse, + `warehouseId=${warehouseId}`, options) || '') + if (!palletId) return null + + const detailResponse = await this.request('GET', + `/api/platform/v1/pallets/${encodeURIComponent(palletId)}?tenantId=0&warehouseId=${encodeURIComponent(warehouseId)}`) + const pallet = this.apiSuccess(module, '回读本轮库存快照专用称重托盘', detailResponse, + `palletId=${palletId}`, options) + const valid = String(pallet?.id || '') === palletId + && String(pallet?.warehouseId || '') === String(warehouseId) + && Boolean(String(pallet?.palletCode || '').trim()) + this.assert(module, '本轮专用称重托盘尚未复用历史占用身份', detailResponse, valid, + `palletId=${palletId}, palletCode=${pallet?.palletCode || 'missing'}`, options) + if (!valid) return null + + const fixture = { + id: palletId, code: String(pallet.palletCode), warehouseId: String(warehouseId), + marker: this.marker, retainedReason: '测量审核后成为正式库存占用,保留为开发验收证据', + } + this.createdResources.phase3LoadingFreshMeasurementPallet = fixture + return fixture + } + async listAvailableLocationsForA0(warehouseId, module, options) { const locations = [] const seen = new Set() diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 0aa1a47e7..d7de7fe61 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -117,7 +117,7 @@ node --env-file="$env:USERPROFILE\.codex\chongchong-global-dev-acceptance.env" s 其中,无号 push 只冻结装柜计划、建立 bridge 并使预装可见,不得创建 PDA task;订舱放舱也不创建真实货柜或 allocation。港口取得真实柜号后,四字段 link 直接补全原模拟柜并创建 allocation,原 `containerBookingId/cabinetNo/loadingPlan/items` 不变;未关联、订舱未 `RELEASED` 或整份计划未确认时,PDA 查询必须返回业务码 `A0404`。唯一确认入口为 `POST /api/platform/v1/first-leg/loading-plans/{loadingPlanId}/confirm`,请求体只含当前计划 `expectedVersion` 并携带 `Idempotency-Key`;成功必须返回 `confirmationStatus=CONFIRMED`、`readiness.readyForPda=true` 和唯一 `readiness.pdaTaskId`。验收脚本禁止调用旧入库单级 `/preload/confirming`、`/preload/confirm`。 -计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。 +计划在 `EDITING/LOADING` 可通过正式 assignments/PATCH/DELETE 端点增量;计划、任务和明细版本必须递增,PDA 四元身份不变,旧 `inventorySnapshotToken` 必须返回 `A0409 库存已变化,请刷新后重试`。完整三期装柜 fixture 通过平台托盘 API 创建本轮专用称重托盘,再交给通用测量模块;当普通位不足时由 Warehouse 自然分配到 A0,随后仅使用公开 A0 换绑接口制造库存版本变化,不直连数据库或调用内部 Warehouse 接口。主三期链强制完成该专项,四期生产矩阵的附加柜只负责跨区生产,不重复要求 A0;附加柜若自然落入 A0 仍执行同一验证。批量待分柜入口仍只复用 `TP-001`/`YW-TP-002`,不创建托盘。已拣项不可删或减量,未拣项可删;增量会把全拣的 `AWAITING_REVIEW` 撤回为 `IN_PROGRESS`,刷新 task detail 与 inventory 后续拣才可再次等待最终确认。最终唯一完成入口仍为 `POST /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete`:保留同柜 `RELEASED` 订舱、bridge 与五类 ACTIVE 证据门禁;拖车状态和数量不是前置门禁。主事务提交后,零条有效拖车直接跳过,恰有一条时在独立事务尝试推进 `COMPLETED`,多条或并发冲突只记录拖车同步失败,不回滚已完成的 task/plan/inbound。PDA 结算可能先递增来源入库单版本或创建实装分片,完成动作必须在结算后重新锁定实际完成行并使用当前版本;HTTP finalizer 会回查 full/partial 履约分片,失败时保存 `requestId/traceId` 并断言 task/plan/inbound/operation 零漂移。后续模块必须复用 `phase3Loading` 发布的本轮 `taskId + inboundId + loadingPlanId + containerBookingId`,不得回退历史资源。 `phase3-pda-convergence` 同时回查 `/tasks/pending` 与稳定 task detail:两者的 `workflowStatus`、`itemCount`、`pendingItemCount` 必须来自同一任务明细事实。全拣任务必须在两处均为 `AWAITING_REVIEW`,可执行证据 GET,但派生 `actions=[]`,不得重复发送 batch;操作历史必须恰有一次有效 `BATCH`。若 pending 仍按 assignment 返回 `IN_PROGRESS`,该轮以真实 HTTP 失败阻断,不由脚本或前端改写状态。 diff --git a/scripts/admin-test-env-modules/inbound-measurement.mjs b/scripts/admin-test-env-modules/inbound-measurement.mjs index b7e2caed7..cc5115cbd 100644 --- a/scripts/admin-test-env-modules/inbound-measurement.mjs +++ b/scripts/admin-test-env-modules/inbound-measurement.mjs @@ -30,6 +30,13 @@ function validPdf(response) { async function reusablePallets(context, warehouseId) { const cacheKey = asId(warehouseId) + const override = context.context?.measurementPalletOverride + if (asId(override?.warehouseId) === cacheKey && asId(override?.id) + && String(override?.code || '').trim()) { + context.assert(MODULE, '使用编排层提供的本轮专用称重托盘', null, true, + `warehouseId=${warehouseId}, palletId=${asId(override.id)}`, OPTIONS) + return [{ id: asId(override.id), code: String(override.code) }] + } const allowAnyExistingPallet = context.context?.allowAnyExistingPallet === true const scopedCacheKey = `${cacheKey}:${allowAnyExistingPallet ? 'any' : 'fixed'}` if (reusablePalletsByWarehouse.has(scopedCacheKey)) return reusablePalletsByWarehouse.get(scopedCacheKey) @@ -185,7 +192,7 @@ async function provisionWarehouseResources(context, { inboundId, tenantId, wareh context.createdResources.inboundWarehouseResources ??= {} context.createdResources.inboundWarehouseResources[inboundId] = { marker: context.marker, tenantId: '0', warehouseId: asId(warehouseId), palletCount, - reusablePalletCodes: REUSABLE_PALLET_CODES, resources, + reusablePalletCodes: candidates.map((item) => item.code), resources, } return { palletId: palletIds[0], palletIds, palletCount, resources } } diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 8890a5ba4..fbaed3f88 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -760,12 +760,15 @@ export async function run(context) { if (refreshedInventory?.snapshotToken) { effectiveBatchPayload = { ...batchPayload, inventorySnapshotToken: refreshedInventory.snapshotToken } } - } else { + } else if (context.context?.phase3LoadingRequireSharedOverflow === true) { 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') + } else { + context.addStep(MODULE, '附加生产者不重复要求 A0 库存快照变更专项', null, true, + '主三期装柜链已独立强制验证;当前变体仅生成四期跨区生产者', OPTIONS) } const canonicalPick = await request(context, 'POST', batchPath, effectiveBatchPayload, diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index 2db771bdc..bd1ef2a7f 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -546,18 +546,26 @@ test('phase3 loading fixture paginates A0 candidates and refuses mass location m const orders = source('./orders.mjs') assertOrder(entry, [ 'disableAvailableLocationsForA0(warehouseId, requiredPalletCount)', - 'this.context.phase3LoadingRequireSharedOverflow = isolation.forced === true', + 'this.context.phase3LoadingRequireSharedOverflow = requireSharedOverflow', + 'createPhase3FreshMeasurementPallet(warehouseId)', "executeModule('inbound-measurement-phase3-loading-fixture'", 'restoreAvailableLocationsAfterA0(warehouseId, isolation)', ]) assert.match(entry, /page=\$\{pageNumber\}&size=\$\{A0_LOCATION_PAGE_SIZE\}/) assert.match(entry, /A0_MAX_LOCATION_MUTATIONS = 8/) assert.match(entry, /A0 大仓保护:不批量停用普通库位/) + assert.match(entry, /创建本轮库存快照专用称重托盘/) + assert.match(entry, /\/api\/platform\/v1\/pallets\?tenantId=0/) + assert.match(entry, /measurementPalletOverride/) + assert.doesNotMatch(entry, /internal\/warehouse.*inventory-snapshot-a0/) assert.match(entry, /remainingNormal\.length < isolation\.requiredSlots/) assert.match(entry, /\/locations\/\$\{encodeURIComponent\(location\.id\)\}\/status/) assert.match(entry, /\{ status: 'DISABLED', version: Number\(location\.version\) \}/) assert.match(entry, /\{ status: 'AVAILABLE', version: location\.version \}/) assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/) + const loading = source('./phase3-loading.mjs') + assert.match(loading, /context\.context\?\.phase3LoadingRequireSharedOverflow === true/) + assert.match(loading, /附加生产者不重复要求 A0 库存快照变更专项/) }) test('customs reuses the loading chain released booking without pre-completing truck', () => {