From 93b58fdd6da5778d7cf30bd45330c66eaec3b825 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sun, 20 Sep 2026 19:45:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../phase4-overseas-pda-contract.test.mjs | 8 +++++++ .../phase4-overseas-pda.mjs | 23 ++++++++++++------- .../truck-appointments-contract.test.mjs | 4 ++++ .../truck-appointments.mjs | 1 + 4 files changed, 28 insertions(+), 8 deletions(-) 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 3b581ae11..f67aaa181 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 @@ -6,6 +6,14 @@ import { fileURLToPath } from 'node:url' import { expandOverseasCartonCandidates, resolveCartonFixtures, run, uniqueContainerProducer } from './phase4-overseas-pda.mjs' +test('receiving selects one current booking and supplies mandatory booking identity', () => { + assert.match(source, /id\(item.sourceContainerBookingId\) === preferredBookingId/) + assert.match(source, /matchingCandidates.length === 1/) + assert.match(source, /if \(!longId\(sourceContainerBookingId\)\)/) + assert.match(source, /sourceContainerBookingId=\$\{encodeURIComponent\(sourceContainerBookingId\)\}/) + assert.equal((source.match(/\/prepare\$\{bookingQuery\}/g) || []).length, 3) +}) + test('one physical container is received once even when matrix roles overlap', () => { const fba = { containerNo: 'TEST-C1', fulfillmentType: 'STANDARD' } const overseas = { containerNo: 'TEST-C2' } diff --git a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs index 10e9012f7..629055a77 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-pda.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-pda.mjs @@ -692,9 +692,10 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe } const preferredContainer = id(preferredProducer?.containerNo) || context.createdResources?.phase3Loading?.containerNo - const candidate = preferredContainer - ? candidates.data.find((item) => item.containerNo === preferredContainer) - : candidates.data[0] + const preferredBookingId = id(preferredProducer?.containerBookingId) + const matchingCandidates = candidates.data.filter(item => item.containerNo === preferredContainer + && (!preferredBookingId || id(item.sourceContainerBookingId) === preferredBookingId)) + const candidate = matchingCandidates.length === 1 ? matchingCandidates[0] : null if (!candidate) { blockerGap(context, `本轮柜号${preferredContainer || 'missing'}候选`, '三期装柜完成生产者已落地,但海外候选尚未出现该柜;禁止回退处理历史或其他柜', 'producer-chain', candidates.response) @@ -705,27 +706,33 @@ async function runReceiving(context, token, secondaryToken, merchantToken, prefe nonBlank(containerNo) && Number(candidate?.totalPackages) > 0 && Number(candidate?.receivedPackages) >= 0, `containerNo=${containerNo || 'missing'}, total=${candidate?.totalPackages ?? 'missing'}; ${trace(candidates.response)}`, OPTIONS) if (!containerNo) return + const sourceContainerBookingId = id(candidate.sourceContainerBookingId) + if (!longId(sourceContainerBookingId)) { + blockerGap(context, '候选来源柜次身份', '候选未返回有效sourceContainerBookingId,不得仅按可复用柜号选柜') + return + } + const bookingQuery = `?sourceContainerBookingId=${encodeURIComponent(sourceContainerBookingId)}` context.createdResources.phase4OverseasPda.containerNo = containerNo - const detail = await pda(context, token, 'GET', '查询柜号完整入仓码详情', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}`) + const detail = await pda(context, token, 'GET', '查询柜号完整入仓码详情', `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}${bookingQuery}`) if (!Array.isArray(detail.data?.inbounds) || detail.data.inbounds.length === 0) return const prepareBody = { idempotencyKey: key(context, 'prepare') } const prepared = await pda(context, token, 'POST', '选择柜号并建立WAIT_SIGN会话', - `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, prepareBody) + `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`, prepareBody) if (!success(prepared.response)) return const prepareReplay = await pda(context, token, 'POST', '选择柜号同键重放', - `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, prepareBody) + `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`, prepareBody) context.assert(MODULE, '选择柜号幂等结果一致', prepareReplay.response, success(prepareReplay.response) && prepareReplay.data?.containerNo === containerNo && prepareReplay.data?.status === 'WAIT_SIGN', trace(prepareReplay.response), OPTIONS) if (secondaryToken) { await expected(context, secondaryToken, 'POST', '第二仓库操作员抢占同柜返回409', - `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare`, + `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(containerNo)}/prepare${bookingQuery}`, { idempotencyKey: key(context, 'secondary-prepare') }, conflict) } if (runAuthorizationProbes) { const other = candidates.data.find((item) => item.containerNo !== containerNo && item.operable === true && item.allPlansReady === true && Number(item.totalPackages) > 0) if (other) await expected(context, token, 'GET', '当前账号跨柜切换锁返回409', - `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(other.containerNo)}`, undefined, conflict) + `/api/pda/v1/last-leg/receiving-containers/${encodeURIComponent(other.containerNo)}?sourceContainerBookingId=${encodeURIComponent(other.sourceContainerBookingId)}`, undefined, conflict) else blockerGap(context, '跨柜锁409', '本轮首柜作业前只有一个可收柜,无法在不伪造柜数据的前提下验证跨柜锁') } const photos = await uploadPhotos(context, token) diff --git a/scripts/admin-test-env-modules/truck-appointments-contract.test.mjs b/scripts/admin-test-env-modules/truck-appointments-contract.test.mjs index f7af05110..2ca2a9244 100644 --- a/scripts/admin-test-env-modules/truck-appointments-contract.test.mjs +++ b/scripts/admin-test-env-modules/truck-appointments-contract.test.mjs @@ -7,6 +7,10 @@ import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes, const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8') +test('self-contained truck booking bill has its own full run namespace', () => { + assert.match(source, /billOfLadingNo: `BL-TRUCK-\$\{context.marker\}`/) +}) + test('PDF fixtures contain actual PDF magic bytes', () => { const content = Buffer.from(minimalPdfBytes('TRUCK')).toString('ascii') assert.equal(content.slice(0, 5), '%PDF-') diff --git a/scripts/admin-test-env-modules/truck-appointments.mjs b/scripts/admin-test-env-modules/truck-appointments.mjs index aa551cb84..40478fa9b 100644 --- a/scripts/admin-test-env-modules/truck-appointments.mjs +++ b/scripts/admin-test-env-modules/truck-appointments.mjs @@ -143,6 +143,7 @@ async function fixture(context) { const carrier = await ensureMatsonCarrierPartner(context, MODULE, OPTIONS) if (!bookingAgent || !truckSupplier || !carrier?.id) return null const draft = oceanBookingDraft(context, suffix, master, { + billOfLadingNo: `BL-TRUCK-${context.marker}`.slice(0, 64), bookingAgentId: bookingAgent.supplierId, bookingAgentName: bookingAgent.supplierName, shippingCompanyId: id(carrier.id), shippingCompanyName: carrier.name, carrierCode: 'MATSON', containerRequirements: [{ containerType: '40HQ', containerKind: 'FCL', quantity: 2, capacityCbm: 68, maxWeightKg: 19500 }],