diff --git a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs index f0aea53a7..c341f861e 100644 --- a/scripts/admin-test-env-modules/manual-state-loading-pda.mjs +++ b/scripts/admin-test-env-modules/manual-state-loading-pda.mjs @@ -101,10 +101,14 @@ async function createConfirmedContainer(context, source, label) { const draft = { customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户', channelCode: summary.channelCode, warehouseId: source.warehouseId, - warehouseName: summary.warehouseName, shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`, + warehouseName: summary.warehouseName, billOfLadingNo: `BL-${value}`.slice(0, 128), + shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`, bookingAgentId: null, bookingAgentName: `人工代理-${value}`, departurePortCode: 'CNSZX', + vesselName: `人工船-${value}`.slice(0, 128), voyageNo: `V-${value}`.slice(0, 64), departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', - placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: `${context.marker}-${label}`, + placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', + estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00', + remark: `${context.marker}-${label}`, parties: [{ partyType: 'SHIPPER', partyName: `发货方-${value}` }, { partyType: 'CONSIGNEE', partyName: `收货方-${value}` }, { partyType: 'NOTIFY_PARTY', partyName: `通知方-${value}` }], @@ -122,12 +126,10 @@ async function createConfirmedContainer(context, source, label) { { expectedVersion: 0, data: draft }, `${label}-booking-create`) const created = context.apiSuccess(MODULE, `${label} 创建订舱`, createdResponse, '', OPTIONS) const bookingId = id(created?.oceanBooking?.id) - const submittedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/submit`, - { expectedVersion: Number(created?.oceanBooking?.version), reason: context.marker }, `${label}-booking-submit`) - const submitted = context.apiSuccess(MODULE, `${label} 提交订舱`, submittedResponse, '', OPTIONS) - const confirmedResponse = await post(context, `/api/platform/v1/ocean-bookings/${bookingId}/confirm`, - { expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker }, `${label}-booking-confirm`) - const confirmed = context.apiSuccess(MODULE, `${label} 确认订舱`, confirmedResponse, '', OPTIONS) + const confirmed = created + context.assert(MODULE, `${label} 一次登记直接返回 CONFIRMED`, createdResponse, + confirmed?.oceanBooking?.status === 'CONFIRMED', + `status=${confirmed?.oceanBooking?.status || 'missing'}`, OPTIONS) const requirement = confirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ') if (!requirement?.id) throw new Error(`${label} 确认订舱未返回箱型需求`) const linkedResponse = await context.request('POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, { diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs index e97e71f3d..ae7e0b276 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking-contract.test.mjs @@ -4,27 +4,24 @@ import test from 'node:test' const source = readFileSync(new URL('./manual-state-ocean-trucking.mjs', import.meta.url), 'utf8') -test('retains only public ocean booking list states while hidden states stay detail-only', () => { +test('retains only operational ocean booking states from direct CONFIRMED registration', () => { assert.match(source, /const OCEAN_BASE = ['"]\/api\/platform\/v1\/ocean-bookings['"]/) assert.doesNotMatch(source, /\/api\/platform\/v1\/first-leg\/ocean-bookings/) - for (const status of ['DRAFT','SUBMITTED','CONFIRMED','RELEASED','FAILED','CHANGE_PENDING','CANCELLED', + for (const status of ['CONFIRMED','RELEASED','CHANGE_PENDING','CANCELLED', 'PENDING_DISPATCH','DISPATCHED','PICKUP_IN_PROGRESS','ARRIVED_WAREHOUSE','LOADING','DEPARTED', 'COMPLETED','RESCHEDULED','EXCEPTION']) assert.match(source, new RegExp(`['"]${status}['"]`)) assert.match(source, /change-request/) assert.match(source, /const OCEAN_LIST_STATUSES = \['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'\]/) - assert.match(source, /const OCEAN_HIDDEN_STATUSES = \['DRAFT', 'SUBMITTED', 'FAILED'\]/) - assert.match(source, /hiddenFixtures/) - assert.match(source, /code\(listResponse\) === 'A0400'/) - assert.match(source, /后台样本仅通过详情契约验证/) - assert.doesNotMatch(source, /code\(listResponse\) !== 'A0400'\) return/) + assert.match(source, /const OCEAN_STATUSES = \[\.\.\.OCEAN_LIST_STATUSES\]/) + assert.doesNotMatch(source, /OCEAN_HIDDEN_STATUSES|hiddenFixtures|verifyHiddenOcean/) + assert.match(source, /一次创建直接返回 CONFIRMED/) + assert.doesNotMatch(source, /ocean-bookings\/\$\{[^}]+\}\/submit|ocean-bookings\/\$\{[^}]+\}\/confirm/) const draftPayload = source.slice(source.indexOf('function oceanPayload'), source.indexOf('async function createOcean')) for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { - assert.doesNotMatch(draftPayload, new RegExp(`${field}:`)) + assert.match(draftPayload, new RegExp(`${field}:`)) } const lifecycle = source.slice(source.indexOf('async function buildOcean'), source.indexOf('function truckPayload')) - for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { - assert.match(lifecycle, new RegExp(`${field}:`)) - } + assert.doesNotMatch(lifecycle, /'submit'|'confirm'|'fail'/) assert.match(source, /\/reschedule\?plannedLoadingAt=/) assert.match(source, /page=1&size=100&status=/) assert.match(source, /customerId: oceanBooking\.customerId/) diff --git a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs index 212588bdc..0ee369772 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs @@ -3,8 +3,7 @@ const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBloc const OCEAN_BASE = '/api/platform/v1/ocean-bookings' const TRUCK_BASE = '/api/platform/v1/truck-appointments' const OCEAN_LIST_STATUSES = ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'] -const OCEAN_HIDDEN_STATUSES = ['DRAFT', 'SUBMITTED', 'FAILED'] -const OCEAN_STATUSES = ['DRAFT', 'SUBMITTED', 'CONFIRMED', 'RELEASED', 'FAILED', 'CHANGE_PENDING', 'CANCELLED'] +const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES] const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE', 'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED'] @@ -24,12 +23,15 @@ function oceanPayload(context, template, index) { customerId: source.customerId, customerName: source.customerName, channelCode: source.channelCode, warehouseId: source.warehouseId, warehouseName: source.warehouseName, + billOfLadingNo: `BL-${suffix(context, index)}`, vesselName: `人工轮-${suffix(context, index)}`, + voyageNo: `V${suffix(context, index)}`, shippingCompanyId: source.shippingCompanyId, shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId, bookingAgentName: source.bookingAgentName, departurePortCode: source.departurePortCode, departurePortName: source.departurePortName, destinationPortCode: source.destinationPortCode, destinationPortName: source.destinationPortName, placeOfDelivery: source.placeOfDelivery, serviceTerm: source.serviceTerm, freightTerm: source.freightTerm, + estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00', remark: `${context.marker} 人工列表状态 ${index}`, parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) => ({ partyType, partyName, address, contactName, contactPhone })), @@ -41,7 +43,12 @@ function oceanPayload(context, template, index) { async function createOcean(context, template, index) { const response = await post(context, OCEAN_BASE, oceanPayload(context, template, index), `ocean-create-${index}`) const value = context.apiSuccess(MODULE, `创建订舱状态样本 ${index}`, response, '', OPTIONS) - if (!value?.oceanBooking?.id) throw new Error(`创建订舱样本 ${index} 失败`) + context.assert(MODULE, `订舱样本 ${index} 一次创建直接返回 CONFIRMED`, response, + Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'CONFIRMED', + `id=${id(value?.oceanBooking?.id) || 'missing'}, status=${value?.oceanBooking?.status || 'missing'}`, OPTIONS) + if (!value?.oceanBooking?.id || value?.oceanBooking?.status !== 'CONFIRMED') { + throw new Error(`创建订舱样本 ${index} 未直接进入 CONFIRMED`) + } return value } @@ -55,13 +62,8 @@ async function oceanAction(context, value, endpoint, body = {}) { async function buildOcean(context, template, status, index) { let value = await createOcean(context, template, index) - if (status === 'DRAFT') return value - value = await oceanAction(context, value, 'submit') - if (status === 'SUBMITTED') return value - if (status === 'FAILED') return oceanAction(context, value, 'fail') - if (status === 'CANCELLED') return oceanAction(context, value, 'cancel') - value = await oceanAction(context, value, 'confirm') if (status === 'CONFIRMED') return value + if (status === 'CANCELLED') return oceanAction(context, value, 'cancel') if (status === 'CHANGE_PENDING') return oceanAction(context, value, 'change-request', { vesselName: `变更轮-${suffix(context, index)}`, voyageNo: `CV${suffix(context, index)}`, estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00' }) @@ -130,19 +132,6 @@ async function verify(context, base, status, resourceId) { `status=${status}, id=${resourceId}`, OPTIONS) } -async function verifyHiddenOcean(context, status, resourceId) { - const listResponse = await context.request('GET', `${OCEAN_BASE}?page=1&size=100&status=${status}`) - context.assert(MODULE, `${status} 不进入正式订舱列表筛选`, listResponse, - listResponse?.http === 200 && code(listResponse) === 'A0400', - `HTTP=${listResponse?.http ?? 'missing'}, code=${code(listResponse)}, id=${resourceId}`, OPTIONS) - - const detailResponse = await context.request('GET', `${OCEAN_BASE}/${resourceId}`) - const detail = context.apiSuccess(MODULE, `${status} 后台样本仅通过详情契约验证`, detailResponse, '', OPTIONS) - context.assert(MODULE, `${status} 后台详情身份与状态正确`, detailResponse, - id(detail?.oceanBooking?.id) === resourceId && detail?.oceanBooking?.status === status, - `id=${id(detail?.oceanBooking?.id) || 'missing'}, actual=${detail?.oceanBooking?.status || 'missing'}`, OPTIONS) -} - export async function run(context, matrix = context.createdResources.manualStateMatrix) { const sourceOceanId = id(context.createdResources?.oceanBooking?.id) if (!sourceOceanId) throw new Error('缺少现有订舱生命周期模块生成的模板资源') @@ -151,7 +140,7 @@ export async function run(context, matrix = context.createdResources.manualState if (!template?.oceanBooking || !template?.parties?.length || !template?.containerRequirements?.length) { throw new Error('订舱模板缺少渠道、仓库、参与方或箱型需求') } - matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {}, hiddenFixtures: {} } + matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {} } for (const [index, status] of OCEAN_STATUSES.entries()) { const value = await buildOcean(context, template, status, index) const resourceId = id(value?.oceanBooking?.id); const actual = value?.oceanBooking?.status @@ -163,11 +152,6 @@ export async function run(context, matrix = context.createdResources.manualState customerId: id(value?.oceanBooking?.customerId), customerName: value?.oceanBooking?.customerName, warehouseId: id(value?.oceanBooking?.warehouseId), } - if (OCEAN_HIDDEN_STATUSES.includes(status)) { - matrix.pages.oceanBookings.hiddenFixtures[status] = fixtureValue - await verifyHiddenOcean(context, status, resourceId) - continue - } matrix.pages.oceanBookings.fixtures[status] = { ...fixtureValue, detailPath: `/ocean-bookings/${resourceId}`, } diff --git a/scripts/admin-test-env-modules/ocean-booking-channel-contract.test.mjs b/scripts/admin-test-env-modules/ocean-booking-channel-contract.test.mjs index 9aab72802..c5ff0cf13 100644 --- a/scripts/admin-test-env-modules/ocean-booking-channel-contract.test.mjs +++ b/scripts/admin-test-env-modules/ocean-booking-channel-contract.test.mjs @@ -41,7 +41,7 @@ test('backend OpenAPI freezes a platform channel query scoped by originWarehouse test('explicit real booking association locks the selected channel onto the simulation', () => { const fixture = source(new URL('./phase3-container-loading.mjs', import.meta.url)) assert.match(fixture, /ocean-booking-link/) - assert.match(fixture, /CONFIRMED 只产生真实订舱事实且尚未占用模拟柜/) + assert.match(fixture, /一次登记直接返回 CONFIRMED 且尚未占用模拟柜/) assert.match(fixture, /containerDetail\?\.channelCode === channelCode/) assert.match(fixture, /selectPlatformChannelIsolationPair/) assert.doesNotMatch(fixture, /\$\{channelCode\}-OTHER|foreignChannelCode/) diff --git a/scripts/admin-test-env-modules/ocean-booking-fixture-contract.test.mjs b/scripts/admin-test-env-modules/ocean-booking-fixture-contract.test.mjs index 5a50432e3..719181310 100644 --- a/scripts/admin-test-env-modules/ocean-booking-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/ocean-booking-fixture-contract.test.mjs @@ -31,7 +31,7 @@ test('ocean booking fixture blocks creation instead of inventing missing master assert.match(steps[0][4], /missing=customerId,customerName,channelCode,channelName,warehouseId,warehouseName/) }) -test('shared booking draft omits server-owned channelName and release transport data', () => { +test('shared booking data omits server-owned channelName and includes offline-confirmed transport snapshots', () => { const draft = oceanBookingDraft({ marker: 'ADMIN-RUN-3' }, '3', { customerId: '21', customerName: 'IT 测试客户', channelCode: 'SEA-US', channelName: '美西海运', warehouseId: '41', warehouseName: '深圳仓', @@ -44,13 +44,15 @@ test('shared booking draft omits server-owned channelName and release transport assert.equal(draft.freightTerm, 'NINGBO') assert.equal(draft.containerRequirements[0].capacityCbm, 68) assert.equal(draft.containerRequirements[0].maxWeightKg, 28000) - for (const key of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { - assert.equal(Object.hasOwn(draft, key), false, `${key} belongs to release, not draft`) - } + assert.equal(draft.billOfLadingNo, 'BL-3') + assert.equal(draft.vesselName, '验收船-3') + assert.equal(draft.voyageNo, 'V-3') + assert.equal(draft.estimatedSailingAt, '2026-09-01T08:00:00') + assert.equal(draft.estimatedArrivalAt, '2026-09-20T08:00:00') const fixtureSource = readFileSync(new URL('./ocean-booking-fixture.mjs', import.meta.url), 'utf8') const builderSource = fixtureSource.slice(fixtureSource.indexOf('export function oceanBookingDraft'), fixtureSource.indexOf('export function oceanBookingDraftWithoutCustomer')) for (const key of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { - assert.doesNotMatch(builderSource, new RegExp(`${key}\\s*:`), `${key} must not be emitted by the shared draft builder`) + assert.match(builderSource, new RegExp(`${key}\\s*:`), `${key} must be emitted by the confirmed booking builder`) } }) @@ -101,11 +103,22 @@ test('all direct ocean booking payload builders omit channelName under the froze const value = readFileSync(new URL(name, import.meta.url), 'utf8') const region = value.slice(value.indexOf(start), value.indexOf(end)) assert.doesNotMatch(region, /channelName\s*:/, name) + for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { + assert.match(region, new RegExp(`${field}\\s*:`), `${name} must register ${field}`) + } } const container = readFileSync(new URL('phase3-container-loading.mjs', import.meta.url), 'utf8') assert.doesNotMatch(container, /data:\s*\{[^}]*channelName\s*:/s) }) +test('ocean booking producers register CONFIRMED data directly without draft submit or confirm transitions', () => { + for (const name of ['ocean-bookings.mjs', 'phase3-loading.mjs', 'phase3-container-loading.mjs', + 'phase3-order-splitting.mjs', 'manual-state-loading-pda.mjs', 'manual-state-ocean-trucking.mjs']) { + const source = readFileSync(new URL(name, import.meta.url), 'utf8') + assert.doesNotMatch(source, /ocean-bookings\/[^`'"\\s]+\/(?:submit|confirm)|booking-(?:submit|confirm)/, name) + } +}) + test('all independent ocean booking producers use the shared master-data contract', () => { for (const name of ['ocean-bookings.mjs', 'phase3-port-schedules.mjs', 'phase3-carrier-schedule-queries.mjs']) { const source = readFileSync(new URL(name, import.meta.url), 'utf8') diff --git a/scripts/admin-test-env-modules/ocean-booking-fixture.mjs b/scripts/admin-test-env-modules/ocean-booking-fixture.mjs index 8879ac72c..50fa4c3ac 100644 --- a/scripts/admin-test-env-modules/ocean-booking-fixture.mjs +++ b/scripts/admin-test-env-modules/ocean-booking-fixture.mjs @@ -56,10 +56,14 @@ export function oceanBookingDraft(context, suffix, master, labels = {}) { const { channelName: _readOnlyChannelName, ...writableMaster } = master return { ...writableMaster, + billOfLadingNo: labels.billOfLadingNo || `BL-${suffix}`, shippingCompanyId: null, shippingCompanyName: labels.shippingCompanyName || `验收船公司-${suffix}`, bookingAgentId: null, bookingAgentName: labels.bookingAgentName || `验收代理-${suffix}`, + vesselName: labels.vesselName || `验收船-${suffix}`, voyageNo: labels.voyageNo || `V-${suffix}`, departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', + estimatedSailingAt: labels.estimatedSailingAt || '2026-09-01T08:00:00', + estimatedArrivalAt: labels.estimatedArrivalAt || '2026-09-20T08:00:00', remark: context.marker, parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` }, diff --git a/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs b/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs index c5d2b3a19..622a838d9 100644 --- a/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs +++ b/scripts/admin-test-env-modules/ocean-booking-prototype-contract.test.mjs @@ -61,18 +61,24 @@ test('customer is optional across Admin DraftData DTO and OpenAPI', () => { assert.match(openapi, /customerName: \{[^\n]*nullable: true/) }) -test('draft save omits release-only transport values while release still requires all five', () => { +test('offline-confirmed registration requires transport values while release keeps the frozen five-field snapshot', () => { const draftType = adminTypes.slice(adminTypes.indexOf('export interface OceanBookingDraftData'), adminTypes.indexOf('export interface OceanBookingResource')) for (const field of releaseFields) { - assert.doesNotMatch(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`), - `${field} must be read-only outside the release dialog`) - assert.doesNotMatch(draftType, new RegExp(`${field}\\??:`), `${field} must not belong to OceanBookingDraftData`) - assert.match(adminPage, new RegExp(`(?:v-model="releaseForm\\.${field}"|releaseForm\\.${field})`)) + assert.match(adminPage, new RegExp(`v-model="form\\.data\\.${field}"`), + `${field} must be entered when registering an offline-confirmed booking`) + assert.match(draftType, new RegExp(`${field}\\??:`), `${field} must belong to OceanBookingDraftData`) } - assert.match(adminTest, /\['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'\]/) - assert.match(adminTest, /not\.toContain\(`v-model="form\.data\.\$\{field\}"`\)/) + assert.match(adminTest, /线下订舱资料已登记并确认/) + assert.match(adminTest, /登记后直接确认/) const saveRegion = adminPage.slice(adminPage.indexOf('async function save'), adminPage.indexOf('function requireReason')) assert.doesNotMatch(saveRegion, /data:\s*form\.data/) + const required = openapi.match(/DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/)?.[1] || '' + for (const field of releaseFields) assert.match(required, new RegExp(field)) + assert.match(dto, /@NotBlank @Size\(max = 128\) String billOfLadingNo/) + assert.match(dto, /@NotBlank @Size\(max = 128\) String vesselName/) + assert.match(dto, /@NotBlank @Size\(max = 64\) String voyageNo/) + assert.match(dto, /@NotNull LocalDateTime estimatedSailingAt/) + assert.match(dto, /@NotNull LocalDateTime estimatedArrivalAt/) const releaseCall = adminPage.slice(adminPage.indexOf('releaseOceanBooking(id.value'), adminPage.indexOf('releaseDialogOpen.value = false')) for (const field of releaseFields) assert.match(releaseCall, new RegExp(field)) }) diff --git a/scripts/admin-test-env-modules/ocean-bookings.mjs b/scripts/admin-test-env-modules/ocean-bookings.mjs index a2eb7ed68..ba8042422 100644 --- a/scripts/admin-test-env-modules/ocean-bookings.mjs +++ b/scripts/admin-test-env-modules/ocean-bookings.mjs @@ -33,22 +33,7 @@ async function createConfirmedUnassignedBooking(context, suffix, master) { `bookingId=${bookingId || 'missing'}, customerId=${id(created?.oceanBooking?.customerId) || 'null'}, customerName=${created?.oceanBooking?.customerName || 'null'}`, OPTIONS) if (!bookingId) return null - const hiddenResponse = await request(context, 'GET', `${BASE}?page=1&size=20&keyword=${encodeURIComponent(createdBookingNo)}`) - const hiddenPage = context.apiSuccess(MODULE, '未确认订舱不进入正式订舱列表', hiddenResponse, '', OPTIONS) - context.assert(MODULE, 'DRAFT 未指定客户订舱仅可按详情维护', hiddenResponse, - !hiddenPage?.records?.some?.((item) => id(item.id) === bookingId), - `bookingId=${bookingId}, records=${hiddenPage?.records?.length ?? 'n/a'}`, OPTIONS) - const path = `${BASE}/${bookingId}` - const submitResponse = await request(context, 'POST', `${path}/submit`, { - expectedVersion: Number(created.oceanBooking.version), reason: context.marker, - }, { 'Idempotency-Key': `${context.marker}-ocean-unassigned-submit`.slice(0, 128) }) - const submitted = context.apiSuccess(MODULE, '提交未指定客户订舱', submitResponse, '', OPTIONS) - const confirmResponse = await request(context, 'POST', `${path}/confirm`, { - expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker, - }, { 'Idempotency-Key': `${context.marker}-ocean-unassigned-confirm`.slice(0, 128) }) - context.apiSuccess(MODULE, '确认未指定客户真实订舱', confirmResponse, '', OPTIONS) - const detailResponse = await request(context, 'GET', path) const detail = context.apiSuccess(MODULE, '回查未指定客户订舱持久化结果', detailResponse, '', OPTIONS) const bookingNo = id(detail?.oceanBooking?.bookingNo || created?.oceanBooking?.bookingNo) @@ -135,29 +120,23 @@ export async function run(context) { const createResponse = await createBooking(context, suffix, key, master) const detail = data(createResponse); const bookingId = id(detail?.oceanBooking?.id) context.apiSuccess(MODULE, '创建隔离海运订舱', createResponse, `key=${key}`, OPTIONS) - context.assert(MODULE, '订舱 DTO 字段、DRAFT 和9类关联字段完整', createResponse, Boolean(bookingId) && detail?.oceanBooking?.status === 'DRAFT' && detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1, `bookingId=${bookingId || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, requirements=${detail?.containerRequirements?.length ?? 'n/a'}`, OPTIONS) + context.assert(MODULE, '线下已确认订舱 DTO、运输快照和关联字段完整', createResponse, + Boolean(bookingId) && detail?.oceanBooking?.status === 'CONFIRMED' + && detail?.parties?.length === 3 && detail?.containerRequirements?.length === 1 + && ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'] + .every((field) => Boolean(detail?.oceanBooking?.[field])), + `bookingId=${bookingId || 'missing'}, status=${detail?.oceanBooking?.status || 'missing'}, parties=${detail?.parties?.length ?? 'n/a'}, requirements=${detail?.containerRequirements?.length ?? 'n/a'}`, OPTIONS) context.assert(MODULE, '指定客户订舱持久化本轮客户范围快照', createResponse, id(detail?.oceanBooking?.customerId) === id(master.customerId) && detail?.oceanBooking?.customerName === master.customerName, `expectedCustomerId=${id(master.customerId)}, actualCustomerId=${id(detail?.oceanBooking?.customerId) || 'missing'}`, OPTIONS) if (!bookingId) return context.createdResources.oceanBooking = { id: bookingId, customerId: master.customerId, marker: context.marker, - containerBookingId: null, loadingPlanId: null, finalStatus: 'DRAFT' } + containerBookingId: null, loadingPlanId: null, finalStatus: 'CONFIRMED' } const replay = await createBooking(context, suffix, key, master) context.assert(MODULE, '创建 Idempotency-Key 重放返回同一订舱', replay, String(data(replay)?.oceanBooking?.id) === bookingId, `first=${bookingId}, replay=${id(data(replay)?.oceanBooking?.id)}`, OPTIONS) const path = `${BASE}/${bookingId}` - const initialVersion = Number(detail.oceanBooking.version) - const edited = { ...oceanBookingDraft(context, suffix, master), shippingCompanyName: `编辑船司-${suffix}` } - const update = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: edited }, { 'Idempotency-Key': `${context.marker}-ocean-edit` }) - context.apiSuccess(MODULE, '编辑 DRAFT 订舱', update, `version=${initialVersion}`, OPTIONS) - const stale = await request(context, 'PUT', path, { expectedVersion: initialVersion, data: { ...edited, shippingCompanyName: `STALE-${suffix}` } }, { 'Idempotency-Key': `${context.marker}-ocean-stale` }) - step(context, '订舱旧版本编辑返回冲突', stale, stale.http === 200 && ['A0409', '409'].includes(codeOf(stale)), `HTTP=${stale.http}, code=${codeOf(stale)}`) - const afterStale = await request(context, 'GET', path); const afterData = data(afterStale) - context.apiSuccess(MODULE, '冲突后回查订舱', afterStale, '', OPTIONS) - context.assert(MODULE, '订舱冲突未部分更新', afterStale, afterData?.oceanBooking?.shippingCompanyName === edited.shippingCompanyName, `shippingCompanyName=${afterData?.oceanBooking?.shippingCompanyName || 'missing'}`, OPTIONS) - let version = Number(afterData?.oceanBooking?.version) + let version = Number(detail.oceanBooking.version) const action = async (name, endpoint, expected, reason) => { const r = await request(context, 'POST', `${path}/${endpoint}`, { expectedVersion: expected, reason }, { 'Idempotency-Key': `${context.marker}-ocean-${endpoint}` }); const d = data(r); context.apiSuccess(MODULE, name, r, `status=${d?.oceanBooking?.status || 'missing'}`, OPTIONS); version = Number(d?.oceanBooking?.version); return d } - await action('提交订舱', 'submit', version, 'marker submit') - await action('确认订舱', 'confirm', version, 'marker confirm') const changed = await request(context, 'POST', `${path}/change-request`, { expectedVersion: version, vesselName: `改船-${suffix}`, voyageNo: `CV${suffix}`, reason: context.marker }, { 'Idempotency-Key': `${context.marker}-change` }) context.apiSuccess(MODULE, '申请改船', changed, '', OPTIONS); version = Number(data(changed)?.oceanBooking?.version) const changeConfirmed = await action('确认改船', 'change-confirm', version, 'confirm change') @@ -185,7 +164,7 @@ export async function run(context) { const final = await request(context, 'GET', path); const fd = data(final) context.apiSuccess(MODULE, '回查 RELEASED 订舱详情', final, '', OPTIONS) context.assert(MODULE, '独立订舱状态历史、审计操作者和终态完整', final, - fd?.oceanBooking?.status === 'RELEASED' && fd.histories?.length >= 5 && fd.histories.every((h) => h.operatorName) + fd?.oceanBooking?.status === 'RELEASED' && fd.histories?.length >= 4 && fd.histories.every((h) => h.operatorName) && fd?.containerAllocations?.length === 0, `status=${fd?.oceanBooking?.status || 'missing'}, histories=${fd?.histories?.length ?? 'n/a'}, allocations=${fd?.containerAllocations?.length ?? 'n/a'}`, OPTIONS) let downstreamBooking = { diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index d8b7c2597..df4cd8986 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -115,11 +115,14 @@ export async function run(context) { const bookingDraft = { customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`, channelCode, warehouseId, warehouseName, + billOfLadingNo: `BL-${suffix}`.slice(0, 128), shippingCompanyId: null, shippingCompanyName: `验收船公司-${suffix}`, bookingAgentId: null, bookingAgentName: `验收代理-${suffix}`, + vesselName: `验收船-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64), departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', - serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: context.marker, + serviceTerm: 'CY-CY', freightTerm: 'NINGBO', estimatedSailingAt: '2026-09-01T08:00:00', + estimatedArrivalAt: '2026-09-20T08:00:00', remark: context.marker, parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` }, { partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }], @@ -232,32 +235,12 @@ export async function run(context) { const bookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', { expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-loading-booking-create` }) - const bookingCreated = context.apiSuccess(MODULE, '创建含渠道仓库快照的真实订舱草稿', bookingCreate, '', OPTIONS) + const bookingCreated = context.apiSuccess(MODULE, '登记含渠道仓库和运输快照的已确认真实订舱', bookingCreate, '', OPTIONS) const bookingId = id(bookingCreated?.oceanBooking?.id) const bookingNo = id(bookingCreated?.oceanBooking?.bookingNo) if (!bookingId) return - const hiddenDraftResponse = await context.request('GET', - `/api/platform/v1/ocean-bookings?page=1&size=20&keyword=${encodeURIComponent(bookingNo)}`) - const hiddenDraftPage = context.apiSuccess(MODULE, '正式订舱列表查询不含草稿', hiddenDraftResponse, '', OPTIONS) - context.assert(MODULE, '真实订舱确认前不进入正式列表', hiddenDraftResponse, - !records(hiddenDraftPage).some((item) => id(item.id) === bookingId), - `bookingId=${bookingId}, records=${records(hiddenDraftPage).length}`, OPTIONS) - const draftFilterResponse = await request(context, 'GET', '/api/platform/v1/ocean-bookings?page=1&size=5&status=DRAFT') - context.addStep(MODULE, '正式订舱列表拒绝 DRAFT 状态筛选', draftFilterResponse, - failure(draftFilterResponse), `HTTP=${draftFilterResponse.http}, code=${codeOf(draftFilterResponse)}`, OPTIONS) - - const submit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`, - { expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker }, - { 'Idempotency-Key': `${context.marker}-loading-booking-submit` }) - const submitted = context.apiSuccess(MODULE, '提交隔离订舱', submit, '', OPTIONS) - const submittedFilterResponse = await request(context, 'GET', '/api/platform/v1/ocean-bookings?page=1&size=5&status=SUBMITTED') - context.addStep(MODULE, '正式订舱列表拒绝 SUBMITTED 状态筛选', submittedFilterResponse, - failure(submittedFilterResponse), `HTTP=${submittedFilterResponse.http}, code=${codeOf(submittedFilterResponse)}`, OPTIONS) - const confirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`, - { expectedVersion: Number(submitted?.oceanBooking?.version), reason: context.marker }, - { 'Idempotency-Key': `${context.marker}-loading-booking-confirm` }) - const confirmedBooking = context.apiSuccess(MODULE, '确认真实订舱但不自动创建内部柜', confirm, '', OPTIONS) - context.assert(MODULE, 'CONFIRMED 只产生真实订舱事实且尚未占用模拟柜', confirm, + const confirmedBooking = bookingCreated + context.assert(MODULE, '一次登记直接返回 CONFIRMED 且尚未占用模拟柜', bookingCreate, confirmedBooking?.oceanBooking?.status === 'CONFIRMED' && Array.isArray(confirmedBooking?.containerAllocations) && confirmedBooking.containerAllocations.length === 0, `status=${confirmedBooking?.oceanBooking?.status || 'missing'}, allocations=${confirmedBooking?.containerAllocations?.length ?? 'n/a'}`, OPTIONS) @@ -387,7 +370,7 @@ export async function run(context) { voyageNo: `V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt, estimatedArrivalAt: arrivalAt, reason: context.marker, }, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) }) - const releasedBooking = context.apiSuccess(MODULE, '订舱五项运输字段放舱并满足 PDA 订舱门禁', releaseResponse, '', OPTIONS) + const releasedBooking = context.apiSuccess(MODULE, '订舱携带已冻结运输快照放舱并满足 PDA 门禁', releaseResponse, '', OPTIONS) context.assert(MODULE, '放舱后订舱进入 RELEASED 且柜次分配保持稳定', releaseResponse, releasedBooking?.oceanBooking?.status === 'RELEASED' && releasedBooking?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerId), diff --git a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs index b05d3c69d..0b90ed253 100644 --- a/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-container-regression-contract.test.mjs @@ -22,14 +22,16 @@ const containerLoadingAcceptance = read('phase3-container-loading.mjs') const truckAcceptance = read('truck-appointments.mjs') const matrix = read('../../../docs/三期/货柜管理全链验收矩阵-2026-08-13.md') -test('release request freezes all five transport fields and UI blocks incomplete submission', () => { +test('registration freezes all five transport fields and release reuses the confirmed snapshot', () => { assert.match(oceanOpenApi, /required: \[expectedVersion, billOfLadingNo, vesselName, voyageNo, estimatedSailingAt, estimatedArrivalAt\]/) for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { assert.match(releaseTypes, new RegExp(`${field}: string`), field) - assert.match(releaseView, new RegExp(`releaseForm\\.${field}`), field) + assert.match(releaseView, new RegExp(`form\\.data\\.${field}`), field) + assert.match(releaseView, new RegExp(`detail\\.value\\.${field}`), field) assert.match(oceanAcceptance, new RegExp(`${field}:`), field) } - assert.match(releaseView, /if \(!billOfLadingNo \|\| !vesselName \|\| !voyageNo \|\| !releaseForm\.estimatedSailingAt \|\| !releaseForm\.estimatedArrivalAt\)/) + assert.match(releaseView, /当前订舱缺少已确认运输资料,无法放舱/) + assert.match(releaseView, /放舱不会重新采集或修改这些信息/) }) test('overview regression requires workflowStatus and distinguishes 100 percent awaiting review', () => { @@ -88,7 +90,7 @@ test('PDA task opens only after preload push, real container number and ocean re assert.match(containerLoadingAcceptance, /version: Number\(linkedContainer\.version\), reason: context\.marker/) assert.match(containerLoadingAcceptance, /订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务/) assert.match(containerLoadingAcceptance, /ocean-bookings\/\$\{bookingId\}\/release/) - assert.match(containerLoadingAcceptance, /订舱五项运输字段放舱并满足 PDA 订舱门禁/) + assert.match(containerLoadingAcceptance, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/) assert.match(containerLoadingAcceptance, /柜号与 RELEASED 订舱就绪后推送预装计划/) assert.match(containerLoadingAcceptance, /releasedBooking\?\.oceanBooking\?\.status === 'RELEASED'/) }) diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index b32803209..b67220224 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -121,9 +121,12 @@ export async function run(context) { customerId: tenantId, customerName: initial?.summary?.customerName, warehouseId, warehouseName: initial?.summary?.warehouseName, channelCode: initial?.summary?.channelCode, + billOfLadingNo: `P3BL-${suffix}`.slice(0, 128), shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`, + vesselName: `P3VESSEL-${suffix}`.slice(0, 128), voyageNo: `P3V-${suffix}`.slice(0, 64), departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', + estimatedSailingAt: '2026-09-01T08:00:00', estimatedArrivalAt: '2026-09-20T08:00:00', remark: context.marker, parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` }, @@ -148,16 +151,9 @@ export async function run(context) { const bookingCreated = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期订舱', bookingCreateResponse, '', OPTIONS) const bookingId = id(bookingCreated?.oceanBooking?.id) if (!bookingId) return - const bookingSubmitResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/submit`, - { expectedVersion: Number(bookingCreated.oceanBooking.version), reason: context.marker }, - { 'Idempotency-Key': `${context.marker}-p3-booking-submit`.slice(0, 128) }) - const bookingSubmitted = context.apiSuccess(MODULE, '提交三期订舱', bookingSubmitResponse, '', OPTIONS) - const bookingConfirmResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/confirm`, - { expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker }, - { 'Idempotency-Key': `${context.marker}-p3-booking-confirm`.slice(0, 128) }) - const bookingConfirmed = context.apiSuccess(MODULE, '确认真实订舱且不自动生成三期内部柜', bookingConfirmResponse, '', OPTIONS) + const bookingConfirmed = bookingCreated const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ') - context.assert(MODULE, '订舱确认仅冻结真实订舱及箱型需求', bookingConfirmResponse, + context.assert(MODULE, '一次登记直接返回 CONFIRMED 且不自动生成三期内部柜', bookingCreateResponse, bookingConfirmed?.oceanBooking?.status === 'CONFIRMED' && Boolean(requirement?.id) && bookingConfirmed?.containerAllocations?.length === 0, `status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, requirementId=${id(requirement?.id) || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS) @@ -291,7 +287,7 @@ export async function run(context) { voyageNo: `P3V-${suffix}`.slice(0, 64), estimatedSailingAt: sailingAt, estimatedArrivalAt: arrivalAt, reason: context.marker, }, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) }) - const bookingReleased = context.apiSuccess(MODULE, '三期订舱补齐五项运输字段并放舱', releaseResponse, '', OPTIONS) + const bookingReleased = context.apiSuccess(MODULE, '三期订舱携带已冻结运输快照并放舱', releaseResponse, '', OPTIONS) context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse, bookingReleased?.oceanBooking?.status === 'RELEASED' && bookingReleased?.containerAllocations?.some?.((item) => id(item.containerBookingId) === containerBookingId), 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 2c4babb0d..235140da6 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 @@ -7,8 +7,11 @@ const source = readFileSync(new URL('./phase3-order-splitting.mjs', import.meta. 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, /一次登记直接返回 CONFIRMED/) + assert.doesNotMatch(source, /ocean-bookings\/\$\{oceanBookingId\}\/(?:submit|confirm)/) + const createPayload = source.slice(source.indexOf('function bookingPayload'), source.indexOf('async function loadOneFragment')) + for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', + 'estimatedArrivalAt']) assert.match(createPayload, new RegExp(`${field}:`)) assert.match(source, /ocean-booking-link/) assert.match(source, /bookingDetail\.oceanBooking\.status === 'CONFIRMED'/) assert.match(source, /`\$\{bookingPath\}\/release`/) diff --git a/scripts/admin-test-env-modules/phase3-order-splitting.mjs b/scripts/admin-test-env-modules/phase3-order-splitting.mjs index be284b795..c78a3b08d 100644 --- a/scripts/admin-test-env-modules/phase3-order-splitting.mjs +++ b/scripts/admin-test-env-modules/phase3-order-splitting.mjs @@ -122,10 +122,13 @@ function bookingPayload(marker, sequence, source) { customerId: source.tenantId, customerName: source.customerName, channelCode: source.channelCode, warehouseId: source.warehouseId, warehouseName: source.warehouseName, + billOfLadingNo: `BL-${suffix}`.slice(0, 128), shippingCompanyId: null, shippingCompanyName: `SPLIT-SHIPPING-${suffix}`.slice(0, 128), bookingAgentId: null, bookingAgentName: `SPLIT-AGENT-${suffix}`.slice(0, 128), + vesselName: `SPLIT-VESSEL-${suffix}`.slice(0, 128), voyageNo: `SV-${sequence}`.slice(0, 64), departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', - placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', remark: marker, + placeOfDelivery: 'LAX', serviceTerm: 'CY-CY', freightTerm: 'NINGBO', + estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00', remark: marker, parties: [{ partyType: 'SHIPPER', partyName: `SPLIT-SHIPPER-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `SPLIT-CONSIGNEE-${suffix}` }, { partyType: 'NOTIFY_PARTY', partyName: `SPLIT-NOTIFY-${suffix}` }], @@ -151,15 +154,11 @@ async function loadOneFragment(context, state) { const oceanBookingId = id(bookingCreated?.oceanBooking?.id) if (!oceanBookingId) return null context.createdResources.phase3OrderSplitting.retainedAuditRecordIds.push(oceanBookingId) - - const bookingSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/submit`, { - expectedVersion: Number(bookingCreated?.oceanBooking?.version), reason: context.marker, - }, { 'Idempotency-Key': `${operationBase}-booking-submit` }) - const bookingSubmitted = context.apiSuccess(MODULE, `提交 ${sourceOrderNo} 的订舱`, bookingSubmit, '', OPTIONS) - const bookingConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${oceanBookingId}/confirm`, { - expectedVersion: Number(bookingSubmitted?.oceanBooking?.version), reason: context.marker, - }, { 'Idempotency-Key': `${operationBase}-booking-confirm` }) - const bookingConfirmed = context.apiSuccess(MODULE, `确认 ${sourceOrderNo} 的真实订舱`, bookingConfirm, '', OPTIONS) + const bookingConfirmed = bookingCreated + context.assert(MODULE, `${sourceOrderNo} 一次登记直接返回 CONFIRMED`, bookingCreate, + bookingConfirmed?.oceanBooking?.status === 'CONFIRMED' + && bookingConfirmed?.containerAllocations?.length === 0, + `status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, allocations=${bookingConfirmed?.containerAllocations?.length ?? 'n/a'}`, OPTIONS) const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HQ') if (!requirement?.id) return null const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, { 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 7028668b5..5c9ac2e7d 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 @@ -91,7 +91,7 @@ test('bridge loading starts through a real PDA batch before preload confirmation '无真实柜号推送失败后不生成 PDA 任务', '绑定真实柜号后开放三期 PDA 推送门禁', '订舱 CONFIRMED 即使已绑定真实柜号也不生成 PDA 任务', - '三期订舱补齐五项运输字段并放舱', + '三期订舱携带已冻结运输快照并放舱', '三期订舱 RELEASED 后保持模拟柜关联身份', '真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务', "pushedLoadingPlan?.status === 'EDITING'", @@ -153,15 +153,16 @@ test('container loading arranges a simulation before linking a confirmed real bo "const planResponse = await request(context, 'POST', `${SMART}/plans`", "const assignResponse = await request", "const bookingCreate = await request", - "const submit = await request", - "const confirm = await request", "const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`", ]) assert.match(value, /channelCode, warehouseId, warehouseName/) assert.match(value, /publishPlatformLogisticsChannels\(context, warehouseId, MODULE\)/) assert.match(value, /warehouseId, warehouseName/) const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\'')) - assert.doesNotMatch(draft, /billOfLadingNo|vesselName|voyageNo|estimatedSailingAt|estimatedArrivalAt/) + for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) { + assert.match(draft, new RegExp(`${field}:`)) + } + assert.doesNotMatch(value, /loading-booking-(?:submit|confirm)|ocean-bookings\/.*\/(?:submit|confirm)/) assert.match(value, /containerAllocations\.length === 0/) assert.match(value, /version: Number\(linkedContainer\.version\), reason: context\.marker/) assert.doesNotMatch(value, /version: Number\(containerDetail\?\.version \?\? 0\), reason: context\.marker/) @@ -183,7 +184,7 @@ test('container loading arranges a simulation before linking a confirmed real bo assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/) assert.ok(value.includes("'PATCH', `${CONTAINERS}/${containerId}/transport")) assert.match(value, /在放舱前绑定本轮真实箱号/) - assert.match(value, /订舱五项运输字段放舱并满足 PDA 订舱门禁/) + assert.match(value, /订舱携带已冻结运输快照放舱并满足 PDA 门禁/) assert.match(value, /无真实柜号拒绝推送预装计划/) assert.match(value, /柜号与 RELEASED 订舱就绪后推送预装计划/) assert.match(value, /const confirmedTaskAbsent =[\s\S]*if \(!confirmedTaskAbsent\) return/) @@ -263,7 +264,7 @@ test('container loading enforces channel isolation and stable PDA identity acros '无真实柜号推送失败后不生成 PDA 任务', '在放舱前绑定本轮真实箱号', '订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务', - '订舱五项运输字段放舱并满足 PDA 订舱门禁', + '订舱携带已冻结运输快照放舱并满足 PDA 门禁', '柜号与 RELEASED 订舱就绪后推送预装计划', '换柜号前读取 PDA 稳定库存快照', 'PDA 任务创建后替换真实柜号', diff --git a/scripts/full-regression-entry-contract.test.mjs b/scripts/full-regression-entry-contract.test.mjs index 6a7a5f624..f3bd64d95 100644 --- a/scripts/full-regression-entry-contract.test.mjs +++ b/scripts/full-regression-entry-contract.test.mjs @@ -73,7 +73,7 @@ test('test-environment loading chains bind the latest container version before o const bridgeIndex = bridgeLoading.indexOf('原子关联入库预装与柜次装柜计划') const rejectedPushIndex = bridgeLoading.indexOf('无真实柜号拒绝推送柜次计划') const transportIndex = bridgeLoading.indexOf('绑定真实柜号后开放三期 PDA 推送门禁') - const releaseIndex = bridgeLoading.indexOf('三期订舱补齐五项运输字段并放舱') + const releaseIndex = bridgeLoading.indexOf('三期订舱携带已冻结运输快照并放舱') const successfulPushIndex = bridgeLoading.indexOf('真实柜号与 RELEASED 订舱就绪后推送并生成 PDA 任务') const pdaIndex = bridgeLoading.indexOf('读取柜次级 PDA 唯一事实任务') assert.ok(bridgeIndex >= 0 && rejectedPushIndex > bridgeIndex && transportIndex > rejectedPushIndex @@ -85,7 +85,7 @@ test('test-environment loading chains bind the latest container version before o const containerRejectedPushIndex = containerLoading.indexOf('无真实柜号拒绝推送预装计划') const containerTransportIndex = containerLoading.indexOf('在放舱前绑定本轮真实箱号') - const containerReleaseIndex = containerLoading.indexOf('订舱五项运输字段放舱并满足 PDA 订舱门禁') + const containerReleaseIndex = containerLoading.indexOf('订舱携带已冻结运输快照放舱并满足 PDA 门禁') const containerSuccessfulPushIndex = containerLoading.indexOf('柜号与 RELEASED 订舱就绪后推送预装计划') const containerPdaIndex = containerLoading.indexOf('查询本轮柜次 PDA 拣货任务') assert.ok(containerRejectedPushIndex >= 0 && containerTransportIndex > containerRejectedPushIndex