diff --git a/scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs b/scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs index 24d896cd5..e41aa2f9f 100644 --- a/scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-container-documents-contract.test.mjs @@ -8,12 +8,12 @@ import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs' const source = readFileSync(new URL('./phase3-container-documents.mjs', import.meta.url), 'utf8') const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8') -test('four canonical container documents are frozen and use only public platform APIs', () => { +test('canonical operational container documents use only public platform APIs', () => { assert.deepEqual(DOCUMENT_TYPES, [ - 'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER', + 'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER', ]) for (const token of [ - '/clearance/document/upload', '/clearance/complete', '/customs/document/upload', '/customs/complete', + '/clearance/document/upload', '/clearance/complete', '/documents/${type}/upload', '/overview', 'requestMultipart', 'Idempotency-Key', ]) assert.ok(source.includes(token), token) assert.doesNotMatch(source, /\/internal\/|https?:\/\/|process\.env|\bfetch\s*\(/) diff --git a/scripts/admin-test-env-modules/phase3-container-documents.mjs b/scripts/admin-test-env-modules/phase3-container-documents.mjs index 44df3e249..6e39ae731 100644 --- a/scripts/admin-test-env-modules/phase3-container-documents.mjs +++ b/scripts/admin-test-env-modules/phase3-container-documents.mjs @@ -1,10 +1,9 @@ const MODULE = 'phase3-container-documents' -const OPTIONS = { scenario: 'phase3-container-four-documents', deploymentBlocker: true } +const OPTIONS = { scenario: 'phase3-container-operational-documents', deploymentBlocker: true } const CONTAINERS = '/api/platform/v1/containers' -const FIRST_LEG = '/api/platform/v1/first-leg' export const DOCUMENT_TYPES = Object.freeze([ - 'DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER', + 'DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER', ]) function id(value) { return value == null ? '' : String(value) } @@ -37,7 +36,7 @@ async function upload(context, path, type, expectedVersion, operationReason, ide } async function uploadAndVerify(context, { type, uploadPath, readPath, completePath = null }) { - const operationReason = `${context.marker} 四类货柜资料全链验收` + const operationReason = `${context.marker} 货柜作业资料全链验收` const uploadKey = key(context, `${type}-upload`) const response = await upload(context, uploadPath, type, 0, operationReason, uploadKey) let document = context.apiSuccess(MODULE, `上传${type}`, response, `documentType=${type}`, OPTIONS) @@ -102,7 +101,7 @@ export async function run(context) { const warehouseId = id(fixture?.warehouseId) if (fixture?.marker !== context.marker || fixture?.finalizeSucceeded !== true || fixture?.finalStatus !== 'LOADED' || !containerId || !planId || !warehouseId) { - context.addStep(MODULE, '取得本轮LOADED柜次四类资料前置', null, false, + context.addStep(MODULE, '取得本轮LOADED柜次作业资料前置', null, false, `marker=${fixture?.marker || 'missing'}, status=${fixture?.finalStatus || 'missing'}, containerBookingId=${containerId || 'missing'}, loadingPlanId=${planId || 'missing'}, warehouseId=${warehouseId || 'missing'}`, OPTIONS) return @@ -115,12 +114,6 @@ export async function run(context) { readPath: `${CONTAINERS}/${containerId}/clearance`, completePath: `${CONTAINERS}/${containerId}/clearance/complete`, }, - { - type: 'CUSTOMS_DECLARATION', - uploadPath: `${FIRST_LEG}/loading-lists/${planId}/customs/document/upload?warehouseId=${encodeURIComponent(warehouseId)}`, - readPath: `${FIRST_LEG}/loading-lists/${planId}/customs?warehouseId=${encodeURIComponent(warehouseId)}`, - completePath: `${FIRST_LEG}/loading-lists/${planId}/customs/complete?warehouseId=${encodeURIComponent(warehouseId)}`, - }, ...['DELIVERY_ORDER', 'PICKUP_ORDER'].map((type) => ({ type, uploadPath: `${CONTAINERS}/${containerId}/documents/${type}/upload`, @@ -131,9 +124,9 @@ export async function run(context) { const documents = [] for (const spec of specs) documents.push(await uploadAndVerify(context, spec)) const overviewResponse = await context.request('GET', `${CONTAINERS}/${containerId}/overview`) - const overview = context.apiSuccess(MODULE, '回查货柜四类资料聚合', overviewResponse, '', OPTIONS) + const overview = context.apiSuccess(MODULE, '回查货柜作业资料聚合', overviewResponse, '', OPTIONS) const storedTypes = new Set((overview?.documents || []).map((item) => item?.documentType)) - context.assert(MODULE, '同一LOADED柜次聚合返回四类资料且身份未漂移', overviewResponse, + context.assert(MODULE, '同一LOADED柜次聚合返回作业资料且身份未漂移', overviewResponse, id(overview?.container?.id) === containerId && documents.every(Boolean) && DOCUMENT_TYPES.every((type) => storedTypes.has(type)), `containerBookingId=${containerId}, documentTypes=${[...storedTypes].join('|') || 'none'}`, OPTIONS) diff --git a/scripts/admin-test-env-modules/phase3-customs.mjs b/scripts/admin-test-env-modules/phase3-customs.mjs index 9137fae75..403ed2cc8 100644 --- a/scripts/admin-test-env-modules/phase3-customs.mjs +++ b/scripts/admin-test-env-modules/phase3-customs.mjs @@ -1,26 +1,17 @@ -import { customsCaseFieldGaps } from './fulfillment-list-field-contract.mjs' - const MODULE = 'phase3-customs' -const OPTIONS = { scenario: 'phase3-customs-collaboration', deploymentBlocker: true } -const PLATFORM = '/api/platform/v1/customs' -const MERCHANT = '/api/merchant/v1/customs' -const MERCHANT_USERNAME = process.env.MERCHANT_TEST_USERNAME || '' -const MERCHANT_PASSWORD = process.env.MERCHANT_TEST_PASSWORD || '' +const OPTIONS = { scenario: 'phase3-container-customs-producer', deploymentBlocker: true } +const PLATFORM_CUSTOMS = '/api/platform/v1/customs/loading-plans' function id(value) { return value == null ? '' : String(value) } function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) } -function failure(response) { - return [400, 401, 403, 404, 409, 503].includes(response?.http) - || (response?.http === 200 && codeOf(response) !== '200') -} -function key(context, operation) { return `${context.marker}-customs-${operation}`.slice(0, 128) } +function key(context, operation) { return `${context.marker}-container-customs-${operation}`.slice(0, 128) } -async function request(context, method, path, body, token = context.token, headers = {}) { +async function request(context, method, path, body, headers = {}) { try { const response = await fetch(`${context.options.gateway}${path}`, { method, headers: { - Accept: 'application/json', Authorization: token ? `Bearer ${token}` : '', + Accept: 'application/json', Authorization: context.token ? `Bearer ${context.token}` : '', ...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json; charset=utf-8' }), ...headers, }, @@ -29,46 +20,30 @@ async function request(context, method, path, body, token = context.token, heade }) const raw = await response.text(); let payload = null try { payload = raw ? JSON.parse(raw) : null } catch { /* bounded raw evidence */ } - return { method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null, + return { + method, path, http: response.status, body: payload, raw: payload === null ? raw.slice(0, 500) : null, requestId: payload?.requestId || response.headers.get('x-request-id'), - traceId: payload?.traceId || response.headers.get('x-trace-id') } + traceId: payload?.traceId || response.headers.get('x-trace-id'), + } } catch (error) { return { method, path, http: 0, body: null, raw: String(error), requestId: null, traceId: null } } } -async function upload(context, path, fields, content, fileName, token, idempotencyKey) { - const form = new FormData() - for (const [name, value] of Object.entries(fields)) if (value !== null && value !== undefined) form.append(name, String(value)) - form.append('file', new Blob([content], { type: 'text/plain' }), fileName) - return request(context, 'POST', path, form, token, { 'Idempotency-Key': idempotencyKey }) -} - -async function download(context, path, token) { +async function download(context, path) { try { const response = await fetch(`${context.options.gateway}${path}`, { - headers: { Accept: '*/*', Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(60_000), + headers: { Accept: '*/*', Authorization: `Bearer ${context.token}` }, signal: AbortSignal.timeout(60_000), }) const bytes = new Uint8Array(await response.arrayBuffer()) - let body = null - const contentType = response.headers.get('content-type') || '' - if (contentType.includes('application/json')) { - try { body = JSON.parse(new TextDecoder().decode(bytes)) } catch { /* binary evidence remains bounded */ } - } return { method: 'GET', path, http: response.status, bytes, - body, contentType, requestId: body?.requestId || response.headers.get('x-request-id'), - traceId: response.headers.get('x-trace-id') } + contentType: response.headers.get('content-type') || '', + requestId: response.headers.get('x-request-id'), traceId: response.headers.get('x-trace-id') } } catch (error) { return { method: 'GET', path, http: 0, bytes: new Uint8Array(), contentType: '', raw: String(error) } } } -async function detail(context, caseId) { - const response = await context.request('GET', `${PLATFORM}/cases/${caseId}`) - const data = context.apiSuccess(MODULE, '回查报关协同聚合与版本', response, `caseId=${caseId}`, OPTIONS) - return { response, data } -} - export function selectCustomsFlow(createdResources) { const fixtures = [ createdResources?.orders, @@ -89,7 +64,6 @@ export function selectCustomsFlow(createdResources) { async function ensureReleasedOceanBooking(context, container) { const containerBookingId = id(container?.containerBookingId) - const loadingPlanId = id(container?.loadingPlanId) const oceanBookingId = id(container?.oceanBookingId) if (!oceanBookingId) { context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', null, false, @@ -99,390 +73,96 @@ async function ensureReleasedOceanBooking(context, container) { const response = await context.request('GET', `/api/platform/v1/ocean-bookings/${encodeURIComponent(oceanBookingId)}`) const detail = context.apiSuccess(MODULE, '报关前回读装柜链正式订舱', response, '', OPTIONS) const allocated = detail?.containers?.some?.((item) => id(item?.containerBookingId) === containerBookingId) - if (detail?.oceanBooking?.status !== 'RELEASED' || !allocated) { - context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, false, - `oceanStatus=${detail?.oceanBooking?.status || 'missing'}, oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId || 'missing'}, allocated=${allocated === true}`, OPTIONS) - return false + const passed = detail?.oceanBooking?.status === 'RELEASED' && allocated + context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, passed, + `oceanStatus=${detail?.oceanBooking?.status || 'missing'}, oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId}, allocated=${allocated === true}`, OPTIONS) + if (passed) context.createdResources.oceanBooking = { + id: oceanBookingId, marker: context.marker, finalStatus: 'RELEASED', containerBookingId, + loadingPlanId: id(container?.loadingPlanId), } - context.createdResources.oceanBooking = { - id: oceanBookingId, marker: context.marker, finalStatus: 'RELEASED', - containerBookingId, loadingPlanId, - } - context.addStep(MODULE, '报关前订舱已放舱并绑定实际装柜柜次', response, true, - `oceanBookingId=${oceanBookingId}, containerBookingId=${containerBookingId}`, OPTIONS) - return true + return passed } -async function uploadVersion(context, path, label, expectedVersion, operation, content) { - const response = await upload(context, path, { - expectedVersion, operationReason: `${context.marker}-${operation}`, - }, content, `${context.marker}-${operation}.txt`, context.token, key(context, operation)) - const document = context.apiSuccess(MODULE, label, response, '', OPTIONS) - context.assert(MODULE, `${label}返回当前唯一文件事实`, response, - Boolean(document?.id) && Boolean(document?.fileId) && Number.isInteger(Number(document?.version)) - && /^[0-9a-f]{64}$/i.test(String(document?.fileHash || '')), - `documentId=${id(document?.id) || 'missing'}, fileId=${id(document?.fileId) || 'missing'}, version=${document?.version ?? 'missing'}`, OPTIONS) - return { response, document } -} - -async function verifyFourContainerDocuments(context, { containerBookingId, loadingPlanId, warehouseId }) { - const reason = `${context.marker}-人工验收节点资料` - const simpleDocuments = [ - { type: 'DESTINATION_CLEARANCE', base: `/api/platform/v1/containers/${containerBookingId}/clearance`, - uploadPath: `/api/platform/v1/containers/${containerBookingId}/clearance/document/upload` }, - { type: 'CUSTOMS_DECLARATION', base: `/api/platform/v1/first-leg/loading-lists/${loadingPlanId}/customs`, - query: `warehouseId=${encodeURIComponent(warehouseId)}`, - uploadPath: `/api/platform/v1/first-leg/loading-lists/${loadingPlanId}/customs/document/upload?warehouseId=${encodeURIComponent(warehouseId)}` }, - ] - const completed = [] - for (const document of simpleDocuments) { - const first = await uploadVersion(context, document.uploadPath, `首次上传${document.type}`, 0, - `${document.type.toLowerCase()}-v1`, `${context.marker} ${document.type} v1`) - if (!first.document?.fileId) return false - const readPath = `${document.base}${document.query ? `?${document.query}` : ''}` - const firstRead = await context.request('GET', readPath) - const firstEcho = context.apiSuccess(MODULE, `回显${document.type}首版`, firstRead, '', OPTIONS) - context.assert(MODULE, `${document.type}首版回显身份一致`, firstRead, - id(firstEcho?.id) === id(first.document.id) && id(firstEcho?.fileId) === id(first.document.fileId), - `documentId=${id(firstEcho?.id) || 'missing'}, version=${firstEcho?.version ?? 'missing'}`, OPTIONS) - const firstDownloadPath = document.type === 'DESTINATION_CLEARANCE' - ? `${document.base}/document/${id(first.document.fileId)}/download` - : `${document.base}/document/${id(first.document.fileId)}/download?${document.query}` - const firstDownload = await download(context, firstDownloadPath, context.token) - context.assert(MODULE, `下载${document.type}首版`, firstDownload, - firstDownload.http === 200 && firstDownload.bytes.length > 0 && firstDownload.contentType.includes('text/plain'), - `HTTP=${firstDownload.http}, bytes=${firstDownload.bytes.length}`, OPTIONS) - const second = await uploadVersion(context, document.uploadPath, `更新${document.type}为第二版`, - Number(firstEcho?.version), `${document.type.toLowerCase()}-v2`, `${context.marker} ${document.type} v2`) - context.assert(MODULE, `${document.type}更新保持同一聚合并递增版本`, second.response, - id(second.document?.id) === id(first.document.id) && Number(second.document?.version) === Number(firstEcho?.version) + 1, - `documentId=${id(second.document?.id) || 'missing'}, version=${second.document?.version ?? 'missing'}`, OPTIONS) - const completeResponse = await request(context, 'POST', `${document.base}/complete${document.query ? `?${document.query}` : ''}`, { - expectedVersion: Number(second.document?.version), operationReason: reason, - }, context.token, { 'Idempotency-Key': key(context, `${document.type.toLowerCase()}-complete`) }) - const complete = context.apiSuccess(MODULE, `完成${document.type}`, completeResponse, '', OPTIONS) - context.assert(MODULE, `${document.type}完成态、时间和版本可回读`, completeResponse, - id(complete?.id) === id(first.document.id) && Boolean(complete?.completedAt) - && Number(complete?.version) === Number(second.document?.version) + 1, - `completedAt=${complete?.completedAt || 'missing'}, version=${complete?.version ?? 'missing'}`, OPTIONS) - completed.push(complete) - } - - for (const type of ['DELIVERY_ORDER', 'PICKUP_ORDER']) { - const base = `/api/platform/v1/containers/${containerBookingId}/documents/${type}` - const first = await uploadVersion(context, `${base}/upload`, `首次上传${type}`, 0, - `${type.toLowerCase()}-v1`, `${context.marker} ${type} v1`) - if (!first.document?.fileId) return false - const firstDownload = await download(context, `${base}/${id(first.document.fileId)}/download`, context.token) - context.assert(MODULE, `下载${type}首版`, firstDownload, - firstDownload.http === 200 && firstDownload.bytes.length > 0 && firstDownload.contentType.includes('text/plain'), - `HTTP=${firstDownload.http}, bytes=${firstDownload.bytes.length}`, OPTIONS) - const second = await uploadVersion(context, `${base}/upload`, `更新${type}为第二版`, - Number(first.document.version), `${type.toLowerCase()}-v2`, `${context.marker} ${type} v2`) - const echoResponse = await context.request('GET', base) - const echo = context.apiSuccess(MODULE, `回显${type}当前版本`, echoResponse, '', OPTIONS) - context.assert(MODULE, `${type}首次与更新保持同一聚合且当前版本唯一`, echoResponse, - id(echo?.id) === id(first.document.id) && id(echo?.fileId) === id(second.document?.fileId) - && Number(echo?.version) === Number(first.document.version) + 1, - `documentId=${id(echo?.id) || 'missing'}, version=${echo?.version ?? 'missing'}`, OPTIONS) - } - +async function verifyContainerOperationalDocuments(context, containerBookingId) { const overviewResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}/overview`) - const overview = context.apiSuccess(MODULE, '四类资料完成后回查整柜聚合', overviewResponse, '', OPTIONS) + const overview = context.apiSuccess(MODULE, '只读复核前置模块已完成的整柜作业资料', overviewResponse, '', OPTIONS) const documents = Array.isArray(overview?.documents) ? overview.documents : [] const photos = Array.isArray(overview?.pda?.photos) ? overview.pda.photos.filter((item) => item?.status === 'ACTIVE') : [] - context.assert(MODULE, '整柜聚合回显四类当前资料与真实PDA照片证据', overviewResponse, - ['DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER'] - .every((type) => documents.some((item) => item?.documentType === type)) - && completed.every((item) => documents.some((value) => id(value?.id) === id(item?.id) && value?.completed === true)) - && photos.length >= 5 && photos.every((item) => /^\/api\/platform\/v1\/containers\/\d+\/pda\/photos\/\d+\/download$/.test(String(item?.downloadPath || ''))), + const passed = ['DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER'] + .every((type) => documents.some((item) => item?.documentType === type)) + && documents.some((item) => item?.documentType === 'DESTINATION_CLEARANCE' && item?.completed === true) + && photos.length >= 5 + context.assert(MODULE, '整柜聚合回显三类当前作业资料与真实PDA照片证据', overviewResponse, passed, `documents=${documents.map((item) => item?.documentType).join('|') || 'none'}, activePhotos=${photos.length}`, OPTIONS) for (const [index, photo] of photos.entries()) { - const response = await download(context, photo.downloadPath, context.token) + const response = await download(context, photo.downloadPath) context.assert(MODULE, `下载整柜PDA照片证据${index + 1}`, response, response.http === 200 && response.bytes.length > 0 && !response.contentType.includes('application/json'), `fileId=${id(photo?.fileId) || 'missing'}, bytes=${response.bytes.length}`, OPTIONS) } - return true + return passed +} + +async function completeContainerCustoms(context, loadingPlanId) { + const base = `${PLATFORM_CUSTOMS}/${encodeURIComponent(loadingPlanId)}` + let response = await request(context, 'GET', base) + let detail = context.apiSuccess(MODULE, '读取正式整柜报关聚合', response, + `loadingPlanId=${loadingPlanId}`, OPTIONS) + if (!detail?.aggregateId) return null + if (detail.status === 'COMPLETED') return detail + if (detail.status === 'READY_TO_START') { + response = await request(context, 'POST', `${base}/start`, { + expectedVersion: Number(detail.version), confirm: true, remark: context.marker, + }, { 'Idempotency-Key': key(context, 'start') }) + detail = context.apiSuccess(MODULE, '正式整柜报关开始并冻结资料', response, '', OPTIONS) + context.assert(MODULE, '正式整柜报关进入DECLARING', response, + detail?.status === 'DECLARING', `status=${detail?.status || 'missing'}`, OPTIONS) + } + const allBuy = Array.isArray(detail?.inbounds) && detail.inbounds.length > 0 + && detail.inbounds.every((item) => item?.taxMode === 'BUY_DECLARATION') + context.assert(MODULE, '本轮生产者整柜为全买单正式完成路径', response, allBuy, + `taxModes=${(detail?.inbounds || []).map((item) => item?.taxMode).join('|') || 'missing'}`, OPTIONS) + if (!allBuy || detail?.status !== 'DECLARING') return null + response = await request(context, 'POST', `${base}/complete-all-buy`, { + expectedVersion: Number(detail.version), confirm: true, remark: context.marker, + }, { 'Idempotency-Key': key(context, 'complete-all-buy') }) + const completed = context.apiSuccess(MODULE, '正式全买单整柜报关完成', response, '', OPTIONS) + context.assert(MODULE, '正式整柜报关完成并满足出仓报关门禁', response, + completed?.status === 'COMPLETED' && Boolean(completed?.completedAt), + `status=${completed?.status || 'missing'}, completedAt=${completed?.completedAt || 'missing'}`, OPTIONS) + return completed?.status === 'COMPLETED' ? completed : null } export async function run(context) { const { fixture, container } = selectCustomsFlow(context.createdResources) const tenantId = id(fixture?.tenantId) - const parentOrderId = id(fixture?.orderId) const inboundId = id(fixture?.inboundId) const containerBookingId = id(container?.containerBookingId) const loadingPlanId = id(container?.loadingPlanId) - const warehouseId = id(container?.warehouseId || fixture?.warehouseId) - if (!tenantId || !parentOrderId || !inboundId || !containerBookingId || !loadingPlanId || !warehouseId + if (!tenantId || !inboundId || !containerBookingId || !loadingPlanId || !['AWAITING_REVIEW', 'LOADED'].includes(container?.finalStatus) || (container?.finalStatus === 'AWAITING_REVIEW' && (container?.loadingPlanStatus !== 'LOADING' || !container?.canonicalPdaTaskId))) { - context.addStep(MODULE, '取得本轮客户/订单/入库单/柜次隔离 fixture', null, false, - `tenantId=${tenantId || 'missing'}, parentOrderId=${parentOrderId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`, OPTIONS) + context.addStep(MODULE, '取得本轮客户、入仓单与柜次隔离fixture', null, false, + `tenantId=${tenantId || 'missing'}, inboundId=${inboundId || 'missing'}, containerBookingId=${containerBookingId || 'missing'}, loadingPlanId=${loadingPlanId || 'missing'}, containerStatus=${container?.finalStatus || 'missing'}`, + OPTIONS) return } if (!await ensureReleasedOceanBooking(context, container)) return - - const slicesPath = `/api/platform/v1/orders/container-bookings/${encodeURIComponent(containerBookingId)}/fulfillment-slices?tenantId=${encodeURIComponent(tenantId)}` - const slicesResponse = await context.request('GET', slicesPath) - const slices = context.apiSuccess(MODULE, '读取柜次真实履约分片以确定报关订单', slicesResponse, - `containerBookingId=${containerBookingId}, tenantId=${tenantId}`, OPTIONS) - const carryingSlices = Array.isArray(slices) - ? slices.filter((item) => id(item?.tenantId) === tenantId - && id(item?.containerBookingId) === containerBookingId - && id(item?.parentOrderId) === parentOrderId - && item?.splitRole === 'LOADED' - && (!loadingPlanId || id(item?.loadingPlanId) === loadingPlanId)) - : [] - const carryingSlice = carryingSlices.length === 1 ? carryingSlices[0] : null - const actualOrderId = id(carryingSlice?.childOrderId) - const sourceOrderId = id(carryingSlice?.sourceOrderId) - const sourceCarriesLoaded = Boolean(actualOrderId) && actualOrderId === sourceOrderId - const legacyLoadedChild = Boolean(actualOrderId) && Boolean(sourceOrderId) - && actualOrderId !== sourceOrderId && actualOrderId !== parentOrderId - context.assert(MODULE, '报关绑定柜次实际承载的唯一 LOADED 来源分片', slicesResponse, - carryingSlices.length === 1 && (sourceCarriesLoaded || legacyLoadedChild) - && id(carryingSlice?.parentOrderId) === parentOrderId - && id(carryingSlice?.containerBookingId) === containerBookingId - && id(carryingSlice?.tenantId) === tenantId - && Number.isInteger(Number(carryingSlice?.packageCount)) && Number(carryingSlice.packageCount) > 0 - && (!loadingPlanId || id(carryingSlice?.loadingPlanId) === loadingPlanId), - `parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId || 'missing'}, actualOrderId=${actualOrderId || 'missing'}, packageCount=${carryingSlice?.packageCount ?? 'missing'}, model=${sourceCarriesLoaded ? 'SOURCE_CARRIES_LOADED' : legacyLoadedChild ? 'LEGACY_LOADED_CHILD' : 'INVALID'}, containerBookingId=${containerBookingId}, loadingPlanId=${id(carryingSlice?.loadingPlanId) || 'missing'}, matches=${carryingSlices.length}`, OPTIONS) - if (carryingSlices.length !== 1 || (!sourceCarriesLoaded && !legacyLoadedChild) - || !Number.isInteger(Number(carryingSlice?.packageCount)) || Number(carryingSlice.packageCount) <= 0) return - - const loginResponse = await request(context, 'POST', '/api/auth/login', { - loginId: MERCHANT_USERNAME, password: MERCHANT_PASSWORD, - }, '') - const merchantLogin = context.apiSuccess(MODULE, '客户账号登录用于真实报关文件协同', loginResponse, - `username=${MERCHANT_USERNAME}`, OPTIONS) - const merchantToken = merchantLogin?.accessToken - if (!merchantToken) return - - const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-18).toUpperCase() - const checklistPayload = { - checklistCode: `USFBA${suffix}`.slice(0, 40), versionNo: `V${suffix}`.slice(0, 32), - countryCode: 'US', channelCode: 'FBA', cargoType: 'GENERAL', - items: [{ code: 'INVOICE', name: '商业发票', required: true }], + if (!await verifyContainerOperationalDocuments(context, containerBookingId)) { + context.addStep(MODULE, '三类整柜作业资料必须完成后才允许进入出仓确认', null, false, + `containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS) + return } - const checklistResponse = await request(context, 'POST', `${PLATFORM}/checklist-versions`, checklistPayload, - context.token, { 'Idempotency-Key': key(context, 'checklist') }) - const checklist = context.apiSuccess(MODULE, '创建本轮不可变报关资料清单版本', checklistResponse, '', OPTIONS) - const checklistVersionId = id(checklist?.version?.id) - context.assert(MODULE, '资料清单保留国家、渠道、货物类型和必填项目版本', checklistResponse, - Boolean(checklistVersionId) && checklist?.version?.countryCode === 'US' && checklist?.version?.channelCode === 'FBA' - && checklist?.version?.cargoType === 'GENERAL' && checklist?.items?.[0]?.code === 'INVOICE' - && checklist?.items?.[0]?.required === true, - `checklistVersionId=${checklistVersionId || 'missing'}, items=${checklist?.items?.length ?? 'n/a'}`, OPTIONS) - if (!checklistVersionId) return - - const casePayload = { customerId: tenantId, orderId: actualOrderId, inboundId, containerBookingId, - checklistVersionId, remark: context.marker, expectedVersion: 0 } - const caseResponse = await request(context, 'POST', `${PLATFORM}/cases`, casePayload, context.token, - { 'Idempotency-Key': key(context, 'case') }) - const created = context.apiSuccess(MODULE, '创建客户订单柜次关联报关协同单', caseResponse, '', OPTIONS) - const caseId = id(created?.customsCase?.id) - const customsFieldGaps = customsCaseFieldGaps(created) - context.assert(MODULE, '报关单绑定本轮实际承载来源分片、入库单、柜次和清单版本', caseResponse, - Boolean(caseId) && id(created?.customsCase?.tenantId) === tenantId && id(created?.customsCase?.orderId) === actualOrderId - && id(created?.customsCase?.inboundId) === inboundId && id(created?.customsCase?.containerBookingId) === containerBookingId - && id(created?.customsCase?.checklistVersionId) === checklistVersionId - && created?.customsCase?.status === 'DOCUMENTS_PENDING' && Number(created?.customsCase?.version) === 0, - `caseId=${caseId || 'missing'}, parentOrderId=${parentOrderId}, sourceOrderId=${sourceOrderId}, actualOrderId=${actualOrderId}, status=${created?.customsCase?.status || 'missing'}`, OPTIONS) - context.assert(MODULE, '报关列表单号、订单入库柜次、清单版本、状态与更新时间字段完整', caseResponse, - customsFieldGaps.length === 0, `missing=${customsFieldGaps.join(',') || 'none'}`, OPTIONS) - if (!caseId) return - - const caseReplay = await request(context, 'POST', `${PLATFORM}/cases`, casePayload, context.token, - { 'Idempotency-Key': key(context, 'case') }) - context.assert(MODULE, '创建报关单幂等重放返回同一聚合', caseReplay, - codeOf(caseReplay) === '200' && id(caseReplay.body?.data?.customsCase?.id) === caseId, - `HTTP=${caseReplay.http}, code=${codeOf(caseReplay)}, caseId=${id(caseReplay.body?.data?.customsCase?.id) || 'missing'}`, OPTIONS) - - const forgedCaseResponse = await request(context, 'POST', `${PLATFORM}/cases`, { - ...casePayload, containerBookingId: '999999999999999999', remark: `${context.marker}-forged-container`, - }, context.token, { 'Idempotency-Key': key(context, 'forged-case') }) - context.addStep(MODULE, '伪造未承载客户订单的柜次被拒绝', forgedCaseResponse, failure(forgedCaseResponse), - `HTTP=${forgedCaseResponse.http}, code=${codeOf(forgedCaseResponse)}`, OPTIONS) - - const firstUploadPath = `${MERCHANT}/cases/${caseId}/documents/upload` - const firstUpload = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 0 }, - `${context.marker} invoice version 1`, `${context.marker}-invoice-v1.txt`, merchantToken, key(context, 'invoice-v1')) - const document1 = context.apiSuccess(MODULE, '客户上传首版报关发票', firstUpload, '', OPTIONS) - const document1Id = id(document1?.id) - context.assert(MODULE, '客户文件版本、哈希、上传主体和业务归属已持久化', firstUpload, - Boolean(document1Id) && document1?.checklistItemCode === 'INVOICE' && Number(document1?.documentVersion) === 1 - && document1?.status === 'UPLOADED' && document1?.uploadedByType === 'MERCHANT' - && /^[0-9a-f]{64}$/.test(String(document1?.fileHash || '')), - `documentId=${document1Id || 'missing'}, version=${document1?.documentVersion ?? 'missing'}`, OPTIONS) - if (!document1Id) return - - const firstUploadReplay = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 0 }, - `${context.marker} invoice version 1`, `${context.marker}-invoice-v1.txt`, merchantToken, key(context, 'invoice-v1')) - context.assert(MODULE, '客户文件上传在聚合版本变化后仍可幂等重放', firstUploadReplay, - codeOf(firstUploadReplay) === '200' && id(firstUploadReplay.body?.data?.id) === document1Id - && Number(firstUploadReplay.body?.data?.documentVersion) === 1, - `HTTP=${firstUploadReplay.http}, code=${codeOf(firstUploadReplay)}, documentId=${id(firstUploadReplay.body?.data?.id) || 'missing'}`, OPTIONS) - - let current = await detail(context, caseId) - context.assert(MODULE, '资料齐备后进入 REVIEWING 且版本递增', current.response, - current.data?.customsCase?.status === 'REVIEWING' && Number(current.data?.customsCase?.version) === 1, - `status=${current.data?.customsCase?.status || 'missing'}, version=${current.data?.customsCase?.version ?? 'missing'}`, OPTIONS) - - const returnedResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents/${document1Id}/review`, { - decision: 'RETURN', reason: `${context.marker}-发票金额需补正`, supplementDueAt: '2026-09-30T18:00:00', expectedVersion: 1, - }, context.token, { 'Idempotency-Key': key(context, 'return-v1') }) - const returned = context.apiSuccess(MODULE, '平台退回具体文件并创建补件请求', returnedResponse, '', OPTIONS) - context.assert(MODULE, '退回进入 SUPPLEMENT_REQUIRED 并保留原因、时限和历史', returnedResponse, - returned?.customsCase?.status === 'SUPPLEMENT_REQUIRED' && Number(returned?.customsCase?.version) === 2 - && returned?.supplements?.some?.((item) => item.status === 'OPEN' && item.checklistItemCode === 'INVOICE' - && String(item.reason || '').includes(context.marker) && Boolean(item.dueAt)) - && returned?.history?.some?.((item) => item.action === 'REVIEW_DOCUMENT_RETURN' && id(item.resourceId) === document1Id), - `status=${returned?.customsCase?.status || 'missing'}, supplements=${returned?.supplements?.length ?? 'n/a'}`, OPTIONS) - - const replacement = await upload(context, firstUploadPath, { itemCode: 'INVOICE', expectedVersion: 2 }, - `${context.marker} invoice version 2 corrected`, `${context.marker}-invoice-v2.txt`, merchantToken, key(context, 'invoice-v2')) - const document2 = context.apiSuccess(MODULE, '客户补件替换发票并生成新版本', replacement, '', OPTIONS) - const document2Id = id(document2?.id) - context.assert(MODULE, '替换文件保留上一版本引用且版本递增', replacement, - Boolean(document2Id) && Number(document2?.documentVersion) === 2 && id(document2?.replacesDocumentId) === document1Id - && document2?.status === 'UPLOADED', - `documentId=${document2Id || 'missing'}, replaces=${id(document2?.replacesDocumentId) || 'missing'}`, OPTIONS) - if (!document2Id) return - - current = await detail(context, caseId) - context.assert(MODULE, '补件任务解决且历史文件版本未覆盖', current.response, - current.data?.supplements?.some?.((item) => item.status === 'RESOLVED' && id(item.resolvedDocumentId) === document2Id) - && current.data?.documents?.some?.((item) => id(item.id) === document1Id && item.status === 'SUPERSEDED') - && current.data?.documents?.some?.((item) => id(item.id) === document2Id && Number(item.documentVersion) === 2), - `documents=${current.data?.documents?.length ?? 'n/a'}, supplements=${current.data?.supplements?.length ?? 'n/a'}`, OPTIONS) - - const approveResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents/${document2Id}/review`, { - decision: 'APPROVE', reason: '资料完整', supplementDueAt: null, expectedVersion: 3, - }, context.token, { 'Idempotency-Key': key(context, 'approve-v2') }) - const approved = context.apiSuccess(MODULE, '平台审核通过补件版本', approveResponse, '', OPTIONS) - context.assert(MODULE, '全部必填文件通过后进入 READY_TO_SUBMIT', approveResponse, - approved?.customsCase?.status === 'READY_TO_SUBMIT' && Number(approved?.customsCase?.version) === 4, - `status=${approved?.customsCase?.status || 'missing'}, version=${approved?.customsCase?.version ?? 'missing'}`, OPTIONS) - - const supplierCreateResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', { - name: `报关代理-${context.marker}`.slice(0, 128), category: 'CUSTOMS', region: '深圳', - legalRepresentative: '验收法人', contactName: '报关联系人', phone: '13800000000', - email: 'customs-acceptance@example.com', address: '深圳市验收地址', - businessLicenseNo: `CUSTOMS${context.marker.replace(/\D/g, '')}`.slice(0, 64), licenseFileId: null, - status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: context.marker, - }) - const customsAgentId = id(context.apiSuccess(MODULE, '创建本轮正式 ACTIVE 报关代理供应商', - supplierCreateResponse, `marker=${context.marker}`, OPTIONS)) - if (!customsAgentId) return - const supplierDetailResponse = await context.request('GET', - `/api/platform/v1/suppliers/${encodeURIComponent(customsAgentId)}?tenantId=0`) - const customsAgent = context.apiSuccess(MODULE, '回查本轮报关代理供应商', supplierDetailResponse, '', OPTIONS) - context.assert(MODULE, '代理提交使用本轮正式 CUSTOMS 供应商身份', supplierDetailResponse, - id(customsAgent?.id) === customsAgentId && customsAgent?.status === 'ACTIVE' - && customsAgent?.category === 'CUSTOMS' && customsAgent?.remark === context.marker, - `customsAgentId=${id(customsAgent?.id) || 'missing'}, customsAgentName=${customsAgent?.name || 'missing'}`, OPTIONS) - if (id(customsAgent?.id) !== customsAgentId || customsAgent?.status !== 'ACTIVE' - || customsAgent?.category !== 'CUSTOMS') return - context.createdResources.phase3CustomsSupplier = { marker: context.marker, supplierId: customsAgentId, - supplierName: customsAgent.name, status: customsAgent.status } - const batchPayload = { customsAgentId, externalReference: `EXT-${suffix}`, - note: context.marker, expectedVersion: 4 } - const batchResponse = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, batchPayload, - context.token, { 'Idempotency-Key': key(context, 'batch') }) - const batch = context.apiSuccess(MODULE, '生成代理提交批次并冻结文件版本', batchResponse, '', OPTIONS) - const batchId = id(batch?.id) - let snapshot = [] - try { snapshot = JSON.parse(batch?.fileSnapshotJson || '[]') } catch { /* asserted below */ } - context.assert(MODULE, '代理批次冻结当前文件 ID、版本和哈希', batchResponse, - Boolean(batchId) && batch?.status === 'SUBMITTED' && Number(batch?.batchVersion) === 1 - && snapshot.some?.((item) => id(item.documentId) === document2Id && Number(item.documentVersion) === 2 - && /^[0-9a-f]{64}$/.test(String(item.fileHash || ''))), - `batchId=${batchId || 'missing'}, frozenFiles=${snapshot.length}`, OPTIONS) - if (!batchId) return - - const batchReplay = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, batchPayload, - context.token, { 'Idempotency-Key': key(context, 'batch') }) - context.assert(MODULE, '代理提交幂等重放返回同一冻结批次', batchReplay, - codeOf(batchReplay) === '200' && id(batchReplay.body?.data?.id) === batchId, - `HTTP=${batchReplay.http}, code=${codeOf(batchReplay)}, batchId=${id(batchReplay.body?.data?.id) || 'missing'}`, OPTIONS) - - const staleSubmit = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/submission-batches`, { - ...batchPayload, externalReference: `EXT-DIFFERENT-${suffix}`, - }, context.token, { 'Idempotency-Key': key(context, 'batch') }) - context.addStep(MODULE, '同一幂等键不同代理提交请求被拒绝', staleSubmit, - staleSubmit.http === 200 && codeOf(staleSubmit) !== '200', - `HTTP=${staleSubmit.http}, code=${codeOf(staleSubmit)}`, OPTIONS) - - const receivedUpload = await upload(context, `${PLATFORM}/submission-batches/${batchId}/receipts/upload`, { - receiptType: 'RECEIVED', visibility: 'PLATFORM', note: context.marker, expectedVersion: 5, - }, `${context.marker} agent received`, `${context.marker}-received.txt`, context.token, key(context, 'receipt-received')) - const received = context.apiSuccess(MODULE, '上传平台可见代理接收回执', receivedUpload, '', OPTIONS) - const receivedId = id(received?.id) - context.assert(MODULE, '接收回执版本和平台可见范围正确', receivedUpload, - Boolean(receivedId) && received?.receiptType === 'RECEIVED' && received?.visibility === 'PLATFORM' - && Number(received?.receiptVersion) === 1, - `receiptId=${receivedId || 'missing'}, visibility=${received?.visibility || 'missing'}`, OPTIONS) - - const releaseUpload = await upload(context, `${PLATFORM}/submission-batches/${batchId}/receipts/upload`, { - receiptType: 'RELEASED', visibility: 'CUSTOMER', note: `${context.marker}-放行`, expectedVersion: 6, - }, `${context.marker} customs released`, `${context.marker}-released.txt`, context.token, key(context, 'receipt-released')) - const released = context.apiSuccess(MODULE, '上传客户可见海关放行回执', releaseUpload, '', OPTIONS) - const releasedId = id(released?.id) - context.assert(MODULE, '放行回执类型、版本和客户可见范围正确', releaseUpload, - Boolean(releasedId) && released?.receiptType === 'RELEASED' && released?.visibility === 'CUSTOMER' - && Number(released?.receiptVersion) === 1, - `receiptId=${releasedId || 'missing'}, visibility=${released?.visibility || 'missing'}`, OPTIONS) - if (!releasedId) return - - current = await detail(context, caseId) - context.assert(MODULE, '报关放行推进 CLEARANCE_COMPLETED 并保留完整稳定历史', current.response, - current.data?.customsCase?.status === 'CLEARANCE_COMPLETED' && Number(current.data?.customsCase?.version) === 7 - && current.data?.documents?.some?.((item) => id(item.id) === document2Id && item.status === 'FROZEN') - && current.data?.history?.some?.((item) => item.action === 'SUBMIT_AGENT_BATCH' && id(item.resourceId) === batchId) - && current.data?.history?.some?.((item) => item.action === 'UPLOAD_RECEIPT' && id(item.resourceId) === releasedId - && item.toStatus === 'CLEARANCE_COMPLETED' && Boolean(item.operatorId) && Boolean(item.createdAt)), - `status=${current.data?.customsCase?.status || 'missing'}, history=${current.data?.history?.length ?? 'n/a'}`, OPTIONS) - - const platformDownload = await download(context, `${PLATFORM}/cases/${caseId}/receipts/${releasedId}/download`, context.token) - context.assert(MODULE, '平台授权下载真实放行回执内容', platformDownload, - platformDownload.http === 200 && platformDownload.contentType.includes('text/plain') && platformDownload.bytes.length > 0, - `HTTP=${platformDownload.http}, bytes=${platformDownload.bytes.length}, contentType=${platformDownload.contentType || 'missing'}`, OPTIONS) - - const merchantDetailResponse = await request(context, 'GET', `${MERCHANT}/cases/${caseId}`, undefined, merchantToken) - const merchantDetail = context.apiSuccess(MODULE, '客户查询本企业报关进度和可见回执', merchantDetailResponse, '', OPTIONS) - context.assert(MODULE, '客户详情过滤平台回执并保留客户放行回执', merchantDetailResponse, - id(merchantDetail?.customsCase?.tenantId) === tenantId - && merchantDetail?.receipts?.some?.((item) => id(item.id) === releasedId) - && !merchantDetail?.receipts?.some?.((item) => id(item.id) === receivedId), - `receipts=${merchantDetail?.receipts?.length ?? 'n/a'}`, OPTIONS) - - const merchantDownload = await download(context, `${MERCHANT}/cases/${caseId}/receipts/${releasedId}/download`, merchantToken) - context.assert(MODULE, '客户授权下载客户可见放行回执', merchantDownload, - merchantDownload.http === 200 && merchantDownload.bytes.length > 0, - `HTTP=${merchantDownload.http}, bytes=${merchantDownload.bytes.length}`, OPTIONS) - - const hiddenDownload = await download(context, `${MERCHANT}/cases/${caseId}/receipts/${receivedId}/download`, merchantToken) - context.addStep(MODULE, '客户下载平台可见回执被拒绝', hiddenDownload, - hiddenDownload.http !== 200 || String(hiddenDownload.body?.code || '') !== '200', - `HTTP=${hiddenDownload.http}, code=${hiddenDownload.body?.code || 'binary'}, bytes=${hiddenDownload.bytes.length}`, OPTIONS) - - const forgedReference = await request(context, 'POST', `${PLATFORM}/cases/${caseId}/documents`, { - itemCode: 'INVOICE', fileId: released?.fileId, expectedVersion: 7, - }, context.token, { 'Idempotency-Key': key(context, 'forged-file-reference') }) - context.addStep(MODULE, '回执 fileId 不能伪造为客户报关资料引用', forgedReference, failure(forgedReference), - `HTTP=${forgedReference.http}, code=${codeOf(forgedReference)}`, OPTIONS) - - if (!(await verifyFourContainerDocuments(context, { containerBookingId, loadingPlanId, warehouseId }))) { - context.addStep(MODULE, '四类整柜资料必须完成后才允许进入出仓确认', null, false, - `containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}, warehouseId=${warehouseId}`, OPTIONS) + const customs = await completeContainerCustoms(context, loadingPlanId) + if (!customs) { + context.addStep(MODULE, '正式整柜报关聚合必须完成后才允许进入出仓确认', null, false, + `containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS) return } - context.createdResources.phase3Customs = { - marker: context.marker, tenantId, parentOrderId, orderId: actualOrderId, inboundId, containerBookingId, loadingPlanId, - customsCaseId: caseId, submissionBatchId: batchId, releasedReceiptId: releasedId, - finalStatus: 'CLEARANCE_COMPLETED', + marker: context.marker, tenantId, orderId: id(fixture?.orderId), inboundId, + containerBookingId, loadingPlanId, customsContainerId: id(customs.aggregateId), + finalStatus: 'COMPLETED', } } diff --git a/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs index b9c9d1c20..0e5fe00dd 100644 --- a/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-phase4-departure-pickup-chain-contract.test.mjs @@ -9,15 +9,19 @@ const read = (name) => readFileSync(resolve(here, name), 'utf8') const readFirstlegTest = (name) => readFileSync(resolve(here, '../../firstleg/src/test/java/com/baiyue/firstleg/portschedule/service', name), 'utf8') -test('current-marker departure batch proves four documents photos customs and container-level confirm', () => { +test('current-marker departure batch proves operational documents photos customs and container-level confirm', () => { const customs = read('phase3-customs.mjs') + const documents = read('phase3-container-documents.mjs') const outbound = read('phase3-outbound-dispatch.mjs') - for (const type of ['DESTINATION_CLEARANCE', 'CUSTOMS_DECLARATION', 'DELIVERY_ORDER', 'PICKUP_ORDER']) { + for (const type of ['DESTINATION_CLEARANCE', 'DELIVERY_ORDER', 'PICKUP_ORDER']) { assert.match(customs, new RegExp(type)) } - for (const evidence of ['首次上传', '回显', '下载', '更新', '完成']) assert.match(customs, new RegExp(evidence)) + for (const evidence of ['上传', '回读', '完成']) assert.match(documents, new RegExp(evidence)) + assert.match(customs, /只读复核前置模块已完成的整柜作业资料/) assert.match(customs, /photos\.length >= 5/) - assert.match(customs, /四类整柜资料必须完成后才允许进入出仓确认/) + assert.match(customs, /\/customs\/loading-plans/) + assert.match(customs, /complete-all-buy/) + assert.match(customs, /正式整柜报关聚合必须完成后才允许进入出仓确认/) assert.match(outbound, /CUSTOMS_CLEARANCE/) assert.match(outbound, /拖车与独立船期仅作聚合参考且不参与出仓门禁/) assert.match(outbound, /\$\{BASE\}\/\$\{encodeURIComponent\(containerBookingId\)\}\/confirm/) 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 3990c537e..4abe9c924 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 @@ -564,7 +564,8 @@ test('customs reuses the loading chain released booking without pre-completing t assertOrder(value, [ 'selectCustomsFlow(context.createdResources)', 'if (!await ensureReleasedOceanBooking(context, container)) return', - 'const slicesPath =', + 'if (!await verifyContainerOperationalDocuments(context, containerBookingId))', + 'const customs = await completeContainerCustoms(context, loadingPlanId)', ]) assert.match(value, /const oceanBookingId = id\(container\?\.oceanBookingId\)/) assert.match(value, /ocean-bookings\/\$\{encodeURIComponent\(oceanBookingId\)\}/) @@ -572,11 +573,10 @@ test('customs reuses the loading chain released booking without pre-completing t assert.doesNotMatch(value, /oceanBookings\.run\(context\)/) assert.doesNotMatch(value, /serviceCenter\.run\(context\)/) assert.doesNotMatch(value, /truck\?\.finalStatus !== 'COMPLETED'/) - assert.match(value, /request\('POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/) - assert.match(value, /category: 'CUSTOMS'/) - assert.match(value, /context\.createdResources\.phase3CustomsSupplier/) - assert.match(value, /const batchPayload = \{ customsAgentId,/) - assert.doesNotMatch(value, /customsAgentName: `代理-/) + assert.match(value, /PLATFORM_CUSTOMS = '\/api\/platform\/v1\/customs\/loading-plans'/) + assert.match(value, /complete-all-buy/) + assert.match(value, /finalStatus: 'COMPLETED'/) + assert.doesNotMatch(value, /checklist-versions|submission-batches|category: 'CUSTOMS'/) }) test('downstream container consumers prefer the phase3Loading fixture', () => { @@ -661,7 +661,8 @@ test('container fixture isolates order idempotency while publishing the run mark test('measurement fixture polls asynchronous completion before exporting loading identity', () => { const measurement = source('./inbound-measurement.mjs') assert.match(measurement, /waitForCompletedMeasurement/) - assert.match(measurement, /measurement\?\.processStatus === 'WAIT_SPLIT'/) + assert.match(measurement, /expectedStatuses\.includes\(measurement\?\.processStatus\)/) + assert.match(measurement, /completeDropshipPalletization/) assert.match(measurement, /setTimeout\(resolve, 1_000\)/) }) diff --git a/scripts/full-regression-entry-contract.test.mjs b/scripts/full-regression-entry-contract.test.mjs index 810d2da16..a02ed9c50 100644 --- a/scripts/full-regression-entry-contract.test.mjs +++ b/scripts/full-regression-entry-contract.test.mjs @@ -65,7 +65,7 @@ test('test-environment full acceptance keeps the DRAFT to RELEASED booking chain assert.doesNotMatch(bookingModule, /realContainerBookingId|CHANGE_PENDING|CANCELLED/) }) -test('test-environment loading chains push without a number and open PDA only after all prerequisites', () => { +test('test-environment loading chains create the bridge before opening PDA after all prerequisites', () => { const containerLoading = readFileSync( resolve(scriptsDir, 'admin-test-env-modules', 'phase3-container-loading.mjs'), 'utf8') const bridgeLoading = readFileSync( @@ -74,8 +74,8 @@ test('test-environment loading chains push without a number and open PDA only af assert.match(containerLoading, /oceanBookingId: bookingId, requirementId: id\(requirement\.id\), containerNo: realContainerNo/) assert.doesNotMatch(containerLoading, /realContainerBookingId/) - const pushIndex = bridgeLoading.indexOf('无真实柜号推送预装成功并冻结柜次明细') - const absentIndex = bridgeLoading.indexOf('无号 push 成功后 PDA 查询明确 A0404') + const pushIndex = bridgeLoading.indexOf('分配同步建立 bridge 关联') + const absentIndex = bridgeLoading.indexOf('无真实柜号时 PDA 查询明确 A0404') const releaseIndex = bridgeLoading.indexOf('三期订舱 RELEASED 冻结运输快照') const linkIndex = bridgeLoading.indexOf('模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号') const confirmIndex = bridgeLoading.indexOf('计划级原子确认整份预装并触发唯一 PDA 任务') @@ -83,14 +83,14 @@ test('test-environment loading chains push without a number and open PDA only af assert.ok(pushIndex >= 0 && absentIndex > pushIndex && releaseIndex > absentIndex && linkIndex > releaseIndex && confirmIndex > linkIndex && pdaIndex > confirmIndex) assert.match(bridgeLoading, /codeOf\(missingContainerTaskResponse\) === 'A0404'/) - assert.match(bridgeLoading, /push exact replay 对计划版本历史关联和任务零漂移/) + assert.match(bridgeLoading, /分配自动建立唯一预装关联且身份稳定/) assert.match(bridgeLoading, /bookingConfirmed\?\.oceanBooking\?\.status === 'RELEASED'/) assert.match(bridgeLoading, /loading-plans\/\$\{loadingPlanId\}\/confirm/) assert.match(bridgeLoading, /readiness\?\.readyForPda === true/) assert.match(bridgeLoading, /计划级确认同 key 异参返回冲突[\s\S]*已确认计划使用新 key 重复确认返回冲突/) assert.doesNotMatch(bridgeLoading, /\/preload\/confirming|\/preload\/confirm\?tenantId=/) - const containerPushIndex = containerLoading.indexOf('无真实柜号推送预装计划成功') + const containerPushIndex = containerLoading.indexOf('分配同步建立 bridge') const containerReleaseIndex = containerLoading.indexOf('订舱放舱为 RELEASED 且不预生成真实货柜') const containerLinkIndex = containerLoading.indexOf('首次关联 RELEASED 订舱需求并补真实柜号') const containerConfirmIndex = containerLoading.indexOf('计划级原子确认整份预装并触发唯一 PDA 任务')