This commit is contained in:
2026-08-14 18:50:07 +08:00
parent 9b36978448
commit 10d3390bb1
17 changed files with 461 additions and 61 deletions
@@ -26,4 +26,8 @@ test('统一初始化 SQL 提供人工验收所需公共仓储主数据', () =>
"'CUS-001'", "'CUSTOMER'", "'SHIPPER'", "'EXP-TEST'", "'IT 测试快递'",
]) assert.ok(sql.includes(marker), `missing initialization marker: ${marker}`)
assert.match(sql, /'EXP-TEST', 'IT 测试快递'[\s\S]*?'ACTIVE'/)
const palletSeed = sql.match(/INSERT INTO wh_pallet \([\s\S]*?remark = VALUES\(remark\);/)?.[0]
assert.ok(palletSeed, 'missing pallet initialization seed')
assert.doesNotMatch(palletSeed, /\bstatus\b|'AVAILABLE'/)
})
@@ -100,7 +100,7 @@ async function createConfirmedContainer(context, source, label) {
const summary = source.detail.summary
const draft = {
customerId: source.tenantId, customerName: summary.customerName || 'IT 测试客户',
channelCode: summary.channelCode, channelName: summary.channelName, warehouseId: source.warehouseId,
channelCode: summary.channelCode, warehouseId: source.warehouseId,
warehouseName: summary.warehouseName, shippingCompanyId: null, shippingCompanyName: `人工船司-${value}`,
bookingAgentId: null, bookingAgentName: `人工代理-${value}`, departurePortCode: 'CNSZX',
departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶',
@@ -20,7 +20,7 @@ function oceanPayload(context, template, index) {
const source = template.oceanBooking
return { expectedVersion: 0, data: {
customerId: source.customerId, customerName: source.customerName,
channelCode: source.channelCode, channelName: source.channelName,
channelCode: source.channelCode,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
shippingCompanyId: source.shippingCompanyId,
shippingCompanyName: source.shippingCompanyName, bookingAgentId: source.bookingAgentId,
@@ -0,0 +1,47 @@
import assert from 'node:assert/strict'
import { readFileSync, readdirSync } from 'node:fs'
import test from 'node:test'
const adminPageUrl = new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue', import.meta.url)
const adminTestUrl = new URL('../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts', import.meta.url)
const adminApiUrl = new URL('../../../apps/admin-web/src/modules/ocean-bookings/api/platformOceanBookings.ts', import.meta.url)
const openapiUrl = new URL('../../firstleg/src/main/resources/openapi/', import.meta.url)
function source(url) { return readFileSync(url, 'utf8') }
test('Admin ocean booking selects a warehouse-scoped logistics channel instead of accepting free text', () => {
const page = source(adminPageUrl)
const api = source(adminApiUrl)
const pageTest = source(adminTestUrl)
assert.doesNotMatch(page, /<el-input[^>]*v-model="form\.data\.channelCode"/)
assert.doesNotMatch(page, /<el-input[^>]*v-model="form\.data\.channelName"/)
assert.match(page, /<el-form-item[^>]*label="物流渠道"[^>]*>[\s\S]*?<el-select[^>]*:model-value="form\.data\.channelCode"[^>]*@update:model-value="selectChannel\(/)
assert.match(`${page}\n${api}`, /originWarehouseId/)
assert.match(api, /basePath\s*=\s*['"]\/platform\/v1\/ocean-bookings['"][\s\S]*\$\{basePath\}\/channels/)
assert.doesNotMatch(api, /price-books[\s\S]*for \(const version of versions\)/)
assert.match(page, /shippingCompanyId/)
assert.match(page, /channelCode/)
assert.doesNotMatch(pageTest, /expect\(source\)\.toContain\(['"]v-model="form\.data\.channel(?:Code|Name)"['"]\)/)
assert.match(pageTest, /expect\(source\)\.toContain\(['"]:model-value="form\.data\.channelCode" filterable['"]\)/)
assert.match(pageTest, /expect\(source\)\.not\.toContain\(['"]<el-input v-model="form\.data\.channelCode"['"]\)/)
})
test('backend OpenAPI freezes a platform channel query scoped by originWarehouseId', () => {
const documents = readdirSync(openapiUrl)
.filter((name) => name.endsWith('.yaml'))
.map((name) => source(new URL(name, openapiUrl)))
const platformChannelContract = documents.find((value) =>
/\/api\/platform\/v1\/ocean-bookings\/channels/.test(value) && /originWarehouseId/.test(value)
&& /server validates it against the current ACTIVE price book/.test(value))
assert.ok(platformChannelContract,
'BLOCKED_CONTRACT: platform logistics-channel query by originWarehouseId is not frozen in OpenAPI')
})
test('container confirmation acceptance locks the selected channel onto the generated container', () => {
const fixture = source(new URL('./phase3-container-loading.mjs', import.meta.url))
assert.match(fixture, /containerAllocations\?\.\[0\]/)
assert.match(fixture, /containerDetail\?\.channelCode === channelCode/)
assert.match(fixture, /selectPlatformChannelIsolationPair/)
assert.doesNotMatch(fixture, /\$\{channelCode\}-OTHER|foreignChannelCode/)
})
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { oceanBookingDraft, resolveOceanBookingMaster } from './ocean-booking-fixture.mjs'
import { oceanBookingDraft, publishPlatformLogisticsChannels, resolveOceanBookingMaster } from './ocean-booking-fixture.mjs'
test('ocean booking fixture resolves the current inbound customer channel and warehouse', async () => {
const steps = []
@@ -31,14 +31,15 @@ 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 carries all required DTO fields without release transport data', () => {
test('shared booking draft omits server-owned channelName and release transport data', () => {
const draft = oceanBookingDraft({ marker: 'ADMIN-RUN-3' }, '3', {
customerId: '21', customerName: 'IT 测试客户', channelCode: 'SEA-US', channelName: '美西海运',
warehouseId: '41', warehouseName: '深圳仓',
})
for (const key of ['customerId', 'customerName', 'channelCode', 'channelName', 'warehouseId', 'warehouseName']) {
for (const key of ['customerId', 'customerName', 'channelCode', 'warehouseId', 'warehouseName']) {
assert.ok(draft[key], `${key} is required`)
}
assert.equal(draft.channelName, undefined)
assert.equal(draft.containerRequirements[0].capacityCbm, 68)
assert.equal(draft.containerRequirements[0].maxWeightKg, 28000)
for (const key of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt']) {
@@ -46,6 +47,45 @@ test('shared booking draft carries all required DTO fields without release trans
}
})
test('platform channel producer queries the selected origin warehouse and publishes current-marker resources', async () => {
const calls = []
const context = {
marker: 'ADMIN-RUN-CHANNELS', createdResources: {},
request: async (method, path) => {
calls.push({ method, path })
return { body: { code: '200', data: [
{ channelCode: 'SEA-A', channelName: '海运 A' },
{ channelCode: 'SEA-B', channelName: '海运 B' },
] } }
},
apiSuccess: (_module, _name, response) => response.body.data,
assert: (_module, _name, _response, condition) => assert.equal(condition, true),
}
const catalog = await publishPlatformLogisticsChannels(context, '41', 'test')
assert.deepEqual(calls, [{ method: 'GET', path: '/api/platform/v1/ocean-bookings/channels?originWarehouseId=41' }])
assert.equal(catalog.marker, context.marker)
assert.equal(catalog.originWarehouseId, '41')
assert.deepEqual(catalog.channels.map((value) => value.channelCode), ['SEA-A', 'SEA-B'])
assert.deepEqual(context.createdResources.platformLogisticsChannels, catalog)
})
test('all direct ocean booking payload builders omit channelName under the frozen DraftData contract', () => {
const regions = [
['manual-state-loading-pda.mjs', 'const draft = {', 'const createResponse'],
['manual-state-ocean-trucking.mjs', 'function oceanPayload', 'async function createOcean'],
['phase3-loading.mjs', 'const bookingDraft = {', 'const bookingCreate'],
['phase3-order-splitting.mjs', 'function bookingPayload', 'async function loadOneFragment'],
['phase3-container-loading.mjs', 'const bookingDraft = {', "context.assert(MODULE, '订舱渠道与仓库快照完整'"],
]
for (const [name, start, end] of regions) {
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)
}
const container = readFileSync(new URL('phase3-container-loading.mjs', import.meta.url), 'utf8')
assert.doesNotMatch(container, /data:\s*\{[^}]*channelName\s*:/s)
})
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')
@@ -53,6 +93,6 @@ test('all independent ocean booking producers use the shared master-data contrac
assert.match(source, /oceanBookingDraft\(context, suffix, master/, name)
}
const container = readFileSync(new URL('phase3-container-loading.mjs', import.meta.url), 'utf8')
for (const snippet of ['channelCode: channelCode', 'channelName', 'warehouseId', 'warehouseName',
for (const snippet of ['channelCode', 'publishPlatformLogisticsChannels', 'warehouseId', 'warehouseName',
'capacityCbm: 100', 'maxWeightKg: 100000']) assert.match(container, new RegExp(snippet), snippet)
})
@@ -1,4 +1,25 @@
function text(value) { return value == null ? '' : String(value).trim() }
const PLATFORM_CHANNELS = '/api/platform/v1/ocean-bookings/channels'
export async function publishPlatformLogisticsChannels(context, originWarehouseId, module) {
const warehouseId = text(originWarehouseId)
const sourcePath = `${PLATFORM_CHANNELS}?originWarehouseId=${encodeURIComponent(warehouseId)}`
const response = await context.request('GET', sourcePath)
const data = context.apiSuccess(module, '按起运仓查询平台正式物流渠道', response,
`originWarehouseId=${warehouseId}`, { scenario: 'ocean-booking-channel-catalog', deploymentBlocker: true })
const channels = (Array.isArray(data) ? data : []).flatMap((value) => {
const channelCode = text(value?.channelCode)
const channelName = text(value?.channelName)
return channelCode && channelName ? [{ channelCode, channelName }] : []
})
const resource = { marker: context.marker, originWarehouseId: warehouseId, sourcePath, channels }
context.createdResources.platformLogisticsChannels = resource
context.assert(module, '平台渠道目录归属本轮起运仓且不含空渠道', response,
Boolean(warehouseId) && channels.length > 0,
`originWarehouseId=${warehouseId}, channels=${channels.map((value) => value.channelCode).join('|') || 'none'}`,
{ scenario: 'ocean-booking-channel-catalog', deploymentBlocker: true })
return resource
}
export async function resolveOceanBookingMaster(context, module) {
const resources = context.createdResources || {}
@@ -32,8 +53,9 @@ export async function resolveOceanBookingMaster(context, module) {
}
export function oceanBookingDraft(context, suffix, master, labels = {}) {
const { channelName: _readOnlyChannelName, ...writableMaster } = master
return {
...master,
...writableMaster,
shippingCompanyId: null, shippingCompanyName: labels.shippingCompanyName || `验收船公司-${suffix}`,
bookingAgentId: null, bookingAgentName: labels.bookingAgentName || `验收代理-${suffix}`,
vesselName: labels.vesselName, voyageNo: labels.voyageNo,
@@ -1,3 +1,5 @@
import { publishPlatformLogisticsChannels } from './ocean-booking-fixture.mjs'
const MODULE = 'phase3-container-loading'
const OPTIONS = { scenario: 'phase3-container-smart-loading-pda', deploymentBlocker: true }
const CONTAINERS = '/api/platform/v1/containers'
@@ -46,6 +48,32 @@ export function selectContainerLoadingFixture(context) {
return candidates.find((candidate) => candidate.marker === context.marker) || candidates[0]
}
export function selectPlatformChannelIsolationPair(catalog, marker, originWarehouseId, primaryChannelCode) {
const catalogMarker = id(catalog?.marker)
const catalogWarehouseId = id(catalog?.originWarehouseId)
const sourcePath = id(catalog?.sourcePath)
if (catalogMarker !== id(marker) || catalogWarehouseId !== id(originWarehouseId)
|| !sourcePath.startsWith('/api/platform/')) {
return { pair: null, reason: 'BLOCKED_FIXTURE: missing current-marker platform channel catalog for origin warehouse' }
}
const channels = []
const seen = new Set()
for (const value of Array.isArray(catalog?.channels) ? catalog.channels : []) {
const channelCode = id(value?.channelCode).trim()
const channelName = id(value?.channelName).trim()
const status = id(value?.status).trim().toUpperCase()
if (!channelCode || !channelName || (status && status !== 'ACTIVE') || seen.has(channelCode)) continue
seen.add(channelCode)
channels.push({ channelCode, channelName })
}
const primary = channels.find((value) => value.channelCode === id(primaryChannelCode))
const foreign = channels.find((value) => value.channelCode !== id(primaryChannelCode))
if (!primary || !foreign) {
return { pair: null, reason: `BLOCKED_FIXTURE: ACTIVE platform channels=${channels.length}, distinct pair unavailable` }
}
return { pair: { primary, foreign }, reason: null }
}
export async function run(context) {
const source = selectContainerLoadingFixture(context)
const tenantId = id(source?.tenantId)
@@ -70,11 +98,19 @@ export async function run(context) {
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
const channelCode = id(inbound?.summary?.channelCode || source?.channelCode)
const channelName = inbound?.summary?.channelName || source?.channelName || channelCode
const warehouseName = inbound?.summary?.warehouseName || source?.warehouseName
if (!channelCode || !warehouseName) return
const channelCatalog = await publishPlatformLogisticsChannels(context, warehouseId, MODULE)
const selectedChannel = channelCatalog.channels.find((value) => value.channelCode === channelCode)
context.assert(MODULE, '入库渠道存在于当前起运仓 ACTIVE 平台渠道目录', inboundResponse,
Boolean(selectedChannel),
`originWarehouseId=${warehouseId}, channelCode=${channelCode}, catalog=${channelCatalog.channels.map((value) => value.channelCode).join('|') || 'none'}`,
OPTIONS)
if (!selectedChannel) return
const channelName = selectedChannel.channelName
const bookingDraft = {
customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`,
channelCode: channelCode || 'FBA', channelName, warehouseId, warehouseName,
channelCode, warehouseId, warehouseName,
shippingCompanyId: null, shippingCompanyName: `验收船公司-${suffix}`,
bookingAgentId: null, bookingAgentName: `验收代理-${suffix}`,
departurePortCode: 'CNSZX', departurePortName: '深圳',
@@ -87,7 +123,6 @@ export async function run(context) {
}
context.assert(MODULE, '订舱渠道与仓库快照完整', inboundResponse, Boolean(channelCode && channelName && warehouseName),
`channelCode=${channelCode || 'missing'}, warehouseId=${warehouseId}, warehouseName=${warehouseName || 'missing'}`, OPTIONS)
if (!channelCode || !warehouseName) return
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)
@@ -118,35 +153,42 @@ export async function run(context) {
`candidateFound=${Boolean(candidate)}, available=${candidate?.availablePackageCount ?? 'missing'}`, OPTIONS)
if (!candidate?.inboundDetailId) return
const foreignChannelCode = `${channelCode}-OTHER`.slice(0, 64)
const foreignBookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', {
expectedVersion: 0,
data: { ...bookingDraft, channelCode: foreignChannelCode, channelName: `异渠道-${suffix}`, remark: `${context.marker}-other-channel` },
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-create`.slice(0, 128) })
const foreignBookingCreated = context.apiSuccess(MODULE, '创建异渠道隔离订舱', foreignBookingCreate, '', OPTIONS)
const foreignBookingId = id(foreignBookingCreated?.oceanBooking?.id)
if (!foreignBookingId) return
const foreignSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/submit`, {
expectedVersion: Number(foreignBookingCreated?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-submit`.slice(0, 128) })
const foreignSubmitted = context.apiSuccess(MODULE, '提交异渠道隔离订舱', foreignSubmit, '', OPTIONS)
const foreignConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/confirm`, {
expectedVersion: Number(foreignSubmitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-confirm`.slice(0, 128) })
const foreignConfirmed = context.apiSuccess(MODULE, '确认异渠道订舱并自动生成柜次', foreignConfirm, '', OPTIONS)
const foreignContainerId = id(foreignConfirmed?.containerAllocations?.[0]?.containerBookingId)
if (!foreignContainerId) return
const foreignPlanResponse = await request(context, 'POST', `${SMART}/plans`, {
bookingId: foreignContainerId, remark: `${context.marker}-other-channel`,
})
const foreignPlan = context.apiSuccess(MODULE, '创建异渠道智能装柜计划', foreignPlanResponse, '', OPTIONS)
const foreignAssignResponse = await request(context, 'POST', `${SMART}/plans/${foreignPlan?.id}/assignments`, {
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(foreignPlan?.version), operationId: `${context.marker}-other-channel-assign`.slice(0, 128),
})
context.addStep(MODULE, '异渠道入库单拒绝装入当前柜次', foreignAssignResponse,
foreignAssignResponse.http === 200 && ['A0422', '422'].includes(codeOf(foreignAssignResponse)),
`HTTP=${foreignAssignResponse.http}, code=${codeOf(foreignAssignResponse)}`, OPTIONS)
const channelIsolation = selectPlatformChannelIsolationPair(
context.createdResources?.platformLogisticsChannels, context.marker, warehouseId, channelCode)
if (!channelIsolation.pair) {
context.addStep(MODULE, '两个真实有效渠道的柜次隔离 fixture', null, false,
`${channelIsolation.reason}; originWarehouseId=${warehouseId}, primaryChannelCode=${channelCode}`, OPTIONS)
} else {
const foreignChannel = channelIsolation.pair.foreign
const foreignBookingCreate = await request(context, 'POST', '/api/platform/v1/ocean-bookings', {
expectedVersion: 0,
data: { ...bookingDraft, channelCode: foreignChannel.channelCode, remark: `${context.marker}-other-channel` },
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-create`.slice(0, 128) })
const foreignBookingCreated = context.apiSuccess(MODULE, '创建真实异渠道隔离订舱', foreignBookingCreate, '', OPTIONS)
const foreignBookingId = id(foreignBookingCreated?.oceanBooking?.id)
if (!foreignBookingId) return
const foreignSubmit = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/submit`, {
expectedVersion: Number(foreignBookingCreated?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-submit`.slice(0, 128) })
const foreignSubmitted = context.apiSuccess(MODULE, '提交真实异渠道隔离订舱', foreignSubmit, '', OPTIONS)
const foreignConfirm = await request(context, 'POST', `/api/platform/v1/ocean-bookings/${foreignBookingId}/confirm`, {
expectedVersion: Number(foreignSubmitted?.oceanBooking?.version), reason: context.marker,
}, { 'Idempotency-Key': `${context.marker}-other-channel-booking-confirm`.slice(0, 128) })
const foreignConfirmed = context.apiSuccess(MODULE, '确认真实异渠道订舱并自动生成柜次', foreignConfirm, '', OPTIONS)
const foreignContainerId = id(foreignConfirmed?.containerAllocations?.[0]?.containerBookingId)
if (!foreignContainerId) return
const foreignPlanResponse = await request(context, 'POST', `${SMART}/plans`, {
bookingId: foreignContainerId, remark: `${context.marker}-other-channel`,
})
const foreignPlan = context.apiSuccess(MODULE, '创建真实异渠道智能装柜计划', foreignPlanResponse, '', OPTIONS)
const foreignAssignResponse = await request(context, 'POST', `${SMART}/plans/${foreignPlan?.id}/assignments`, {
inboundId, inboundDetailId: id(candidate.inboundDetailId), packageCount: 1,
expectedVersion: Number(foreignPlan?.version), operationId: `${context.marker}-other-channel-assign`.slice(0, 128),
})
context.addStep(MODULE, '两个真实有效渠道之间拒绝跨渠道装柜', foreignAssignResponse,
foreignAssignResponse.http === 200 && ['A0422', '422'].includes(codeOf(foreignAssignResponse)),
`primary=${channelCode}, foreign=${foreignChannel.channelCode}, HTTP=${foreignAssignResponse.http}, code=${codeOf(foreignAssignResponse)}`, OPTIONS)
}
const containerDetailResponse = await context.request('GET', `${CONTAINERS}/${containerId}`)
const containerDetail = context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS)
@@ -120,7 +120,7 @@ export async function run(context) {
const bookingDraft = {
customerId: tenantId, customerName: initial?.summary?.customerName,
warehouseId, warehouseName: initial?.summary?.warehouseName,
channelCode: initial?.summary?.channelCode, channelName: initial?.summary?.channelName,
channelCode: initial?.summary?.channelCode,
shippingCompanyName: `P3-SHIPPING-${suffix}`, bookingAgentName: `P3-AGENT-${suffix}`,
departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX',
destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', serviceTerm: 'CY/CY', freightTerm: 'PREPAID',
@@ -120,7 +120,7 @@ function bookingPayload(marker, sequence, source) {
const suffix = `${marker}-${sequence}`.replace(/[^A-Za-z0-9]/g, '').slice(-18)
return {
customerId: source.tenantId, customerName: source.customerName,
channelCode: source.channelCode, channelName: source.channelName,
channelCode: source.channelCode,
warehouseId: source.warehouseId, warehouseName: source.warehouseName,
shippingCompanyId: null, shippingCompanyName: `SPLIT-SHIPPING-${suffix}`.slice(0, 128),
bookingAgentId: null, bookingAgentName: `SPLIT-AGENT-${suffix}`.slice(0, 128),
@@ -4,7 +4,7 @@ import test from 'node:test'
import { selectCustomsFlow } from './phase3-customs.mjs'
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
import { selectContainerLoadingFixture } from './phase3-container-loading.mjs'
import { selectContainerLoadingFixture, selectPlatformChannelIsolationPair } from './phase3-container-loading.mjs'
import { AcceptanceRun, normalizeModuleOrder, parseArguments } from '../admin-test-env-acceptance.mjs'
function source(name) {
@@ -141,7 +141,8 @@ test('container loading creates and confirms booking before scoped candidates',
"const planResponse = await request(context, 'POST', `${SMART}/plans`",
"const assignResponse = await request",
])
assert.match(value, /channelCode: channelCode/)
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/)
@@ -182,12 +183,46 @@ test('container loading keeps an unmatched candidate so marker failure stays obs
assert.equal(selected.marker, 'ADMIN-RUN-CAB')
})
test('channel isolation accepts only two active channels from the current platform warehouse catalog', () => {
const selected = selectPlatformChannelIsolationPair({
marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41',
sourcePath: '/api/platform/v1/frozen-channel-contract?originWarehouseId=41',
channels: [
{ channelCode: 'SEA-A', channelName: '海运 A', status: 'ACTIVE' },
{ channelCode: 'SEA-B', channelName: '海运 B', status: 'ACTIVE' },
{ channelCode: 'SEA-C', channelName: '海运 C', status: 'DISABLED' },
],
}, 'ADMIN-RUN-AUTO1', '41', 'SEA-A')
assert.deepEqual(selected.pair, {
primary: { channelCode: 'SEA-A', channelName: '海运 A' },
foreign: { channelCode: 'SEA-B', channelName: '海运 B' },
})
assert.equal(selected.reason, null)
})
test('channel isolation reports BLOCKED_FIXTURE instead of inventing a second channel', () => {
for (const catalog of [
{ marker: 'OLD-RUN', originWarehouseId: '41', sourcePath: '/api/platform/v1/channels', channels: [] },
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '42', sourcePath: '/api/platform/v1/channels', channels: [] },
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41', sourcePath: '/api/merchant/v1/first-leg/pricing/channels', channels: [] },
{ marker: 'ADMIN-RUN-AUTO1', originWarehouseId: '41', sourcePath: '/api/platform/v1/channels',
channels: [{ channelCode: 'SEA-A', channelName: '海运 A', status: 'ACTIVE' }] },
]) {
const selected = selectPlatformChannelIsolationPair(catalog, 'ADMIN-RUN-AUTO1', '41', 'SEA-A')
assert.equal(selected.pair, null)
assert.match(selected.reason, /^BLOCKED_FIXTURE:/)
}
const value = source('./phase3-container-loading.mjs')
assert.doesNotMatch(value, /foreignChannelCode|\$\{channelCode\}-OTHER|异渠道-\$\{suffix\}/)
assert.match(value, /context\.createdResources\?\.platformLogisticsChannels/)
})
test('container loading enforces channel isolation and stable PDA identity across container number changes', () => {
const value = source('./phase3-container-loading.mjs')
assertOrder(value, [
'确认订舱并自动生成真实柜次',
'创建异渠道隔离订舱',
'异渠道入库单拒绝装入当前柜次',
'创建真实异渠道隔离订舱',
'两个真实有效渠道之间拒绝跨渠道装柜',
'无真实柜号时计划可排柜但 PDA 任务不开放',
'在放舱前绑定本轮真实箱号',
'订舱 CONFIRMED 即使已有真实柜号也不开放 PDA 任务',