diff --git a/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2Test.java b/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2Test.java new file mode 100644 index 000000000..1a5ba5abb --- /dev/null +++ b/common-event-contract/src/test/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2Test.java @@ -0,0 +1,24 @@ +package com.baiyue.common.event; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class FirstLegLoadingPlanCompletedV2Test { + @Test + void carriesTheThreeItemRemarks() throws Exception { + var item = new FirstLegLoadingPlanCompletedV2.Item("1", "SKU", "中文", "EN", 1, + 2, null, null, null, null, null, null, null, null, null, null, null, + "客户备注", "内部备注", "仓库备注"); + var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK", + null, null, null, 1, 2, null, null, null, null, List.of(item)); + var payload = new FirstLegLoadingPlanCompletedV2("4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)); + String json = new ObjectMapper().writeValueAsString(payload); + assertThat(json).contains("customerRemark", "internalRemark", "warehouseRemark"); + assertThat(new ObjectMapper().readTree(json).at("/orders/0/items/0/warehouseRemark").asText()) + .isEqualTo("仓库备注"); + } +} diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java index 709246390..854959615 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/PlatformSmartLoadingControllerContractTest.java @@ -23,7 +23,7 @@ class PlatformSmartLoadingControllerContractTest { authority("assign","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,AssignRequest.class); authority("recommend","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,RecommendRequest.class); authority("adjust","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,long.class,AdjustRequest.class); - authority("warehouseRemark","platform:firstleg:smart-loading:assign",AuthenticatedUser.class,long.class,long.class,WarehouseRemarkRequest.class); + authority("warehouseRemark","platform:firstleg:preload:edit",AuthenticatedUser.class,long.class,long.class,WarehouseRemarkRequest.class); authority("delete","platform:firstleg:smart-loading:delete",AuthenticatedUser.class,long.class,long.class,DeleteRequest.class); authority("push","platform:firstleg:smart-loading:push",AuthenticatedUser.class,long.class,String.class,PushRequest.class); } diff --git a/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs b/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs index 24ada16cb..05894ba14 100644 --- a/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs +++ b/scripts/admin-test-env-modules/fulfillment-list-field-contract.mjs @@ -80,7 +80,7 @@ export function oceanBookingFieldGaps(record) { return gaps(record, { id: isPositiveId, bookingNo: isNonBlank, - status: (value) => ['CONFIRMED', 'CHANGE_PENDING', 'RELEASED', 'CANCELLED'].includes(value), + status: (value) => ['DRAFT', 'RELEASED'].includes(value), vesselName: isNonBlank, voyageNo: isNonBlank, estimatedSailingAt: isIsoDateTime, 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 4380ea249..57b62fb3d 100644 --- a/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs +++ b/scripts/admin-test-env-modules/manual-state-ocean-trucking.mjs @@ -2,7 +2,7 @@ const MODULE = 'manual-state-ocean-trucking' const OPTIONS = { scenario: 'manual-ocean-trucking-state-matrix', deploymentBlocker: true } 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_LIST_STATUSES = ['DRAFT', 'RELEASED'] const OCEAN_STATUSES = [...OCEAN_LIST_STATUSES] const TRUCK_STATUSES = ['DRAFT', 'PENDING_DISPATCH', 'DISPATCHED', 'PICKUP_IN_PROGRESS', 'ARRIVED_WAREHOUSE', 'LOADING', 'DEPARTED', 'COMPLETED', 'RESCHEDULED', 'EXCEPTION', 'CANCELLED'] @@ -36,20 +36,20 @@ function oceanPayload(context, template, index) { remark: `${context.marker} 人工列表状态 ${index}`, parties: template.parties.map(({ partyType, partyName, address, contactName, contactPhone }) => ({ partyType, partyName, address, contactName, contactPhone })), - containers: template.containers.map(({ containerType, containerKind, capacityCbm, maxWeightKg }, containerIndex) => - ({ containerNo: `MAN-${suffix(context, index)}-${String(containerIndex + 1).padStart(2, '0')}`.slice(0, 64), - containerType, containerKind, capacityCbm, maxWeightKg })), + containerRequirements: template.containerRequirements.map(({ containerType, containerKind, quantity, capacityCbm, maxWeightKg }) => + ({ containerType, containerKind, quantity, capacityCbm, maxWeightKg })), } } } 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) - context.assert(MODULE, `订舱样本 ${index} 一次创建直接返回 CONFIRMED`, response, - Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'CONFIRMED', + context.assert(MODULE, `订舱样本 ${index} 创建为 DRAFT 且无真实柜`, response, + Boolean(value?.oceanBooking?.id) && value?.oceanBooking?.status === 'DRAFT' + && (!value?.containers || value.containers.length === 0), `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`) + if (!value?.oceanBooking?.id || value?.oceanBooking?.status !== 'DRAFT') { + throw new Error(`创建订舱样本 ${index} 未进入 DRAFT`) } return value } @@ -64,15 +64,8 @@ async function oceanAction(context, value, endpoint, body = {}) { async function buildOcean(context, template, status, index) { let value = await createOcean(context, template, index) - 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' }) - if (status === 'RELEASED') return oceanAction(context, value, 'release', { - billOfLadingNo: value.oceanBooking.billOfLadingNo, vesselName: value.oceanBooking.vesselName, - voyageNo: value.oceanBooking.voyageNo, estimatedSailingAt: value.oceanBooking.estimatedSailingAt, - estimatedArrivalAt: value.oceanBooking.estimatedArrivalAt }) + if (status === 'DRAFT') return value + if (status === 'RELEASED') return oceanAction(context, value, 'release') throw new Error(`unsupported ocean status ${status}`) } @@ -139,8 +132,8 @@ export async function run(context, matrix = context.createdResources.manualState if (!sourceOceanId) throw new Error('缺少现有订舱生命周期模块生成的模板资源') const templateResponse = await context.request('GET', `${OCEAN_BASE}/${sourceOceanId}`) const template = context.apiSuccess(MODULE, '读取订舱状态矩阵模板', templateResponse, '', OPTIONS) - if (!template?.oceanBooking || !template?.parties?.length || !template?.containers?.length) { - throw new Error('订舱模板缺少渠道、仓库、参与方或真实柜明细') + if (!template?.oceanBooking || !template?.parties?.length || !template?.containerRequirements?.length) { + throw new Error('订舱模板缺少渠道、仓库、参与方或柜型需求') } matrix.pages.oceanBookings = { requiredStatuses: OCEAN_LIST_STATUSES, fixtures: {} } for (const [index, status] of OCEAN_STATUSES.entries()) { diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 86da86c34..3080f0c18 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -161,9 +161,10 @@ export async function run(context) { parties: [{ partyType: 'SHIPPER', partyName: `发货方-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `收货方-${suffix}` }, { partyType: 'NOTIFY_PARTY', partyName: `通知方-${suffix}` }], - containers: [ - { containerNo: realContainerNo, ...primaryRequirement }, - { containerNo: wrongContainerNo, containerType: '20GP', containerKind: 'FCL', capacityCbm: 33, maxWeightKg: 28000 }, + containerRequirements: [ + { containerType: primaryRequirement.containerType, containerKind: primaryRequirement.containerKind, quantity: 1, + capacityCbm: primaryRequirement.capacityCbm, maxWeightKg: primaryRequirement.maxWeightKg }, + { containerType: '20GP', containerKind: 'FCL', quantity: 1, capacityCbm: 33, maxWeightKg: 28000 }, ], } context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName), @@ -373,8 +374,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 bookingId = id(bookingCreated?.oceanBooking?.id) + const bookingCreatedDraft = context.apiSuccess(MODULE, '登记含渠道仓库和运输快照的 DRAFT 订舱', bookingCreate, '', OPTIONS) + const bookingId = id(bookingCreatedDraft?.oceanBooking?.id) + const bookingRelease = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { + expectedVersion: Number(bookingCreatedDraft?.oceanBooking?.version), reason: context.marker, + }, { 'Idempotency-Key': `${context.marker}-loading-booking-release`.slice(0, 128) }) + const bookingCreated = context.apiSuccess(MODULE, '放舱订舱并冻结运输事实', bookingRelease, '', OPTIONS) const bookingNo = id(bookingCreated?.oceanBooking?.bookingNo) if (!bookingId) return const confirmedBooking = bookingCreated @@ -383,35 +388,31 @@ export async function run(context) { const frozenTransportMatches = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '' && String(frozenTransport[field]) === String(bookingDraft[field])) - context.assert(MODULE, '一次登记直接返回 CONFIRMED 且尚未占用模拟柜', bookingCreate, - confirmedBooking?.oceanBooking?.status === 'CONFIRMED' - && Array.isArray(confirmedBooking?.containers) && confirmedBooking.containers.length === 2, - `status=${confirmedBooking?.oceanBooking?.status || 'missing'}, containers=${confirmedBooking?.containers?.length ?? 'n/a'}`, OPTIONS) - context.assert(MODULE, '已确认订舱回显完整且一致的冻结运输快照', bookingCreate, + context.assert(MODULE, '订舱放舱为 RELEASED 且不预生成真实货柜', bookingRelease, + confirmedBooking?.oceanBooking?.status === 'RELEASED' + && Array.isArray(confirmedBooking?.containerRequirements) && confirmedBooking.containerRequirements.length === 2 + && (!confirmedBooking?.containers || confirmedBooking.containers.length === 0), + `status=${confirmedBooking?.oceanBooking?.status || 'missing'}, requirements=${confirmedBooking?.containerRequirements?.length ?? 'n/a'}, containers=${confirmedBooking?.containers?.length ?? 0}`, OPTIONS) + context.assert(MODULE, 'RELEASED 订舱回显完整且一致的冻结运输快照', bookingRelease, frozenTransportMatches, transportFields.map((field) => `${field}=${frozenTransport[field] || 'missing'}`).join(', '), OPTIONS) if (!frozenTransportMatches) return const confirmedListResponse = await context.request('GET', `/api/platform/v1/ocean-bookings?page=1&size=20&keyword=${encodeURIComponent(bookingNo)}`) const confirmedList = context.apiSuccess(MODULE, '按真实订舱号查询正式列表', confirmedListResponse, '', OPTIONS) - context.assert(MODULE, 'CONFIRMED 后真实订舱进入正式列表', confirmedListResponse, - records(confirmedList).some((item) => id(item.id) === bookingId && item.status === 'CONFIRMED'), + context.assert(MODULE, 'RELEASED 订舱进入正式列表', confirmedListResponse, + records(confirmedList).some((item) => id(item.id) === bookingId && item.status === 'RELEASED'), `bookingId=${bookingId}, records=${records(confirmedList).length}`, OPTIONS) - const requirement = confirmedBooking?.containers?.find?.((item) => item.containerNo === realContainerNo) - const wrongTypeRequirement = confirmedBooking?.containers?.find?.((item) => item.containerNo === wrongContainerNo) - if (!requirement?.containerBookingId || !wrongTypeRequirement?.containerBookingId) return - const wrongTypeLinkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, { - realContainerBookingId: id(wrongTypeRequirement.containerBookingId), expectedVersion: Number(simulationBeforeLink.version), - }) - context.addStep(MODULE, '箱型不匹配拒绝关联真实订舱柜', wrongTypeLinkResponse, - failure(wrongTypeLinkResponse), `HTTP=${wrongTypeLinkResponse.http}, code=${codeOf(wrongTypeLinkResponse)}`, OPTIONS) + const requirement = confirmedBooking?.containerRequirements?.find?.((item) => item.containerType === primaryRequirement.containerType) + if (!requirement?.id) return const beforeLinkItems = JSON.stringify(assigned.items?.map((item) => ({ id: id(item.id), inboundId: id(item.inboundId), packageCount: Number(item.packageCount) })) || []) const linkResponse = await request(context, 'POST', `${CONTAINERS}/${containerId}/ocean-booking-link`, { - realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(simulationBeforeLink.version), + oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo: realContainerNo, + expectedVersion: Number(simulationBeforeLink.version), }) - const linkedContainer = context.apiSuccess(MODULE, '首次关联真实已确认订舱柜', linkResponse, + const linkedContainer = context.apiSuccess(MODULE, '首次关联 RELEASED 订舱需求并补真实柜号', linkResponse, responseEvidence(linkResponse), OPTIONS) if (!linkedContainer) return const afterLinkPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`) @@ -426,24 +427,23 @@ export async function run(context) { const activeContainersResponse = await context.request('GET', `${CONTAINERS}?page=1&size=50&keyword=${encodeURIComponent(realContainerNo)}`) - const activeContainers = context.apiSuccess(MODULE, '合并后按真实柜号回查正常货柜列表', activeContainersResponse, '', OPTIONS) + const activeContainers = context.apiSuccess(MODULE, '关联后按真实柜号回查正常货柜列表', activeContainersResponse, '', OPTIONS) const activeRealRows = records(activeContainers).filter((item) => item.containerNo === realContainerNo) - context.assert(MODULE, '合并后列表仅保留模拟柜原地转成的一条 ACTIVE 操作身份', activeContainersResponse, - activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId - && !records(activeContainers).some((item) => id(item?.id) === id(requirement.containerBookingId)), - `activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}, mergedSourceId=${id(requirement.containerBookingId)}`, OPTIONS) + context.assert(MODULE, '关联后列表仅保留模拟柜原地补全的一条 ACTIVE 操作身份', activeContainersResponse, + activeRealRows.length === 1 && id(activeRealRows[0]?.id) === containerId, + `activeIds=${activeRealRows.map((item) => id(item?.id)).join('|') || 'none'}`, OPTIONS) const containerGaps = containerFieldGaps(activeRealRows[0], { requireContainerNo: true }) context.assert(MODULE, '货柜列表关键展示字段来自正式柜次快照且格式完整', activeContainersResponse, activeRealRows.length === 1 && containerGaps.length === 0, `missing=${containerGaps.join(',') || 'none'}`, OPTIONS) const mergedBookingResponse = await context.request('GET', `/api/platform/v1/ocean-bookings/${bookingId}`) - const mergedBooking = context.apiSuccess(MODULE, '合并后回查订舱真实柜当前操作身份', mergedBookingResponse, '', OPTIONS) + const mergedBooking = context.apiSuccess(MODULE, '关联后回查订舱 allocation 当前操作身份', mergedBookingResponse, '', OPTIONS) const mergedContainer = mergedBooking?.containers?.find?.((item) => item.containerNo === realContainerNo) - context.assert(MODULE, '订舱真实柜 allocation 已原子改指稳定模拟柜身份', mergedBookingResponse, + context.assert(MODULE, '订舱 allocation 原子创建并指向稳定模拟柜身份', mergedBookingResponse, id(mergedContainer?.containerBookingId) === containerId - && id(mergedContainer?.requirementId) === id(requirement.requirementId) - && id(mergedContainer?.allocationId) === id(requirement.allocationId), + && id(mergedContainer?.requirementId) === id(requirement.id) + && Boolean(mergedContainer?.allocationId), `containerBookingId=${id(mergedContainer?.containerBookingId) || 'missing'}, requirementId=${id(mergedContainer?.requirementId) || 'missing'}, allocationId=${id(mergedContainer?.allocationId) || 'missing'}`, OPTIONS) const duplicateSimulationResponse = await request(context, 'POST', CONTAINERS, { @@ -452,9 +452,10 @@ export async function run(context) { }) const duplicateSimulation = context.apiSuccess(MODULE, '新增重复容量占用负向模拟柜', duplicateSimulationResponse, '', OPTIONS) const duplicateLinkResponse = await request(context, 'POST', `${CONTAINERS}/${id(duplicateSimulation?.id)}/ocean-booking-link`, { - realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(duplicateSimulation?.version), + oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo: realContainerNo, + expectedVersion: Number(duplicateSimulation?.version), }) - context.addStep(MODULE, '已消费 MERGED 真实柜拒绝再次关联', duplicateLinkResponse, + context.addStep(MODULE, '已消费订舱需求拒绝再次关联', duplicateLinkResponse, failure(duplicateLinkResponse), `HTTP=${duplicateLinkResponse.http}, code=${codeOf(duplicateLinkResponse)}`, OPTIONS) const unlinkAfterLoadingResponse = await request(context, 'DELETE', `${CONTAINERS}/${containerId}/ocean-booking-link`, { diff --git a/scripts/admin-test-env-modules/phase3-loading.mjs b/scripts/admin-test-env-modules/phase3-loading.mjs index 3cef58256..387e95ce9 100644 --- a/scripts/admin-test-env-modules/phase3-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-loading.mjs @@ -136,7 +136,7 @@ export async function run(context) { parties: [{ partyType: 'SHIPPER', partyName: `P3-SHIPPER-${suffix}` }, { partyType: 'CONSIGNEE', partyName: `P3-CONSIGNEE-${suffix}` }, { partyType: 'NOTIFY_PARTY', partyName: `P3-NOTIFY-${suffix}` }], - containers: [{ containerNo, containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, + containerRequirements: [{ containerType: '40HC', containerKind: 'FCL', quantity: 1, capacityCbm: 76, maxWeightKg: 28000 }], } const simulationResponse = await request(context, 'POST', '/api/platform/v1/containers', { @@ -287,49 +287,41 @@ export async function run(context) { const bookingCreateResponse = await request(context, 'POST', '/api/platform/v1/ocean-bookings', { expectedVersion: 0, data: bookingDraft }, { 'Idempotency-Key': `${context.marker}-p3-booking-create`.slice(0, 128) }) - const bookingConfirmed = context.apiSuccess(MODULE, '创建带渠道仓库快照的三期 CONFIRMED 订舱', bookingCreateResponse, '', OPTIONS) - const bookingId = id(bookingConfirmed?.oceanBooking?.id) + const bookingDraftResponse = context.apiSuccess(MODULE, '创建三期 DRAFT 订舱(不生成真实柜)', bookingCreateResponse, '', OPTIONS) + const bookingId = id(bookingDraftResponse?.oceanBooking?.id) + const draftVersion = Number(bookingDraftResponse?.oceanBooking?.version) + const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, + { expectedVersion: draftVersion, reason: context.marker }, + { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) }) + const bookingConfirmed = context.apiSuccess(MODULE, '三期订舱 RELEASED 冻结运输快照', releaseResponse, '', OPTIONS) const transportFields = ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt'] const frozenTransport = Object.fromEntries(transportFields.map((field) => [field, bookingConfirmed?.oceanBooking?.[field]])) const frozenTransportValid = transportFields.every((field) => frozenTransport[field] != null && String(frozenTransport[field]).trim() !== '') const frozenTransportMatches = frozenTransportValid && transportFields.every((field) => String(frozenTransport[field]) === String(bookingDraft[field])) - const requirement = bookingConfirmed?.containers?.find?.((item) => item.containerNo === containerNo) - context.assert(MODULE, 'CONFIRMED 订舱回显完整且冻结五项运输快照', bookingCreateResponse, - Boolean(bookingId) && frozenTransportMatches && Boolean(requirement?.containerBookingId) - && bookingConfirmed?.containers?.length === 1, + const requirement = bookingConfirmed?.containerRequirements?.find?.((item) => item.containerType === '40HC') + context.assert(MODULE, 'RELEASED 订舱回显完整冻结运输快照且无真实柜号', releaseResponse, + Boolean(bookingId) && bookingConfirmed?.oceanBooking?.status === 'RELEASED' && frozenTransportMatches + && Boolean(requirement?.id) && (!bookingConfirmed?.containers || bookingConfirmed.containers.length === 0), `bookingId=${bookingId || 'missing'}, status=${bookingConfirmed?.oceanBooking?.status || 'missing'}`, OPTIONS) - if (!bookingId || !requirement?.containerBookingId || !frozenTransportMatches) return + if (!bookingId || !requirement?.id || !frozenTransportMatches) return const latestContainerResponse = await context.request('GET', `/api/platform/v1/containers/${containerBookingId}`) const latestContainer = context.apiSuccess(MODULE, '合并前读取模拟柜最新版本', latestContainerResponse, '', OPTIONS) const linkResponse = await request(context, 'POST', `/api/platform/v1/containers/${containerBookingId}/ocean-booking-link`, { - realContainerBookingId: id(requirement.containerBookingId), expectedVersion: Number(latestContainer?.version), + oceanBookingId: bookingId, requirementId: id(requirement.id), containerNo, + expectedVersion: Number(latestContainer?.version), }) - const linked = context.apiSuccess(MODULE, '模拟柜原地合并三期 CONFIRMED 真实订舱柜', linkResponse, '', OPTIONS) + const linked = context.apiSuccess(MODULE, '模拟柜原地关联三期 RELEASED 订舱需求并补真实柜号', linkResponse, '', OPTIONS) context.assert(MODULE, '订舱关联保持柜次计划与真实柜号身份', linkResponse, id(linked?.id) === containerBookingId && linked?.cabinetNo === simulation.cabinetNo && linked?.containerNo === containerNo && id(linked?.oceanBookingId) === bookingId, `containerBookingId=${containerBookingId}, loadingPlanId=${loadingPlanId}`, OPTIONS) if (id(linked?.id) !== containerBookingId) return - const confirmedBookingTaskResponse = await context.request('GET', - `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`) - context.addStep(MODULE, '订舱已关联但未 RELEASED 仍无 PDA 任务', confirmedBookingTaskResponse, - confirmedBookingTaskResponse.http === 200 && codeOf(confirmedBookingTaskResponse) === 'A0404', - `HTTP=${confirmedBookingTaskResponse.http}, code=${codeOf(confirmedBookingTaskResponse)}`, OPTIONS) - - const releaseResponse = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${bookingId}/release`, { - expectedVersion: Number(bookingConfirmed?.oceanBooking?.version), - ...frozenTransport, reason: context.marker, - }, { 'Idempotency-Key': `${context.marker}-p3-booking-release`.slice(0, 128) }) - const bookingReleased = context.apiSuccess(MODULE, '三期订舱携带已冻结运输快照并放舱', releaseResponse, '', OPTIONS) - context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份', releaseResponse, - bookingReleased?.oceanBooking?.status === 'RELEASED' - && bookingReleased?.containers?.some?.((item) => id(item.containerBookingId) === containerBookingId - && item.containerNo === containerNo) - && transportFields.every((field) => String(bookingReleased?.oceanBooking?.[field]) === String(frozenTransport[field])), - `status=${bookingReleased?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS) - if (bookingReleased?.oceanBooking?.status !== 'RELEASED') return + context.assert(MODULE, '三期订舱 RELEASED 后保持模拟柜关联身份和运输快照', releaseResponse, + bookingConfirmed?.oceanBooking?.status === 'RELEASED' + && id(linked?.oceanBookingId) === bookingId && linked?.containerNo === containerNo, + `status=${bookingConfirmed?.oceanBooking?.status || 'missing'}, containerBookingId=${containerBookingId}`, OPTIONS) const unconfirmedTaskResponse = await context.request('GET', `${pdaBase}/tasks?loadingPlanId=${encodeURIComponent(loadingPlanId)}&warehouseId=${encodeURIComponent(warehouseId)}`)