Test #1
@@ -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`, {
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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}`,
|
||||
}
|
||||
|
||||
@@ -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/)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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}` },
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'/)
|
||||
})
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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`/)
|
||||
|
||||
@@ -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`, {
|
||||
|
||||
@@ -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 任务创建后替换真实柜号',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user