diff --git a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs index 7dac0aea0..2c4babb0d 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting-contract.test.mjs @@ -4,12 +4,21 @@ import test from 'node:test' const source = readFileSync(new URL('./phase3-order-splitting.mjs', import.meta.url), 'utf8') -test('each remainder uses a simulation explicitly linked to a confirmed booking', () => { +test('each remainder uses a simulation linked to a confirmed then released booking', () => { assert.match(source, /POST', CONTAINERS/) assert.match(source, /POST', '\/api\/platform\/v1\/ocean-bookings'/) assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/submit/) assert.match(source, /ocean-bookings\/\$\{oceanBookingId\}\/confirm/) assert.match(source, /ocean-booking-link/) + assert.match(source, /bookingDetail\.oceanBooking\.status === 'CONFIRMED'/) + assert.match(source, /`\$\{bookingPath\}\/release`/) + const releaseStart = source.indexOf('const releaseResponse') + const releaseBlock = source.slice(releaseStart, source.indexOf('bookingDetailResponse = await', releaseStart)) + for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', + 'estimatedArrivalAt', 'expectedVersion']) assert.match(releaseBlock, new RegExp(`${field}:`)) + assert.match(releaseBlock, /'Idempotency-Key': `\$\{operationBase\}-booking-release`\.slice\(0, 128\)/) + assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/) + assert.match(source, /id\(item\?\.containerBookingId\) === containerId/) assert.match(source, /candidates\?bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/) assert.match(source, /`\$\{SMART\}\/plans`, \{ bookingId: containerId/) assert.doesNotMatch(source, /candidates\?warehouseId=/) @@ -18,8 +27,9 @@ test('each remainder uses a simulation explicitly linked to a confirmed booking' test('PDA opens only after a real container number and uses stable plan identity', () => { const pushIndex = source.indexOf("`${SMART}/plans/${planId}/push-preload`") const transportIndex = source.indexOf("`${CONTAINERS}/${encodeURIComponent(containerId)}/transport`") + const releaseIndex = source.indexOf("`${bookingPath}/release`") const taskIndex = source.indexOf("`${PDA}/tasks?loadingPlanId=${encodeURIComponent(planId)}&warehouseId=${encodeURIComponent(warehouseId)}`") - assert.ok(transportIndex >= 0 && pushIndex > transportIndex && taskIndex > pushIndex) + assert.ok(transportIndex >= 0 && releaseIndex > transportIndex && pushIndex > releaseIndex && taskIndex > pushIndex) assert.match(source, /containerNo: realContainerNo/) assert.match(source, /const transportStable = transported\?\.containerNo === realContainerNo/) assert.match(source, /transported\?\.cabinetNo === simulation\.cabinetNo/) diff --git a/scripts/admin-test-env-modules/phase3-order-splitting.mjs b/scripts/admin-test-env-modules/phase3-order-splitting.mjs index 4c5ac85f4..be284b795 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting.mjs @@ -222,6 +222,35 @@ async function loadOneFragment(context, state) { `containerBookingId=${containerId}, containerNo=${transported?.containerNo || 'missing'}, cabinetNo=${transported?.cabinetNo || 'missing'}`, OPTIONS) if (!transportStable) return null + const bookingPath = `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}` + let bookingDetailResponse = await context.request('GET', bookingPath) + let bookingDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 柜次所属真实订舱`, bookingDetailResponse, '', OPTIONS) + const allocationMatches = bookingDetail?.containerAllocations?.some?.((item) => + id(item?.containerBookingId) === containerId) + context.assert(MODULE, `${sourceOrderNo} 订舱分配保持稳定柜次`, bookingDetailResponse, + id(bookingDetail?.oceanBooking?.id) === oceanBookingId && allocationMatches, + `oceanBookingId=${id(bookingDetail?.oceanBooking?.id) || 'missing'}, containerBookingId=${containerId}, allocations=${bookingDetail?.containerAllocations?.length ?? 'n/a'}`, OPTIONS) + if (!allocationMatches || id(bookingDetail?.oceanBooking?.id) !== oceanBookingId) return null + + if (bookingDetail.oceanBooking.status === 'CONFIRMED') { + const releaseResponse = await request(context, 'POST', `${bookingPath}/release`, { + expectedVersion: Number(bookingDetail.oceanBooking.version), + billOfLadingNo: `BL-${operationBase}`.slice(0, 128), + vesselName: `SPLIT-VESSEL-${sequence}`.slice(0, 128), voyageNo: `SV-${sequence}`.slice(0, 64), + estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00', + reason: context.marker, + }, { 'Idempotency-Key': `${operationBase}-booking-release`.slice(0, 128) }) + context.apiSuccess(MODULE, `放舱 ${sourceOrderNo} 的真实订舱并满足 PDA 门禁`, releaseResponse, '', OPTIONS) + bookingDetailResponse = await context.request('GET', bookingPath) + bookingDetail = context.apiSuccess(MODULE, `回查 ${sourceOrderNo} 的 RELEASED 订舱`, bookingDetailResponse, '', OPTIONS) + } + const releasedAllocationMatches = bookingDetail?.containerAllocations?.some?.((item) => + id(item?.containerBookingId) === containerId) + context.assert(MODULE, `${sourceOrderNo} 仅在 RELEASED 且柜次分配稳定后推送`, bookingDetailResponse, + bookingDetail?.oceanBooking?.status === 'RELEASED' && releasedAllocationMatches, + `status=${bookingDetail?.oceanBooking?.status || 'missing'}, containerBookingId=${containerId}`, OPTIONS) + if (bookingDetail?.oceanBooking?.status !== 'RELEASED' || !releasedAllocationMatches) return null + const push = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`, { expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': `${operationBase}-push` }) const pushed = context.apiSuccess(MODULE, `推送 ${sourceOrderNo} 的预装计划`, push, '', OPTIONS)