diff --git a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs index e64e70553..aa7bc88c8 100644 --- a/scripts/admin-test-env-modules/phase3-loading-finalize.mjs +++ b/scripts/admin-test-env-modules/phase3-loading-finalize.mjs @@ -366,6 +366,7 @@ export async function run(context) { sourceOrderId: orderId, sourceInboundId: inboundId, cartonMark: fixture.cartonMark || null, + cartonMarks: Array.isArray(fixture.cartonMarks) ? fixture.cartonMarks : [], destinationType: fixture.destinationType || null, destinationWarehouseId: id(fixture.destinationWarehouseId), destinationWarehouseCode: fixture.destinationWarehouseCode || null, diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index b2b22f6ef..38c2da961 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -1213,6 +1213,8 @@ export async function run(context) { context.createdResources.phase3Loading = { tenantId, orderId: id(source?.orderId), inboundId, preloadPlanId: planId, cartonMark: source?.boxCode || source?.cartonMark || null, + cartonMarks: [...new Set((stagedTask?.items || []).flatMap((item) => item?.boxCodes || []) + .map((value) => String(value || '').trim()).filter(Boolean))], destinationType: initial?.summary?.destinationType || source?.destinationType || null, destinationWarehouseId: id(initial?.summary?.destinationWarehouseId || source?.destinationWarehouseId), destinationWarehouseCode: initial?.summary?.destinationCode || source?.destinationWarehouseCode || null, diff --git a/scripts/admin-test-env-modules/phase3-port-schedules.mjs b/scripts/admin-test-env-modules/phase3-port-schedules.mjs index 314271727..823a79b81 100644 --- a/scripts/admin-test-env-modules/phase3-port-schedules.mjs +++ b/scripts/admin-test-env-modules/phase3-port-schedules.mjs @@ -122,8 +122,7 @@ export async function run(context) { || context.createdResources?.phase3ContainerLoading const ocean = await ensureOceanBooking(context, suffix, master, flow?.oceanBookingId) if (!ocean?.id) return - const containerBookingId = ['COMPLETED', 'LOADED'].includes(flow?.finalStatus) - ? id(flow.containerBookingId) : null + const containerBookingId = id(flow?.containerBookingId) || null const knownAllocation = containerBookingId && ocean.allocatedContainerBookingIds?.includes(containerBookingId) ? containerBookingId : null if (containerBookingId && !knownAllocation) { 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 735a08096..4d1662b28 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 @@ -401,6 +401,8 @@ test('preload and loading versions use immediate operation baselines and exact p assert.match(loading, /shippingCompanyId: id\(shippingCarrier\.id\), shippingCompanyName: shippingCarrier\.name/) assert.match(loading, /bookingAgentId: id\(supplier\.id\), bookingAgentName: supplier\.name/) assert.doesNotMatch(loading, /shippingCompanyId: id\(supplier\.id\), shippingCompanyName: supplier\.name/) + assert.match(loading, /cartonMarks: \[\.\.\.new Set\(\(stagedTask\?\.items \|\| \[\]\)\.flatMap/) + assert.match(source('./phase3-loading-finalize.mjs'), /cartonMarks: Array\.isArray\(fixture\.cartonMarks\)/) }) test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => { @@ -510,6 +512,8 @@ test('downstream container consumers prefer the phase3Loading fixture', () => { assert.doesNotMatch(serviceCenter, /customerName: `服务中心链路客户-/) const schedules = source('./phase3-port-schedules.mjs') assert.match(schedules, /ensureOceanBooking\(context, suffix, master, flow\?\.oceanBookingId\)/) + assert.match(schedules, /const containerBookingId = id\(flow\?\.containerBookingId\) \|\| null/) + assert.doesNotMatch(schedules, /\['COMPLETED', 'LOADED'\]\.includes\(flow\?\.finalStatus\)/) assert.match(schedules, /allocatedContainerBookingIds: \(detail\.containers \|\| \[\]\)/) assert.match(schedules, /ocean\.allocatedContainerBookingIds\?\.includes\(containerBookingId\)/) assert.match(schedules, /if \(containerBookingId && !knownAllocation\)/) diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs index ab045f947..07be663c7 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda-contract.test.mjs @@ -43,6 +43,8 @@ test('PDA module covers the container-number receiving contract and excludes out assert.doesNotMatch(source, /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/) assert.match(source, /目的仓 .* 不得代替物理仓/) assert.match(source, /retained as acceptance evidence/) + assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/) + assert.match(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/) }) test('missing WAREHOUSE_PICKER overseas authority is a deployment blocker', async () => { diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index 2da55c862..1720315b5 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -202,27 +202,40 @@ async function receiveFba(context, token, inbound, pallet) { return true } -async function receiveOverseas(context, token, inbound, pallet, producerCartonMark = null) { - const cartonMark = producerCartonMark || context.createdResources?.inboundMeasurement?.boxCode - if (!nonBlank(cartonMark)) { - blockerGap(context, `OVERSEAS入仓码${inbound.inboundCode}逐箱`, '缺少本轮冻结箱唛,逐箱实收不能使用伪造箱号') +async function receiveOverseas(context, token, inbounds, pallets, producerCartonMarks = []) { + const cartonMarks = [...new Set((Array.isArray(producerCartonMarks) ? producerCartonMarks : [producerCartonMarks]) + .map(id).filter(nonBlank))] + if (!cartonMarks.length) { + blockerGap(context, 'OVERSEAS逐箱链', '缺少本轮PDA任务冻结箱唛,逐箱实收不能使用伪造箱号') return false } - const resolved = await pda(context, token, 'POST', '解析OVERSEAS冻结箱唛', '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }) - if (!success(resolved.response) || resolved.data?.inboundCode !== inbound.inboundCode) { - blockerGap(context, `OVERSEAS入仓码${inbound.inboundCode}箱唛归属`, `冻结箱唛未解析到目标入仓码;cartonMark=${cartonMark}`, 'fixture-mismatch', resolved.response) - return false + for (const [index, cartonMark] of cartonMarks.entries()) { + const resolved = await pda(context, token, 'POST', `解析OVERSEAS冻结箱唛${index + 1}`, + '/api/pda/v1/last-leg/receiving-containers/current/cartons/resolve', { cartonMark }) + const inbound = inbounds.find((item) => item.inboundCode === resolved.data?.inboundCode) + const pallet = inbound ? pallets.get(inbound.inboundCode) : null + if (!success(resolved.response) || !inbound || !pallet) { + blockerGap(context, `OVERSEAS箱唛${cartonMark}归属`, + `冻结箱唛未解析到带真实托盘的本柜入仓码;inboundCode=${resolved.data?.inboundCode || 'missing'}`, + 'fixture-mismatch', resolved.response) + return false + } + const body = { cartonMark, palletCode: pallet.palletCode, + idempotencyKey: key(context, 'overseas-carton', cartonMark) } + const first = await pda(context, token, 'POST', `OVERSEAS逐箱实收${index + 1}`, + '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) + if (!success(first.response)) return false + const replay = await pda(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键同载荷重放`, + '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) + context.assert(MODULE, `OVERSEAS逐箱${index + 1}幂等结果一致`, replay.response, + success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS) + await expected(context, token, 'POST', `OVERSEAS逐箱${index + 1}同键异载荷冲突`, + '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', + { ...body, palletCode: `${pallet.palletCode}-MUTATED` }, conflict) + await expected(context, token, 'POST', `OVERSEAS重复箱新键被拒绝${index + 1}`, + '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', + { ...body, idempotencyKey: key(context, 'overseas-duplicate', cartonMark) }, conflict) } - const body = { cartonMark, palletCode: pallet.palletCode, idempotencyKey: key(context, 'overseas-carton') } - const first = await pda(context, token, 'POST', 'OVERSEAS逐箱实收', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) - if (!success(first.response)) return false - const replay = await pda(context, token, 'POST', 'OVERSEAS逐箱同键同载荷重放', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', body) - context.assert(MODULE, 'OVERSEAS逐箱幂等结果一致', replay.response, - success(replay.response) && JSON.stringify(replay.data) === JSON.stringify(first.data), trace(replay.response), OPTIONS) - await expected(context, token, 'POST', 'OVERSEAS逐箱同键异载荷冲突', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', - { ...body, palletCode: `${pallet.palletCode}-MUTATED` }, conflict) - await expected(context, token, 'POST', 'OVERSEAS重复箱新键被拒绝', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', - { ...body, idempotencyKey: key(context, 'overseas-duplicate') }, conflict) return true } @@ -312,7 +325,7 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe success(resolved.response) && resolved.data?.compatible === true && resolved.data?.destinationType === inbound.destinationType, trace(resolved.response), OPTIONS) } const fbaInbounds = current.data.inbounds.filter((item) => item.destinationType === 'FBA') - const overseasInbound = current.data.inbounds.find((item) => item.destinationType === 'OVERSEAS') + const overseasInbounds = current.data.inbounds.filter((item) => item.destinationType === 'OVERSEAS') if (fbaInbounds.length && fbaInbounds.every((item) => pallets.has(item.inboundCode))) { const firstFba = fbaInbounds[0] await expected(context, token, 'POST', 'FBA模式硬拒绝逐箱收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/carton', { @@ -323,13 +336,15 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe } else if (preferredProducer?.destinationType !== 'OVERSEAS') { blockerGap(context, 'FBA SKU按箱数链', '当前FBA柜没有带真实托盘的 FBA 入仓码') } - if (overseasInbound && pallets.has(overseasInbound.inboundCode)) { - const pallet = pallets.get(overseasInbound.inboundCode) + if (overseasInbounds.length && overseasInbounds.every((item) => pallets.has(item.inboundCode))) { + const firstOverseas = overseasInbounds[0] + const pallet = pallets.get(firstOverseas.inboundCode) await expected(context, token, 'POST', 'OVERSEAS模式硬拒绝批量收货', '/api/pda/v1/last-leg/receiving-containers/current/receipts/bulk', { - inboundCode: overseasInbound.inboundCode, skuCode: 'MODE-REJECT', palletCode: pallet.palletCode, + inboundCode: firstOverseas.inboundCode, skuCode: 'MODE-REJECT', palletCode: pallet.palletCode, packageCount: 1, idempotencyKey: key(context, 'overseas-bulk-forbidden'), }, (response) => conflictMessage(response, /OVERSEAS.*carton|bulk.*forbidden/i)) - await receiveOverseas(context, token, overseasInbound, pallet, preferredProducer?.cartonMark) + await receiveOverseas(context, token, overseasInbounds, pallets, + preferredProducer?.cartonMarks?.length ? preferredProducer.cartonMarks : [preferredProducer?.cartonMark]) } else if (preferredProducer?.destinationType !== 'FBA') { blockerGap(context, 'OVERSEAS逐箱链', '当前OVERSEAS柜没有带真实冻结箱唛与托盘的 OVERSEAS 入仓码') }