diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index a13e1dd85..b1032cd59 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -1607,6 +1607,11 @@ export class AcceptanceRun { if (!hasStandardFbaProducer()) { await this.runPhase4ProducerVariant('FBA-STANDARD', 'FBA', 'STANDARD', true) } + const hasSecondStandardFbaProducer = () => (this.createdResources.phase4InboundProducerFixtures?.containers || []) + .some((item) => /-P4-FBA-SECOND(?:-R\d+)?:/.test(String(item?.resourceKey || ''))) + if (!hasSecondStandardFbaProducer()) { + await this.runPhase4ProducerVariant('FBA-SECOND', 'FBA', 'STANDARD', true) + } if (Number(readiness().overseasContainerCount || 0) < 1) { await this.runPhase4ProducerVariant('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true) } @@ -1622,11 +1627,14 @@ export class AcceptanceRun { && item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP' && /-P4-FBA-STANDARD(?:-R\d+)?:/.test(String(item?.resourceKey || ''))) const overseasReady = containers.find((item) => item?.destinationType === 'OVERSEAS') + const secondStandardFbaReady = containers.find((item) => + /-P4-FBA-SECOND(?:-R\d+)?:/.test(String(item?.resourceKey || ''))) const awaitingVariants = containers.filter((item) => ['SECONDARY'] .some((label) => new RegExp(`-P4-${label}(?:-R\\d+)?:`).test(String(item?.resourceKey || '')))) const shippingMatrixReady = dropshipReady?.shippingReady === true && Boolean(dropshipReady?.carrierScheduleQueryId) && standardFbaReady?.shippingReady === true && Boolean(standardFbaReady?.carrierScheduleQueryId) + && secondStandardFbaReady?.shippingReady === true && Boolean(secondStandardFbaReady?.carrierScheduleQueryId) && overseasReady?.shippingReady === true && Boolean(overseasReady?.carrierScheduleQueryId) && awaitingVariants.length >= 1 && awaitingVariants.every((item) => item.shippingReady === false diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 2e157969d..8f2193cab 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -99,6 +99,7 @@ export async function run(context) { - `dropship-outbound-edge-cases.mjs` 是 `dropship-pallet-matrix` 内部探针,不单独运行:使用本轮库存验证超量、错误 SKU、不存在/重复箱唛、箱数不符、同键异载荷、未拣齐完成、并发同键创建、未拣取消和重放。单票单托额外先拣一箱再拣剩余箱;部分拣货被拒时仍保留失败及部署阻断,只有回查冻结事实无变化才继续整条明细拣货以采集后续证据,不能将此计为分批通过。多票共托使用同一 SKU,来源区分必须依赖入仓码。成功数据保留,未拣并发探针出库单正常取消。本扩展不代表跨客户、跨仓、多 SKU 混箱、逐箱不等件数、跨区故障注入均已覆盖,结论以当轮真实报告为准。 - `phase3-container-documents` 从本轮货柜详情读取渠道:海铁验证 DO 上传被明确拒绝且聚合中没有 DO,其他渠道继续验证 DO 上传、重放及零漂移;缺渠道不猜测适用性。 - `phase4-overseas-pda-operations` 用三个独立逐箱任务分别验证正常换标、跳过和异常,异常不得重复使用前两箱。前一步失败时保留失败并停止该链,不继续使用空响应版本。 +- 海外 PDA 生产矩阵为实际作业准备 `FBA-STANDARD`、`FBA-SECOND` 两个通过真实出仓和船节点达到 `PICKUP_READY` 的标准 FBA 柜,同时保留 `SECONDARY` 未放行柜用于负向门禁;不得将负向柜当作第二个收货柜。 - 海外 PDA 按真实柜号去重执行签收,缺少一件代发生产者不得用 FBA 生产者替代;原生产矩阵失败仍保留。P4 解绑样本保留为 `UNBOUND`,P5 只从本轮已完成同柜、同目的仓的入仓码中选择三箱实物托盘库存,排除 `UNBOUND` 和重复仓位,不借用历史订单。 - 排车详情按 `DispatchResponse.Item` 核对托盘与箱数,不读取 DTO 未提供的箱唛字段;`phase5-overseas-fulfillment` 将同一换标排车转为待出库单,通过 `OutboundDetail.pallets.cartonMarks` 验证全部新箱唛、排除原箱唛和重复箱。该测试生成的待出库单及派送单保留为本轮证据,不推进人工退货样本。 - 出库前同操作员、同草稿允许重复预排并更新原排车;脚本核对排车身份与业务数量不漂移,不要求重新生成的明细行 ID 不变。其他草稿抢锁仍必须冲突,生成出库单后仍验证托盘编辑被拒绝。 diff --git a/scripts/admin-test-env-modules/phase3-pda-convergence.mjs b/scripts/admin-test-env-modules/phase3-pda-convergence.mjs index 24c382dac..5571a5f01 100644 --- a/scripts/admin-test-env-modules/phase3-pda-convergence.mjs +++ b/scripts/admin-test-env-modules/phase3-pda-convergence.mjs @@ -11,7 +11,7 @@ export function buildPendingActions(task) { async function request(context, method, path, body, headers = {}) { try { const response = await fetch(`${context.options.gateway}${path}`, { method, - headers: { Accept: 'application/json', Authorization: `Bearer ${context.pdaToken && /\/pda-loading\/tasks\/\d+/.test(path) ? context.pdaToken : context.token}`, + headers: { Accept: 'application/json', Authorization: `Bearer ${context.pdaToken && /\/pda-loading\/tasks\/(?:\d+|pending)(?:[/?]|$)/.test(path) ? context.pdaToken : context.token}`, ...(body === undefined ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers }, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(60_000) }) const raw = await response.text(); let payload = null @@ -55,25 +55,8 @@ export async function run(context) { const pendingResponse = await request(context, 'GET', `/api/platform/v1/first-leg/pda-loading/tasks/pending?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}`) const pendingTasks = context.apiSuccess(MODULE, '读取当前操作员 PDA pending 列表', pendingResponse, '', OPTIONS) - const pendingRecords = Array.isArray(pendingTasks) ? pendingTasks - : pendingTasks?.records || pendingTasks?.items || [] - let pendingTask = pendingRecords.find((item) => id(item?.taskId || item?.id) === taskId) - if (!pendingTask) { - const currentKeyword = id(taskDetail?.currentContainerNo) - const currentPendingResponse = currentKeyword ? await request(context, 'GET', - `/api/platform/v1/first-leg/pda-loading/tasks/pending?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(currentKeyword)}`) : null - const currentPending = currentPendingResponse - ? context.apiSuccess(MODULE, '按任务当前真实柜号回查 PDA pending 列表', currentPendingResponse, '', OPTIONS) : [] - const currentRecords = Array.isArray(currentPending) ? currentPending : currentPending?.records || currentPending?.items || [] - pendingTask = currentRecords.find((item) => id(item?.taskId || item?.id) === taskId) - } - if (!pendingTask) { - const broadPendingResponse = await request(context, 'GET', - `/api/platform/v1/first-leg/pda-loading/tasks/pending?warehouseId=${encodeURIComponent(warehouseId)}`) - const broadPending = context.apiSuccess(MODULE, '无关键词回查当前操作员 PDA pending 列表', broadPendingResponse, '', OPTIONS) - const broadRecords = Array.isArray(broadPending) ? broadPending : broadPending?.records || broadPending?.items || [] - pendingTask = broadRecords.find((item) => id(item?.taskId || item?.id) === taskId) - } + const pendingTask = Array.isArray(pendingTasks) + ? pendingTasks.find((item) => id(item?.taskId) === taskId) : null const unresolvedItems = (taskDetail?.items || []).filter((item) => ['PENDING', 'PICKING'].includes(item?.itemStatus)) context.assert(MODULE, 'pending 与 detail 使用同一 workflowStatus 和未完成数量事实', pendingResponse, diff --git a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs index 188b05f6a..55b5fac54 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-producer-fixture-contract.test.mjs @@ -129,6 +129,8 @@ test('only PDA-operated variants traverse real outbound and carrier nodes before const entry = read('../admin-test-env-acceptance.mjs') assert.match(entry, /runPhase4ProducerVariant\('OVERSEAS-PRIMARY', 'OVERSEAS', 'ONE_CLICK_DROPSHIP', true\)/) assert.match(entry, /runPhase4ProducerVariant\('FBA-STANDARD', 'FBA', 'STANDARD', true\)/) + assert.match(entry, /runPhase4ProducerVariant\('FBA-SECOND', 'FBA', 'STANDARD', true\)/) + assert.match(entry, /secondStandardFbaReady\?\.shippingReady === true/) assert.match(entry, /runPhase4ProducerVariant\('SECONDARY', 'FBA', 'STANDARD', false\)/) assert.match(entry, /const hasSecondaryProducer =/) assert.match(entry, /if \(!hasSecondaryProducer\(\)\)/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index 629055a77..81593dd47 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -934,6 +934,8 @@ export async function run(context) { const dropship = matrix?.containers?.find((item) => item?.fulfillmentType === 'ONE_CLICK_DROPSHIP') const standardFbaProducers = (matrix?.byDestinationType?.FBA || []) .filter((item) => item?.fulfillmentType !== 'ONE_CLICK_DROPSHIP') + .filter((item) => /-P4-(?:FBA-STANDARD|FBA-SECOND)(?:-R\d+)?:/.test(String(item?.resourceKey || ''))) + .sort((left, right) => String(left?.resourceKey).localeCompare(String(right?.resourceKey))) .slice(0, 2) // The sealed dropship pallet proves the indivisible handover. A separate standard FBA // fixture supplies multiple cartons for reversible operations. The second standard FBA